栈漏洞系列(一)-- 栈迁移
原理
当溢出的长度太小时,可以通过leave_ret_gadget来将ebp和esp迁移到我们想要的位置,比如:一个咱已经布置好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已经指向了一个奇怪(后面的例题会展示奇怪的点)的地方,但是问题不大,我们只要明白ebp和esp最根本的作用是什么就不会觉得奇怪了,ebp的作用是来定位每个函数的栈的大小,当函数返回时才需要通过ebp来恢复主函数的栈,而esp才是真正控制栈的进出,所以我们只要控制esp指向我们想要指向的函数就可以达到我们的目的了,至于ebp嘛,我们都拿到shell了,它能不能成功的返回已经不关我事了嘿嘿嘿~
例题解析
这里拿19年国赛的一道题(ciscn_2019_es_2)来讲解一下,经典入门题,在buu上,大伙可以去玩玩
习惯checksec一下:
main函数看看:
vul函数不用多说,肯定就是受害函数了,仔细一看,只能溢出8字节吼!也真够抠的,不过我们刚刚学了栈迁移,不慌
我们一个个来看吼,首先程序memset了s这个数组,但是很奇怪,它初始化又不全部清零,就整了0x20个字节,它为啥要这么做呢,其实就是在泄露信息,如果它没全部清零,那后0x10是什么东西呢?诶~,就是栈上的地址对吧,所以当我们覆盖了s数组最后的"\n",那printf打印s的时候,是不是就不会截断,是不是就打印了那未初始化的值,也就泄露了栈上的地址,有人可能又有疑问了,泄露栈上的地址有什么用呢?别急,慢慢来!
在第一次read()的时候,我输入了24个a和我的id,计算一下s的位置到ebp的距离,拿起你的小计算器计算一下:0xa8 - 0x80 是不是等于0x28,至于为啥要到ebp呢,别问,问就是后面好定位偏移
按照上面的想法我们写出下面的exp:
1 | io.recvuntil('your name?\n') |
前面我说过,我们栈迁移是迁移到另一个有我们ROP链的地方,但是这里我们并没有办法往其他地方写入我们的ROP链,我们只能往栈上写入,所以我有个大胆的想法,栈迁移到栈上,这样我们刚刚泄露的栈地址就有用了是不,所以我们在第二次read()的时候就将栈布置成这样:
1 | payload = 'aaaa' |
咱动调看看:
leave之后:
注意esp和ebp,是不是esp比ebp还大哈哈哈:
再往下执行,咱就发现esp指向了system的plt:
完整EXP:
1 | #!/usr/bin/python |

