공부중/시스템 해킹

[Dreamhack] Memory Corruption: Use After Free

silver surfer 2024. 10. 5.

원룸을 임대하여 거주하다가 계약이 만료될 경우, 세입자는 임대인에게 원룸 접근 권한을 반납해야 한다. 열쇠로 문을 열어왔다면 열쇠를 돌려주고, 도어락을 사용한다면 도어락 비밀번호를 재설정해줘야 한다. 그러면 임대인은 원룸을 청소하고 다시 세입자를 구한다.

만약 이전 세입자의 접근 권한을 회수하지 않는다면, 계약이 끝난 뒤에도 그 원룸은 무단으로 사용될 수 있다. 또한 만약 방을 깨끗이 청소하지 않아서 이전 세입자의 개인정보가 적혀있는 문서가 남는다면, 다음 세입자가 이전 세입자의 개인정보를 알게 될 위험도 있다. 따라서 "접근 권한 회수"와 "깨끗한 청소"를 마친 뒤에 새 임대인을 구하는 것이 바람직하다.

ptmalloc2 를 이용하여 메모리를 관리할 때도 이런 과정에 주의를 기울이지 않으면 비슷한 문제가 발생할 수 있다. 이번 강의에서 배울 Use-After-Free메모리 참조에 사용한 포인터를 메모리 해제 후에 적절히 초기화하지 않아서, 또는 해제한 메모리를 초기화하지 않고 다음 청크에 재할당 해주면서 발생하는 취약점이다. 이 취약점은 현재까지도 브라우저 및 커널에서 자주 발견되고 있으며, 익스플로잇 성공률도 다른 취약점에 비해 높아 상당히 위험하다고 알려져있다.

이번 강의에서는 Use-After-Free의 원인과 취약점이 발생하는 코드의 패턴, 그리고 공격자의 관점에서 해당 취약점을 이용했을 때 얻을 수 있는 효과에 대해 배워본다.

 

실습환경 Dockerfile

Ubuntu 18.04 64-bit(Glibc 2.27) 실습 환경 구축을 위한 Dockerfile은 다음과 같습니다. 다른 버전의 우분투를 사용하는 경우, 이번 강의의 실습을 수행하는 과정이 원활하지 않을 수 있으니 반드시 우분투 18.04 64-bit 환경을 구축한 후 실습하시기를 바랍니다.

FROM ubuntu:18.04

ENV PATH="${PATH}:/usr/local/lib/python3.6/dist-packages/bin"
ENV LC_CTYPE=C.UTF-8

RUN apt update
RUN apt install -y \
    gcc \
    git \
    python3 \
    python3-pip \
    ruby \
    sudo \
    tmux \
    vim \
    wget

# install pwndbg
WORKDIR /root
RUN git clone https://github.com/pwndbg/pwndbg
WORKDIR /root/pwndbg
RUN git checkout 2023.03.19
RUN ./setup.sh

# install pwntools
RUN pip3 install --upgrade pip
RUN pip3 install pwntools

# install one_gadget command
RUN gem install one_gadget

WORKDIR /root

 

위 내용을 Dockerfile 이라는 이름의 파일로 저장한 후, 아래의 명령어로 이미지를 빌드하고 컨테이너를 실행한 후 셸을 켤 수 있습니다.

$ IMAGE_NAME=ubuntu1804 CONTAINER_NAME=my_container; \
docker build . -t $IMAGE_NAME; \
docker run -d -t --privileged --name=$CONTAINER_NAME $IMAGE_NAME; \
docker exec -it -u root $CONTAINER_NAME bash

 

** Use After Free

Dangling Pointer

컴퓨터 과학에서, Dangling Pointer는 유효하지 않은 메모리 영역을 가리키는 포인터를 말한다. 메모리의 동적 할당에 사용되는 malloc 함수는 할당한 메모리의 주소를 반환한다. 일반적으로 메모리를 동적할당할 때는 포인터를 선언하고, 그 포인터에 malloc 함수가 할당한 메모리의 주소를 저장한다. 그리고 그 포인터를 참조하여 할당한 메모리에 접근한다.

