Memory Corruption: Double Free Bug

Dreamhack
#System Hacking

0 views

들어가며

1. 서론

image.png
  • free 함수로 청크를 해제하면, ptmalloc2는 이를 tcache나 bins에 추가하여 관리한다.
    • 그리고 이후에 malloc으로 비슷한 크기의 동적 할당이 발생하면, 이 연결 리스트들을 탐색하여 청크를 재할당해준다.
    • 이 메커니즘에서, 해커들은 free로 해제한 청크를 free로 다시 해제했을 때 발생하는 현상에 주목했다.
  • Tcache와 bins를 free list라고 통칭한다면, free list의 관점에서 free는 청크를 추가하는 함수, malloc은 청크를 꺼내는 함수이다.
    • 그러므로, 임의의 청크에 대해 free를 두 번이상 적용할 수 있다는 것은, 같은 청크를 free list에 여러 번 추가할 수 있음을 의미한다.
  • 청크가 free list에 중복해서 존재하면 청크가 duplicated 됐다고 표현하는데, 해커들은 duplicated free list를 이용하면 임의 주소에 청크를 할당할 수 있음을 밝혀냈다.
    • 이렇게 할당한 청크의 값을 읽거나 조작함으로써 해커는 임의 주소 읽기 또는 쓰기를 할 수 있다.
    • 자세한 방법과 원리에 대해서는 이번 강의와 다음 강의를 통해 천천히 설명하겠다.
  • 이상의 이유로, 같은 청크를 중복해서 해제할 수 있는 코드는 보안상의 약점으로 분류되어 Double Free Bug (DFB) 라고 불린다.
    • 이번 강의에서는 Double free bug가 발생하는 원인과 효과, 그리고 이를 막기 위해 도입된 보호 기법에 대해 알아보겠다.
  • 힙에 대한 공격 기법은 연구되고 발전하면서 힙과 관련된 새로운 보호 기법들이 탄생하게 되었다.
    • 그로 인해 Glibc 버전이 높아질수록 공격의 복잡도가 올라가며, 그만큼 공격 기법을 이해하기가 어려워진다.
    • 따라서 본 강의에서는 실습의 편의상 Glibc 2.27 버전이 내장된 Ubuntu 18.04 64-bit 환경을 기준으로 설명한다.

2. 실습 환경 Dockerfile

2.1 실습 환경 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
    

Double Free Bug

1. Double Free Bug

  • Double Free Bug (DFB)는 같은 청크를 두 번 해제할 수 있는 버그를 말한다.
    • ptmalloc2에서 발생하는 버그 중 하나이며, 공격자에게 임의 주소 쓰기, 임의 주소 읽기, 임의 코드 실행, 서비스 거부 등의 수단으로 활용될 수 있다.
  • dangling pointer는 Double free bug를 유발하는 대표적인 원인이다.
    • 코드 상에서 dangling pointer가 생성되는지, 그리고 이를 대상으로 free를 호출하는 것이 가능한지 살피면 Double free bug가 존재하는지 가늠할 수 있다.
  • Double free bug를 이용하면 duplicated free list를 만드는 것이 가능한데, 이는 청크와 연결리스트의 구조때문이다.
    • ptmalloc2에서, free list의 각 청크들은 fdbk로 연결된다.
    • fd는 자신보다 이후에 해제된 청크를, bk는 이전에 해제된 청크를 가리킨다.
  • 그런데, 해제된 청크에서 fdbk 값을 저장하는 공간은 할당된 청크에서 데이터를 저장하는 데 사용된다.
    • 그러므로 만약 어떤 청크가 free list에 중복해서 포함된다면, 첫 번째 재할당에서 fdbk를 조작하여 free list에 임의 주소를 포함시킬 수 있다.
image.png
  • 초기에는 double free에 대한 검사가 미흡하여 Double free bug가 있으면 손쉽게 트리거할 수 있었다.
    • 특히, glibc 2.26 버전부터 도입된 tcache는 도입 당시에 보호 기법이 전무하여 double free의 쉬운 먹잇감이 되었다.
  • 하지만 시간이 흐르면서 관련한 보호 기법이 glibc에 구현되었고 이를 우회하지 않으면 같은 청크를 두 번 해제하는 즉시 프로세스가 종료된다.

1.1 Tcache Double Free

// Name: dfb.c
// Compile: gcc -o dfb dfb.c

#include <stdio.h>
#include <stdlib.h>

int main() {
  char *chunk;
  chunk = malloc(0x50);

  printf("Address of chunk: %p\n", chunk);

  free(chunk);
  free(chunk); // Free again
}
  • 위 코드는 같은 청크를 두 번 해제하는 예제 코드이다.
    • 컴파일하고 실행하면 tcache에 대한 double free가 감지되어 프로그램이 비정상 종료되는 것을 확인할 수 있다.
$ ./dfb
Address of chunk: 0x55ce62641260
free(): double free detected in tcache 2
zsh: abort      ./dfb

Mitigation for Tcache DFB

1. 정적 패치 분석

