Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[System_Hacking] stage12_Double Free Bug 본문

Hack/DreamHack(로드맵)

[System_Hacking] stage12_Double Free Bug

CIDY 2022. 7. 8. 02:54

*Double free

이전 글에서 free함수로 할당 해제된 청크는 tcache나 bin에 보관되며, 비슷한 크기의 재할당 요청이 들어올 경우 이 연결 리스트들을 탐색해 청크를 재할당하는 과정을 설명했었다. 

 

근데 이전 uaf문제를 풀 때도 설명했지만, free함수 자체는 메모리 공간이나 포인터를 초기화시키는 기능이 없다.

 

예를 들어 A = malloc(0x80); 이렇게 할당 후 free(A); 를 해줘도 A가 널포인터가 된 것은 아니므로(Dangling pointer) 다시 free(A)해줄수도 있는 부분이다. -> 청크를 tcache나 bin에 여러 번 중복 추가 가능함.

 

청크가 tcache나 bin에 중복으로 존재하는 것을 "청크가 duplicated됐다" 라고 말한다. 이 duplicated free list를 이용하면 (free list = bin + tcache) 임의 주소에 청크를 할당할 수 있다. -> 이렇게 할당된 청크의 값을 읽거나 조작해 임의 주소 쓰기, 읽기가 가능하다. 

 

-> 이렇게 중복 해제를 통해 청크가 free list에 중복으로 존재할 수 있음을  Double Free Bug라고 함.

 

 

*Duplicated free list

해제되어 free list에 보관되고 있는 청크들은 fd와 bk로 연결된다. fd는 자신보다 나중에 해제된 청크를, bk는 자신 이전에 해제된 청크를 가리킨다. 근데 지난 시간에 봤듯이 해제된 청크의 fd, bk정보가 저장되는 곳은 할당된 청크의 데이터 공간이다. -> 만약 어떤 청크가 free list에 중복 포함된다면, 첫 번째 재할당에서 fd와 bk를 조작해 free list에 임의 주소를 포함시킬 수 있다.

 

예전에는 관련 보호기법이 미흡했는데, 최근 glibc에 구현된 보호 기법에 의하면 같은 청크를 두 번 해제하는 즉시 프로세스가 종료된다.

 

 

그럼 우리는 저 보호기법을 통과해야 DFB를 활용할 수 있다. -> 저 보호기법이 작동하는 코드를 뜯어보자.

 

 

*정적분석

//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청크를 나타내는 구조체이다. 각 청크는 next라는 멤버 변수로 연결된다.

 

+ 부분이 double free 감지를 위해 추가된 부분인데, key라는 포인터가 추가되었다. (tcache entry는 해체된 tcache청크들이 갖는 구조로, 일반 청크의 fd가 next로 대체되고, LIFO로 사용되므로 bk에 대응되는 값은 없다.)

 

*참고로 tcache_perthread_struct는 tcache를 처음 사용하면 할당되는 구조체이다.

 

 

//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에 추가하는 함수이다. + 부분을 보면 키값에 tcache값을 대입하도록 되어 있다. (tcache는 tcache_perthread라는 구조체 변수를 가리킨다.)

 

 

//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에 저장된 청크를 재사용할 때 사용하는 함수인데, 키값에 널을 넣어주도록 바뀌었다.

 

_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

 

이 함수는 청크 해제 시 호출되는 함수이다. 세번째 if문부터 보면, 재할당하려는 청크의 키값이 tcache이면 더블프리가 발생했다고 판단ㄴ하고 프로그램을 abort시킨다. -> 이 조건문만 통과하면 double free일으킬 수 있다.

 

 

*동적분석

$ 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

 

이 중 위에서 할당해준 애는 0x50이니까 두 번째 청크이고, 주소는 0x555555756250이다. 

 

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

 

해당 주소를 chunk라고 정의하자. (gdb 변수 정의)

 

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
}

 

청크가 해제된 직후까지 continue하고 청크 메모리를 조회해보면 키값에 무언가가 들어 있다. 

 

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

 

이를 조회해보면 위와 같은데, 해제했던 청크의 주소가 엔트리에 포함되어 있는 것을 볼 수 있다. -> tcache_perthread에 tcache들이 저장되기 때문이다. 

 

여기서 실행을 재개하면 키값을 변경하지 않고 free를 호출하므로 abort된다. 

 

 

-> if (__glibc_unlikely (e->key == tcache)) 이 부분만 통과하면 tcache청크를 더블프리 시킬 수 있다.

 

즉, 키값을 아주 조금이라도 바꿔버리면 저 if문은 통과됨. (같지 않으니까)

 

 

 

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

 

이 코드는 더블프리 보호기법을 우회한 경우이다. 청크를 해제한 뒤 키값을 조작하고 한 번 더 해제했는데도 abort되지 았았다. -> 마지막 printf문에서 두 번 할당요청을 했는데 tcache에 중복으로 들어있던 값을 꺼내와서 둘 다 같은 주소가 출력되는 것을 볼 수 있다.

 

$ ./tcache_dup
Chunk to be double-freed: 0x55d4db927260
First allocation: 0x55d4db927260
Second allocation: 0x55d4db927260

 

출력 결과는 위와 같다.