메모리를 해제할 때는 free 함수를 호출한다. 그런데 free 함수는 청크를 ptmalloc에 반환하기만할 뿐, 청크의 주소를 담고있던 포인터를 초기화하지는 않는다. 따라서 free의 호출 이후, 프로그래머가 포인터를 초기화하지 않으면, 포인터는 해제된 청크를 가리키는 Dangling Pointer가 된다.

Dangling Pointer가 생긴다고 해서 프로그램이 보안적으로 취약한 것은 아니다. 그러나 Dangling Pointer는 프로그램이 예상치 못한 동작을 할 가능성을 키우며, 경우에 따라서는 공격자에게 공격 수단으로 활용될 수도 있다.

아래의 코드는 Dangling Pointer의 위험성을 보이는 예제이다.

// Name: dangling_ptr.c
// Compile: gcc -o dangling_ptr dangling_ptr.c
#include <stdio.h>
#include <stdlib.h>

int main() {
  char *ptr = NULL;
  int idx;

  while (1) {
    printf("> ");
    scanf("%d", &idx);
    switch (idx) {
      case 1:
        if (ptr) {
          printf("Already allocated\n");
          break;
        }
        ptr = malloc(256);
        break;
      case 2:
        if (!ptr) {
          printf("Empty\n");
        }
        free(ptr);
        break;
      default:
        break;
    }
  }
}

 

예제에서는 청크를 해제한 후에 청크를 가리키던 ptr 변수를 초기화하지 않는다. 따라서 다음과 같이 청크를 할당하고 해제하면, ptr은 이전에 할당한 청크의 주소를 가리키는 Dangling Pointer가 된다.

$ gcc -o dangling_ptr dangling_ptr.c -no-pie
$ ./dangling_ptr
> 1
> 2

ptr이 해제된 청크의 주소를 가리키고 있으므로, 이를 다시 해제할 수 있다.

 

$ ./dangling_ptr
> 1
> 2
> 2
free(): double free detected in tcache 2
Aborted (core dumped)

이를 Double Free Bug라고 하는데, 프로그램에 심각한 보안 위협이 되는 소프트웨어 취약점이다. 이에 대해서는 다음 강의에서 자세히 배우도록 한다.

 

* Use After Free

Use-After-Free(UAF)는 문자 그대로, 해제된 메모리에 접근할 수 있을 때 발생하는 취약점을 말한다. 앞서 살펴봤던 dangling_ptr.c와 같이 Dangling Pointer로 인해 발생하기도 하지만, 새롭게 할당한 영역을 초기화하지 않고 사용하면서 발생하기도 한다.

malloc과 free 함수는 할당 또는 해제할 메모리의 데이터들을 초기화하지 않는다. 그래서 새롭게 할당한 청크를 프로그래머가 명시적으로 초기화하지 않으면, 메모리에 남아있던 데이터가 유출되거나 사용될 수 있다.

아래 코드는 Use-After-Free 취약점이 있는 예제 코드이다. 구조체 NameTag와 Secret이 정의되어 있는데, 예제에서는 그 중 외부에 유출되면 안 되는 Secret 구조체를 먼저 할당한다. 그리고 secret_name, secret_info, code에 값을 입력하고, 이를 해제한다.

// Name: uaf.c
// Compile: gcc -o uaf uaf.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct NameTag {
  char team_name[16];
  char name[32];
  void (*func)();
};

struct Secret {
  char secret_name[16];
  char secret_info[32];
  long code;
};

