最近刷了一下南邮的CTF比赛,1000分的动态积分,pwn了两道800分的,有一道是关于vsyscall的,还有一道就是关于64位格式化字符串利用的,说它简单其实并不简单…毕竟最后只有4人提交了flag…
关于fmtstr的骚操作有很多,比如泄露,写got表,写ret,写.fini_array,栈迁移到堆等
32的fmtstr任意地址写很容易实现,因为地址中不含”\x00”,printf在打印时不会被截断,64位程序就不一样了,64位下用户可见的内存地址高位都带有\x00
说这么多,我们还是看先程序吧….
很简单,程序外面套了2层函数,就看核心代码,printf函数处有格式化字符串漏洞
先说说最开始踩的坑:
- 尝试1
首先想到的是覆盖printf的got表,为了方便就用了pwntools的fmtstr_payload函数,这个函数在32位程序利用下是很方便的fmtstr_payload(6,{printf_got,system_addr},0,'short')
结果屡试不成功,原因是这个函数默认生成的payload字符串将要写的的地址放在了偏移头,对于64位程序printf时输入的字符串必定会被”\x00”截断,导致写入失败。
- 尝试2
然后我更换写入方法,将printf_got地址放在字符串最后,调整偏移和内存对齐,只写一次,payload="a%"+str(system_addr-1)+"c%9$lln"+p64(printf_got)
,这样发送的字符串中就没有00了
继续踩坑,由于str(system_addr-1)
太大,导致io.interactive()时屏幕上的光标会不停闪烁很长一段时间,输出大量的空字符,使用io.recvall()读取这些字符发现数据量高达128.28MB,结果还是写入失败
- 尝试3
接上面,既然一次写数据量太大,我就利用循环多次写,每次写2字节。结果发现第一次写了pintf_got表后,第二次再调用printf时不就崩溃了吗,,,/(ㄒoㄒ)/~~
下面重新整理一下思路:
既然上面方法都不可行,那我们就往ret地址写个one_gadget,循环,每次写2 byte,写三次。
写one_gadget有个限制就是execve的第二个参数必须为0
GDB调试一波,发现只有在main函数返回并进入one_gadget的时候rsp+0x70
的地方满足条件为NULL
详细思路见EXP
执行结果