Memory Corruption: Use After Free

Dreamhack
#System Hacking

0 views

들어가며

1. 서론

image.png
  • 원룸을 임대하여 거주하다가 계약이 만료될 경우, 세입자는 임대인에게 원룸 접근 권한을 반납해야 한다.
    • 열쇠로 문을 열어왔다면 열쇠를 돌려주고, 도어락을 사용한다면 도어락 비밀번호를 재설정해줘야 한다.
    • 그러면 임대인은 원룸을 청소하고 다시 세입자를 구한다.
  • 만약 이전 세입자의 접근 권한을 회수하지 않는다면, 계약이 끝난 뒤에도 그 원룸은 무단으로 사용될 수 있다.
    • 또한, 만약 방을 깨끗이 청소하지 않아서 이전 세입자의 개인 정보가 적혀있는 문서가 남는다면, 다음 세입자가 이전 세입자의 개인 정보를 알게 될 위험도 있다.
    • 따라서 “접근 권한 회수”와 “깨끗한 청소”를 마치고 난 뒤에 새 임대인을 구하는 것이 바람직하다.
  • ptmalloc2를 이용하여 메모리를 관리할 때도 이런 과정에 주의를 기울이지 않으면 비슷한 문제가 발생할 수 있다.
    • 이번 강의에서 배울 Use-After-Free는 메모리 참조에 사용한 포인터를 메모리 해제 후에 적절히 초기화하지 않아서, 또는 해제한 메모리를 초기화하지 않고 다음 청크에 재할당해주면서 발생하는 취약점이다.
    • 이 취약점은 현재까지도 브라우저 및 커널에서 자주 발견되고 있으며, 익스플로잇 성공률도 다른 취약점에 비해 높아 상당히 위험하다고 알려져 있다.
  • 이번 강의에서는 Use-After-Free의 원인과 취약점이 발생하는 코드의 패턴, 그리고 공격자의 관점에서 해당 취약점을 이용했을 때 얻을 수 있는 효과에 대해 배워보겠다.

2. 실습 환경 Dockerfile

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

2.1 Ubuntu 18.04 64-bit(Glibc 2.27) 실습 환경 Dockerfile

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 이라는 이름의 파일로 저장한 후, 아래의 명령어로 이미지를 빌드하고 컨테이너를 실행한 후 셸을 켤 수 있다.

2.2 도커 이미지 빌드/컨테이너 실행/셸 실행 명령어

$ 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

1. Dangling Pointer

  • 컴퓨터 과학에서, Dangling Pointer는 유효하지 않은 메모리 영역을 가리키는 포인터를 말한다.
    • 메모리의 동적 할당에 사용되는 malloc 함수는 할당한 메모리의 주소를 반환한다.
    • 일반적으로, 메모리를 동적 할당할 때는 포인터를 선언하고, 그 포인터에 malloc함수가 할당한 메모리의 주소를 저장한다.
    • 그리고 그 포인터를 참조하여 할당한 메모리에 접근한다.
  • 메모리를 해제할 때는 free 함수를 호출한다.
    • 그런데 free 함수는 청크를 ptmalloc에 반환하기만 할 뿐, 청크의 주소를 담고 있던 포인터를 초기화하지는 않는다.
    • 따라서 free의 호출 이후에 프로그래머가 포인터를 초기화하지 않으면, 포인터는 해제된 청크를 가리키는 Dangling Pointer가 된다.
  • Dangling Pointer가 생긴다고 해서 프로그램이 보안적으로 취약한 것은 아니다.
    • 그러나 Dangling Pointer는 프로그램이 예상치 못한 동작을 할 가능성을 키우며, 경우에 따라서는 공격자에게 공격 수단으로 활용될 수도 있다.
  • 아래 코드는 Dangling Pointer의 위험성을 보이는 예제다.

1.1 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 라고 하는데, 프로그램에 심각한 보안 위협이 되는 소프트웨어 취약점이므로 명심해두도록 하자. 이 강의에서는 깊게 다루지는 않도록 하겠다.

2. Use After Free

  • Use-After-Free (UAF)는 문자 그대로, 해제된 메모리에 접근할 수 있을 때 발생하는 취약점을 말한다.
    • 앞서 살펴봤던 dangling_ptr.c와 같이 Dangling Pointer로 인해 발생하기도 하지만, 새롭게 할당한 영역을 초기화하지 않고 사용하면서 발생하기도 한다.
  • mallocfree 함수는 할당 또는 해제할 메모리의 데이터들을 초기화하지 않는다.
    • 그래서 새롭게 할당한 청크를 프로그래머가 명시적으로 초기화하지 않으면, 메모리에 남아있던 데이터가 유출되거나 사용될 수 있다.
  • 아래 코드는 Use-After-Free 취약점이 있는 예제 코드다.
    • 구조체 NameTagSecret이 정의되어 있는데, 예제에서는 그 중 외부에 유출되면 안 되는 Secret 구조체를 먼저 할당한다.
    • 그리고 secret_name, secret_info, code에 값을 입력하고, 이를 해제한다.

2.1 Use-After-Free 취약점 예제 코드

// 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)
  • 출력 결과를 살펴보면, Name으로 secret_info 의 문자열이 출력되고, 값을 입력한 적 없는 함수 포인터가 0x1337을 가리키는 것을 확인할 수 있다. 이러한 결과가 나타난 이유를 자세히 알아보겠다.

3. uaf 동적 분석

  • ptmalloc2 는 새로운 할당 요청이 들어왔을 때, 요청된 크기와 비슷한 청크가 bin이나 tcache에 있는지 확인한다.
    • 그리고 만약 있다면, 해당 청크를 꺼내어 재사용한다.
    • 예제 코드에서 NametagSecret은 같은 크기의 구조체다.
    • 그러므로 앞서 할당한 secret을 해제하고 nametag를 할당하면, nametagsecret과 같은 메모리 영역을 사용하게 된다.
    • 이때 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이 우리가 살펴보고자 하는 secret에 해당하는 청크다.

    • 해제(free)되었기 때문에 tcache의 엔트리에 들어가 있는 상태다.

추가로 0x602000는 tcache와 관련된 공간으로 tcache_perthread_struct 구조체에 해당하며, libc 단에서
힙 영역을 초기화할 때 할당하는 청크다. 0x602290는 탑 청크에 해당한다.

  • 다음은 이미 해제된 secret이 사용하던 메모리 영역을 출력한 모습이다.

    • secret_name에 해당하는 부분은 적절한 fdbk값으로 초기화됐지만, 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가 발생한다.
  • 예제를 통해 살펴봤듯, 동적 할당한 청크를 해제한 뒤에는 해제된 메모리 영역에 이전 객체의 데이터가 남는다.

    • 이러한 특징을 공격자가 이용한다면 초기화되지 않은 메모리의 값을 읽어내거나, 새로운 객체가 악의적인 값을 사용하도록 유도하여 프로그램의 정상적인 실행을 방해할 수 있다.

마치며

1. 서론

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

1.1 키워드

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

Loading comments...