int main() {
  int idx;

  struct NameTag *nametag;
  struct Secret *secret;

  secret = malloc(sizeof(struct Secret));

  strcpy(secret->secret_name, "ADMIN PASSWORD");
  strcpy(secret->secret_info, "P@ssw0rd!@#");
  secret->code = 0x1337;

  free(secret);
  secret = NULL;

  nametag = malloc(sizeof(struct NameTag));

  strcpy(nametag->team_name, "security team");
  memcpy(nametag->name, "S", 1);

  printf("Team Name: %s\n", nametag->team_name);
  printf("Name: %s\n", nametag->name);

  if (nametag->func) {
    printf("Nametag function: %p\n", nametag->func);
    nametag->func();
  }
}

코드의 34번째 줄부터는 사원의 정보를 담고있는 nametag을 생성한다. team_name, name에 각각의 값을 입력하고, 입력한 데이터를 출력한다. 이후에 함수 포인터 func가 NULL이 아니라면 포인터가 가리키는 주소를 출력하고, 해당 주소의 함수를 호출한다. 예제 코드의 실행 결과는 다음과 같다.

$ gcc -o uaf uaf.c -no-pie
$ ./uaf
Team Name: security team
Name: S@ssw0rd!@#
Nametag function: 0x1337
Segmentation fault (core dumped)

memcpy(nametag->name, "S", 1); 에서 1번째 바이트에 S를 저장해서 P@ssw0rd!@# 가 아닌 S@ssw0rd!@#가 출력된다.

출력 결과를 살펴보면, Name으로 secret_infor의 문자열이 출력되고, 값을 입력한 적 없는 함수 포인터가 0x1337을 가리키는 것을 확인할 수 있다. 이러한 결과가 나타난 이유를 자세히 알아보자.

 

* uaf 동적 분석

ptmalloc2는 새로운 할당 요청이 들어왔을 때, 요청된 크기와 비슷한 청크가 bin이나 tcache에 있는지 확인한다. 그리고 만약 있다면, 해당 청크를 꺼내어 재사용한다. 예제 코드에서 Nametag과 Secret은 같은 크기의 구조체이다. 그러므로 앞서 할당한 secret을 해제하고, nametag를 할당하면, nametag는 secret과 같은 메모리 영역을 사용하게 된다. 이 때 free는 해제한 메모리의 데이터를 초기화하지 않으므로, nametag에는 secret의 값이 일부 남아있게 된다.

gdb를 이용하여 secret을 해제한 후, secret이 사용하던 메모리 영역의 데이터를 살펴보자. 먼저 gdb로 uaf 바이너리를 열고, secret을 해제(free)하는 다음 명령어 부분에 중단점을 설정한 후 실행한다.

$ gdb uaf
pwndbg> disass main
Dump of assembler code for function main:
...
   0x0000000000400647 <+96>:    mov    rax,QWORD PTR [rbp-0x10]
   0x000000000040064b <+100>:   mov    rdi,rax
   0x000000000040064e <+103>:   call   0x4004c0 <free@plt>
   0x0000000000400653 <+108>:   mov    QWORD PTR [rbp-0x10],0x0
...
End of assembler dump.
pwndbg> b *main+108
Breakpoint 1 at 0x400653
pwndbg> r
Starting program: /home/dreamhack/uaf

Breakpoint 1, 0x0000000000400653 in main ()
...
───────────────────────────────────[ DISASM ]───────────────────────────────────
 ► 0x400653 <main+108>    mov    qword ptr [rbp - 0x10], 0
...
Breakpoint *main+108

 

이제 heap 명령어로 할당 및 해제된 청크들의 정보를 조회해본다.

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x602000
Size: 0x251

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x602250
Size: 0x41
fd: 0x00

Top chunk | PREV_INUSE
Addr: 0x602290
Size: 0x20d71
}

총 3개가 있는데 0x602250 (0x405290)이 우리가 살펴보고자 하는 secret에 해당하는 청크이다. 해제(free)되었기 때문에 tcache의 엔트리에 들어가있는 상태이다.

추가로 0x602000(0x405000)은 tcache와 관련된 공간으로 tcache_perthread_struct 구조체에 해당하며, libc 단에서 힙 영역을 초기화할 때 할당하는 청크이다. tcache에 대해서는 이후 강의에서 자세히 다룰 예정이다. 0x602290(0x4052d0)는 탑 청크에 해당한다.

