[Dreamhack] Memory Corruption: Double Free Bug

Sisyphus·2022년 11월 13일
0

Dreamhack - System Hacking

목록 보기
33/49

Double Free Bug

Double Free Bug (DFB)는 같은 청크를 두 번 해제할 수 있는 버그를 말합니다. ptmalloc2에서 발생하는 버그 중 하나이며, 공격자에게 임의 주소 쓰기, 임의 주소 읽기, 임의 코드 실행, 서비스 거부 등의 수단으로 활용될 수 있습니다.

Dangling Pointer는 Double Free Bug를 유발하는 대표적인 원인입니다.

Double Free Bug를 이용하면 duplicated free list를 만드는 것이 가능한데, 이는 청크와 연결리스트의 구조 때문입니다. ptmalloc2에서, free list의 각 청크들은 fdbk로 연결됩니다. fd는 자신보다 이후에 해제된 청크를, bk는 이전에 해제된 청크를 가리킵니다.

그런데, 해제된 청크에서 fdbk 값을 저장하는 공간은 할당된 청크에서 데이터를 저장하는 데 사용됩니다. 그러므로 만약 어떤 청크가 free list에 중복해서 포함된다면, 첫 번째 재할당에서 fdbk를 조작하여 free list에 임의 주소를 포함시킬 수 있습니다.

최근에는 관련한 보호 기법이 glibc에 구현되면서, 이를 우회하지 않으면 같은 청크를 두 번 해제하는 즉시 프로세스가 종료됩니다.


Tcahe 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
}
 ion  ~/dreamhack/Memory_Corruption_Double_Free_Bug  ./dfb
Address of chunk: 0x5586dadef260
free(): double free detected in tcache 2
[1]    3670 abort      ./dfb

청크를 두번 해제하는 예제 코드인데, 컴파일 하고 실행하면 Double Free가 감지되어 프로그램이 비정상 종료됩니다.



Mitigation for Tcache DFB

정적 패치 분석

tcahe_entry

tcache_entry는 해제된 tcache 청크들이 갖는 구조입니다.

tcache_entry 구조체의 Diff

typedef struct tcache_entry {
  struct tcache_entry *next;
+ /* This field exists to detect double frees.  */
+ struct tcache_perthread_struct *key;
} tcache_entry;

double free를 탐지하기 위해 key포인터가 tcache_entry에 추가되었습니다. 일반 청크의 fdnext로 대체되고, LIFO로 사용되므로 bk에 대응되는 값은 없습니다.


tcache_put

tcache_put은 해제한 청크를 tcache에 추가하는 함수입니다.

tcache_put 함수의 Diff

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 함수는 해제되는 청크의 keytcache라는 값을 대입하도록 변경되었습니다. 여기서 tcachetcache_perthread라는 구조체 변수를 가리킵니다.


tcache_get

tcache_get은 tcache에 연결된 청크를 재사용할 때 사용하는 함수입니다.

tcache_get 함수의 Diff

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함수는 재사용하는 청크의 key값에 NULL을 대입하도록 변경되었습니다.


_int_free

_int_free은 청크를 해제할 때 호출되는 함수입니다.

_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

Line 20 아래를 보면, 재할당하려는 청크의 key값이 tcache이면 Double Free가 발생했다고 보고 프로그램을 abort시킵니다. 그 외의 보호 기법은 없으므로, 20번째 줄의 조건문만 통과하면 Double Free를 일으킬 수 있습니다.


동적 분석

 ion  ~/dreamhack/Memory_Corruption_Double_Free_Bug  gdb-pwndbg dfb
pwndbg> disass main
   0x00000000000006ec <+18>:    mov    QWORD PTR [rbp-0x8],rax
pwndbg> b * main+18
Breakpoint 1 at 0x6ec
pwndbg> r

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

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

Top chunk | PREV_INUSE
Addr: 0x5555556022b0
Size: 0x20d51

malloc(0x50)으로 생성한 chunk의 주소는 0x555555602250입니다.

해당 메모리 값을 덤프해보면 아무것도 입력되지 않았습니다.

pwndbg> x/4gx 0x555555602250
0x555555602250: 0x0000000000000000      0x0000000000000061
0x555555602260: 0x0000000000000000      0x0000000000000000

이후의 참조를 위해 청크를 gdb변수로 정의

pwndbg> set $chunk=(tcache_entry *)0x555555756260

chunk를 해제할 때까지 실행하고, 청크의 메모리를 출력해보면

pwndbg> b * main+58
pwndbg> c
pwndbg> print *$chunk
$1 = {
  next = 0x0,
  key = 0x555555602010
}

chunkkey값이 0x555555602010로 설정된 것을 확인할 수 있습니다.

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

pwndbg> print *(tcache_perthread_struct *)0x555555602010
$2 = {
  counts = "\000\000\000\000\001", '\000' <repeats 58 times>,
  entries = {0x0, 0x0, 0x0, 0x0, 0x555555602260, 0x0 <repeats 59 times>}
}
Free chunk (tcache) | PREV_INUSE
Addr: 0x555555602250
Size: 0x61
fd: 0x00

우회 기법

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

보호 기법을 우회하여 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;
}

 ion  ~/security/dreamhack/Memory_Corruption_Double_Free_Bug  ./tcache_dup
Chunk to be double-freed: 0x558585467260
First allocation: 0x558585467260
Second allocation: 0x558585467260

chunktcache에 중복 연결되어 연속으로 재할당 되었습니다.

0개의 댓글