1.1 tcache_entry

  • Tcache에 도입된 보호 기법을 분석하기 위해, 패치된 코드의 diff를 살펴보겠다.
    • 먼저, 하단의 코드를 보면 double free를 탐지하기 위해 key포인터가 tcache_entry에 추가되었음을 알 수 있다.
typedef struct tcache_entry {
  struct tcache_entry *next;
+ /* This field exists to detect double frees.  */
+ struct tcache_perthread_struct *key;
} tcache_entry;
  • tcache_entry는 해제된 tcache 청크들이 갖는 구조다.
    • 일반 청크의 fdnext로 대체되고, LIFO 형태로 사용되므로 bk에 대응되는 값은 없다.

1.2 tcache_put

tcache_put(mchunkptr chunk, size_t tc_idx) {
  tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
  assert(tc_idx < TCACHE_MAX_BINS);
  
+ /* Mark this chunk as "in the tcache" so the test in _int_free will detect a
+      double free.  */
+ e->key = tcache;
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}
  • tcache_put은 해제한 청크를 tcache에 추가하는 함수다.
    • 상단의 코드를 보면 tcache_put 함수는 해제되는 청크의 keytcache라는 값을 대입하도록 변경되었다.
    • 여기서 tcachetcache_perthread라는 구조체 변수를 가리킨다.

1.3 tcache_get

tcache_get (size_t tc_idx)
   assert (tcache->entries[tc_idx] > 0);
   tcache->entries[tc_idx] = e->next;
   --(tcache->counts[tc_idx]);
+  e->key = NULL;
   return (void *) e;
 }
  • tcache_get은 tcache에 연결된 청크를 재사용할 때 사용하는 함수다.
    • 상단의 코드를 보면 tcache_get함수는 재사용하는 청크의 key값에 NULL을 대입하도록 변경되었다.

1.4 _int_free

  • _int_free은 청크를 해제할 때 호출되는 함수다.
    • 하단의 코드의 20번째 줄 이하를 보면, 재할당하려는 청크의 key값이 tcache이면 Double Free가 발생했다고 보고 프로그램을 abort시킨다.
_int_free (mstate av, mchunkptr p, int have_lock)
 #if USE_TCACHE
    {
      size_t tc_idx = csize2tidx (size);
-
-    if (tcache
-       && tc_idx < mp_.tcache_bins
-       && tcache->counts[tc_idx] < mp_.tcache_count)
+    if (tcache != NULL && tc_idx < mp_.tcache_bins)
        {
-       tcache_put (p, tc_idx);
-       return;
+       /* Check to see if it's already in the tcache.  */
+       tcache_entry *e = (tcache_entry *) chunk2mem (p);
+
+       /* This test succeeds on double free.  However, we don't 100%
+          trust it (it also matches random payload data at a 1 in
+          2^<size_t> chance), so verify it's not an unlikely
+          coincidence before aborting.  */
+       if (__glibc_unlikely (e->key == tcache))
+         {
+           tcache_entry *tmp;
+           LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
+           for (tmp = tcache->entries[tc_idx];
+                tmp;
+                tmp = tmp->next)
+             if (tmp == e)
+               malloc_printerr ("free(): double free detected in tcache 2");
+           /* If we get here, it was a coincidence.  We've wasted a
+              few cycles, but don't abort.  */
+         }
+
+       if (tcache->counts[tc_idx] < mp_.tcache_count)
+         {
+           tcache_put (p, tc_idx);
+           return;
+         }
        }
    }
  #endif
  • 그 외의 보호 기법은 없으므로, 20번째 줄의 조건문만 통과하면 double free를 일으킬 수 있다.

2. 동적 분석

  • 이번에는 gdb를 이용하여 보호 기법의 적용 과정을 동적 분석해보겠다.
    • 먼저, 청크 할당 직후에 중단점을 설정하고 실행한다.
$ gdb -q double_free
pwndbg> disass main
   0x00005555555546da <+0>:     push   rbp
   0x00005555555546db <+1>:     mov    rbp,rsp
   0x00005555555546de <+4>:     sub    rsp,0x10
   0x00005555555546e2 <+8>:     mov    edi,0x50
   0x00005555555546e7 <+13>:    call   0x5555555545b0 <malloc@plt>
   0x00005555555546ec <+18>:    mov    QWORD PTR [rbp-0x8],rax
   ...
pwndbg> b *main+18
Breakpoint 1 at 0x5555555546ec
pwndbg> r
  • heap 명령어로 청크들의 정보를 조회하면 다음과 같다.
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555756000
Size: 0x251

Allocated chunk | PREV_INUSE
Addr: 0x555555756250
Size: 0x61

Top chunk | PREV_INUSE
Addr: 0x5555557562b0
Size: 0x20d51
  • 이 중 malloc(0x50)으로 생성한 chunk의 주소는 0x555555756250 이다.
    • 해당 메모리 값을 덤프하면, 아무런 데이터가 입력되지 않았음을 확인할 수 있다.