다음은 이미 해제된 secret이 사용하던 메모리 영역을 출력한 모습이다. secret_name에 해당하는 부분은 적절한 fd와 bk 값으로 초기화 됐지만, secret_info에 해당하는 부분은 값이 그래도 남아있는 모습을 확인할 수 있다.

pwndbg> x/10gx 0x602250
0x602250:	0x0000000000000000	0x0000000000000041
0x602260:	0x0000000000000000	0x0000000000602010
0x602270:	0x6472307773734050	0x0000000000234021
0x602280:	0x0000000000000000	0x0000000000000000
0x602290:	0x0000000000001337	0x0000000000020d71
pwndbg> x/s 0x602270
0x602270:	"P@ssw0rd!@#"
pwndbg>

 

다음으로, nametag를 할당하고, printf 함수를 호출하는 시점에서 nametag 멤버 변수들의 값을 확인해본다.

pwndbg> b *main+207
Breakpoint 2 at 0x4006b6
pwndbg> c
Continuing.

Breakpoint 2, 0x00000000004006b6 in main ()
...
───────────────────────────────────[ DISASM ]───────────────────────────────────
 ► 0x4006b6 <main+207>    call   printf@plt <0x4004d0>
        format: 0x4007a6 ◂— 'Team Name: %s\n'
        vararg: 0x602260 ◂— 'security team'

   0x4006bb <main+212>    mov    rax, qword ptr [rbp - 8]
   0x4006bf <main+216>    add    rax, 0x10
...
Breakpoint *main+207
pwndbg> x/10gx 0x602250
0x602250:   0x0000000000000000  0x0000000000000041
0x602260:   0x7974697275636573  0x0000006d61657420
0x602270:   0x6472307773734053  0x0000000000234021
0x602280:   0x0000000000000000  0x0000000000000000
0x602290:   0x0000000000001337  0x0000000000020d71
pwndbg> x/s 0x602260
0x602260:   "security team"
pwndbg> x/s 0x602270
0x602270:   "S@ssw0rd!@#"
pwndbg> x/gx 0x602290
0x602290:   0x0000000000001337
pwndbg>

도커에서 하는게 나을 듯...

nametag->team_name 에는 "security team"이 그대로 입력되었으나 nametag->name 에는 초기화 되지 않은 secret_info의 값이 존재하는 것을 확인할 수 있다.

또한 nametag->func 위치에 secret->code에 대입했던 0x1337이 남아있는 것을 알 수 있다. 이 값이 0이 아니므로 예제의 42번째 줄에서 nametag->func이 호출되고, Segmentation Fault가 발생한다.

예제를 통해 살펴봤듯, 동적할당한 청크를 해제한 뒤에는 해제된 메모리 영역에 이전 객체의 데이터가 남는다. 이러한 특징을 공격자가 이용한다면 초기화되지 않은 메모리의 값을 읽어내거나, 새로운 객체가 악의적인 값을 사용하도록 유도하여 프로그램의 정상적인 실행을 방해할 수 있다.

 

마치며

마치며

이번 강의에서는 해제된 메모리의 접근을 허용함으로써 발생하는 Use-After-Free 취약점에 대해 배워보았습니다. Use-After-Free 취약점을 통해 공격자는 초기화되지 않은 메모리의 값을 읽어내거나, 새로운 객체가 악의적인 값을 사용하도록 유도할 수 있습니다. 다음 강의에서는 앞서 간단히 언급한 Double Free Bug에 대해 배워보겠습니다. 🚩

키워드
  • Dangling Pointer: 해제된 메모리를 가리키고 있는 포인터. UAF가 발생하는 원인이 될 수 있음.
  • Use-After-Free (UAF): 해제된 메모리에 접근할 수 있을 때 발생하는 취약점

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

댓글