[Pwnable] 18. Memory Corruption: Double Free Bug

Wonder_Land🛕·2022년 11월 11일
0

[Pwnable]

목록 보기
18/21
post-thumbnail

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.


  1. 서론
  2. Double Free Bug
  3. Mitigation for Tcache DFB
  4. Tcache Duplication
  5. Q&A
  6. 마치며

1. 서론

free함수로 청크를 해제하면, ptmalloc2는 이를 tcache나 bins에 추가하여 관리합니다.
그리고 이후에 malloc으로 비슷한 크기의 동적할당이 발생하면, 이 연결리스트들을 탐색하여 청크를 재할당해줍니다.

이 매커니즘에서, 해커들은 free로 해제한 청크를 free로 다시 해제했을 때 발생하는 현상에 주목했습니다.

tcache와 bins를 'free list'라고 통칭한다면,
free list의 관점에서 free는 청크를 추가하는 함수, malloc은 청크를 꺼내는 함수입니다.
그러므로, 임의의 청크에 대해 free를 두 번 이상 적용할 수 있다는 것은, 청크를 'free list'에 여러 번 추가할 수 있음을 의미합니다.

청크가 free list에 중복해서 존재하면 "청크가 duplicated"됐다고 표현하는데,
해커들은 duplcated free list를 이용하면 임의 주소에 청크를 할당할 수 있음을 밝혀냈습니다.
이렇게 할당한 청크의 값을 읽거나 조작함으로써 해커는 임의 주소 읽기 / 쓰기를 할 수 있습니다.

위와 같은 이유로, 같은 청크를 중복해서 해제할 수 있는 코드는 보안상의 약점으로 분류되어 'Double Free Bug'라고 불립니다.


2. Double Free Bug

1) Double Free Bug

'Double Free Bug(DFB)'는 같은 청크를 두 번 해제할 수 있는 버그를 말합니다.

ptmalloc2에서 발생하는 버그 중 하나이며, 공격자에게 임의 주소 쓰기 / 읽기, 임의 코드 실행, 서비스 거부 등의 수단으로 활용될 수 있습니다.

저번에 배운 Dangling Pointer는 DFB를 유발하는 대표적인 원인입니다.
코드 상에서 Dangling Pointer가 생성되는지, 그리고 이를 대상으로 free를 호출하는 것이 가능한지 살피면 DFB가 존재하는지 가늠할 수 있습니다.

DFB를 활용하면, duplicated free list를 만드는 것이 가능한데, 이는 청크와 연결리스트의 구조때문입니다.

ptmalloc2에서, free list의 각 청크들은 fdbk로 연결됩니다.
fd는 자신보다 이후에 해제된 청크를, bk는 이전에 해제된 청크를 가리킵니다.

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

초기에는 Double Free에 대한 검사가 미흡하여 DFB가 있으면 트리거할 수 있었습니다.
특히, tcache와 관련해서는 얼마전까지도 보호 기법이 전무하여 Double Free의 쉬운 먹잇감이었습니다.

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


2) 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
}

위의 예제는 같은 청크를 두 번 해제하는 코드입니다.

컴파일하고 실행하면 Double Free가 감지되어 비정상적으로 종료됩니다.


3. Mitigation for Tcache DFB

1) 정적 패치 분석

(1) tcache_entry

먼저, double free를 탐지하기 위해 key포인터가 tcahce_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에 대응되는 값은 없습니다.

(2) tcache_put

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

tcache_put함수는 해제되는 청크의 keytcache라는 값을 대입하도록 변경되었습니다.

여기서 tcachetcache_perthread라는 구조체 변수를 가리킵니다.

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]);
}

(3) tcache_get

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

아래의 코드를 보면, tcache_get함수는 재사용하는 청크의 key값에 NULL을 대입하도록 변경되었습니다.

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;
 }

(4) _int_free

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

아래의 코드를 보면, 재할당하려는 청크의 key값이 tcache라면 Double Free가 발생했다고 보고 ㅍ로그램을 abort시킵니다.

그 외의 보호 기법이 없으므로, 20번째 줄의 조건문만 통과하면 Double 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

2) 동적 분석

먼저, 청크 할당 직후에 중단점을 설정합니다.

$ gdb -q double_free
pwndbg> disass main
  0x0000000000001189 <+0>:     endbr64
  0x000000000000118d <+4>:     push   rbp
  0x000000000000118e <+5>:     mov    rbp,rsp
  0x0000000000001191 <+8>:     sub    rsp,0x10
  0x0000000000001195 <+12>:    mov    edi,0x50
  0x000000000000119a <+17>:    call   0x1090 <malloc@plt>
  0x000000000000119f <+22>:    mov    QWORD PTR [rbp-0x8],rax
  ...

pwndbg> b *main+22
Breakpoint 1 at 0x0x119f

pwndbg> r

heap명령어로 청크들의 정보를 조회합니다.

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x291

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

Top chunk | PREV_INUSE
Addr: 0x5555555592f0
Size: 0x20d11

이 중, malloc(0x50)으로 생성한 chunk의 주소는 0x555555559290입니다.

해당 메모리 값을 덤프하면, 아무런 데이터가 입력되지 않았음을 알 수 있습니다.

pwndbg> x/4gx 0x555555559290
0x555555559290: 0x0000000000000000      0x0000000000000061
0x5555555592a0: 0x0000000000000000      0x0000000000000000

이후의 참조를 위해 청크를 gdb변수로 정의하겠습니다.

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

chunk를 해제할 때까지 실행하고, 청크의 메모리를 출력하겠습니다.

pwndbg> b *main+62
Breakpoint 2 at 0x5555555551c7

pwndbg> c

pwndbg> print *chunk
$1 = {
  next = 0x0,
  key = 0x555555756010
}

chunkkey 값이 0x555555756010로 설정되었습니다.

이 주소의 메모리 값을 확인하면,
해제한 chunk의 주소 0x5555555592a0entry에 포함되어 있음을 알 수 있는데,
이는 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가 발생합니다.


3) 우회 기법

앞의 분석 기법을 통해 알 수 있듯,

if (__glibc_unlikely (e->key == tcache))만 통과하면 tcache 청크를 Double Free 시킬 수 있습니다.

다시 말해, 해제된 청크의 key값을 1비트만이라도 바꿀 수 있으면, 이 보호 기법을 우회할 수 있습니다.


4. Tcache Duplication

아래의 코드는 tcahce에 적용된 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에 중복 연결되어 연속으로 재할당되는 것을 확인할 수 있습니다.


5. Q&A

-


6. 마치며

-

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.

profile
아무것도 모르는 컴공 학생의 Wonder_Land

0개의 댓글