pwndbg> x/4gx 0x555555756250
0x555555756250: 0x0000000000000000      0x0000000000000061
0x555555756260: 0x0000000000000000      0x0000000000000000
  • 이후의 참조를 위해 청크를 gdb에서 chunk 변수로 정의하고 넘어가겠다.
pwndbg> set $chunk=(tcache_entry *)0x555555756260
  • chunk를 해제할 때까지 실행하고, 청크의 메모리를 출력하면 다음과 같다.
pwndbg> disass main
   0x0000555555554703 <+41>:    call   0x5555555545a0 <printf@plt>
   0x0000555555554708 <+46>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000055555555470c <+50>:    mov    rdi,rax
   0x000055555555470f <+53>:    call   0x555555554590 <free@plt>
   0x0000555555554714 <+58>:    mov    rax,QWORD PTR [rbp-0x8]
pwndbg> b *main+58
Breakpoint 2 at 0x0000555555554714
pwndbg> c
pwndbg> print *$chunk
$1 = {
  next = 0x0,
  key = 0x555555756010
}
  • chunkkey값이 0x555555756010로 설정된 것을 확인할 수 있다.

  • 이 주소의 메모리 값을 조회하면, 해제한 chunk의 주소 0x555555756260가 entry에 포함되어 있음을 알 수 있는데, 이는 tcache_perthread에 tcache들이 저장되기 때문이다.

    print *(tcache_perthread_struct *)0x555555756010
    $2 = {
      counts = "\000\000\000\000\001", '\000' <repeats 58 times>,
      entries = {0x0, 0x0, 0x0, 0x0, 0x555555756260, 0x0 <repeats 59 times>}
    }
    
  • 이 상태에서 실행을 재개하면 key값을 변경하지 않고, 다시 free를 호출하므로, abort가 발생한다.

2.1 우회 기법

  • 앞의 분석을 통해 알 수 있듯, if (__glibc_unlikely (e->key == tcache))만 통과하면 tcache 청크를 double free 시킬 수 있다.
+       /* This test succeeds on double free.  However, we don't 100%
+          trust it (it also matches random payload data at a 1 in
+          2^<size_t> chance), so verify it's not an unlikely
+          coincidence before aborting.  */
+       if (__glibc_unlikely (e->key == tcache)) // Bypass it!
+         {
+           ...
+             if (tmp == e)
+               malloc_printerr ("free(): double free detected in tcache 2");
+         }
+           ...
+       if (tcache->counts[tc_idx] < mp_.tcache_count)
+         {
+           tcache_put (p, tc_idx);
+           return;
+         }
        }
  • 다시 말해, 해제된 청크의 key값을 1비트만이라도 바꿀 수 있으면, 이 보호 기법을 우회할 수 있다.

Tcache Duplication

1. Tcache Duplication

  • 아래 코드는 tcache에 적용된 double free 보호 기법을 우회하여 Double free bug를 트리거하는 코드다.
// Name: tcache_dup.c
// Compile: gcc -o tcache_dup tcache_dup.c

#include <stdio.h>
#include <stdlib.h>

int main() {
  void *chunk = malloc(0x20);
  printf("Chunk to be double-freed: %p\n", chunk);

  free(chunk);

  *(char *)(chunk + 8) = 0xff;  // manipulate chunk->key
  free(chunk);                  // free chunk in twice

  printf("First allocation: %p\n", malloc(0x20));
  printf("Second allocation: %p\n", malloc(0x20));

  return 0;
}
  • 이를 컴파일하고, 실행한 결과는 다음과 같다.
$ ./tcache_dup
Chunk to be double-freed: 0x55d4db927260
First allocation: 0x55d4db927260
Second allocation: 0x55d4db927260
  • chunktcache에 중복 연결되어 연속으로 재할당되는 것을 확인할 수 있다.

Double Free Bug Lab

1. Q1

image.png

2. Q2

image.png

마치며

1. 마치며

  • 이번 강의에서는 Double Free Bug (DFB)를 배우고, 관련된 보호 기법을 우회하는 방법에 대해 알아보았다.
    • 그리고 이를 이용한 Tcache duplication에 대해서도 살펴보았다.
  • 다음 강의에서는 임의 주소 쓰기, 임의 주소 읽기 등에 사용되는 Tcache poisoning 기법에 대해 배워보겠다.
    • 이 공격 기법은 tcache duplication을 응용하므로, 이 강의의 내용을 숙지하고 학습하시기 바란다.

2. 키워드

  • tcache_entry
    • 해제된 tcache 청크를 나타내는 구조체다.
    • 각 청크는 next라는 멤버 변수로 연결된다.
    • Double free 보호 기법이 적용되면서, key라는 멤버 변수가 추가되었다.
  • tcache_perthread_struct
    • tcache를 처음 사용하면 할당되는 구조체다.
  • Double Free Bug
    • 한 청크를 두 번 해제할 수 있는 버그다.
  • Tcache Duplication
    • tcache에 같은 청크가 두 번 연결되는 것이다.
    • Double free bug로 발생시킬 수 있으며, tcache poisoning으로 응용될 수 있다.

Loading comments...