别怕,这玩意儿真没你想得那么玄乎!
说真的,我刚听到“栈迁移”这三个字的时候,脑子嗡的一下,跟被人敲了一闷棍似的,啥玩意儿?栈?迁移?栈还能搬家?这不是跟我开玩笑呢嘛!后来硬着头皮啃了几天,才发现……嗐,也就那么回事儿,今天我就用大白话,给还在门口探头探脑的你写一篇怎样栈迁移入门教程,咱不整那些花里胡哨的术语堆砌,就聊人话。

栈迁移到底是个啥?别被名字唬住了
你先别急着去搜那些满屏寄存器、地址、偏移量的文章,咱先打个比方。
你想象一下,你在一条窄巷子里打架,巷子太窄了,你胳膊都抡不开,技能放不出来,咋办?你能不能先退到旁边那个大广场上去,然后再跟人干?哎,栈迁移就是这么个思路——原来的栈空间不够用了,或者被限制住了,咱就把栈“搬”到另一个地方去,继续该干嘛干嘛。
在二进制安全、CTF的Pwn题里,这招简直是家常便饭,栈溢出的时候可写空间不够、或者需要先泄露点东西再回来搞事,栈迁移就是你手里的那张“换场地”牌,不是啥魔法,真的。
入门之前,你得先搞明白这几个东西
我求你了,别一上来就复现什么“栈迁移+ret2dlresolve”的骚操作,咱先把地基打好,行不?
- 栈指针(ESP/RSP):这哥们儿指向当前栈顶,栈迁移说白了,就是想办法把它的值改到你想去的地方。
- EBP/RBP:栈底指针,很多时候迁移就是拿它做跳板。
- leave; ret:这俩指令简直是栈迁移的灵魂CP。
leave相当于mov rsp, rbp; pop rbp,ret就是pop rip,你把这俩吃透了,栈迁移你就懂一半了。 - 可控的缓冲区:比如bss段上有个你能写入的地址,那就是你的新家。
唉,我当初就是没耐心,直接抄exp,结果换个题就傻眼,所以啊,慢就是快,真的。
怎样栈迁移入门教程:手把手走一遍思路
好了,重头戏来了,我给你捋一个最经典的入门场景。
假设:程序有个栈溢出,但溢出的字节数很少,只够覆盖到ebp和返回地址,想直接ROP一大串?没门儿,空间不够。
这时候栈迁移就登场了:
- 第一步:找一个你能控制内容的地方,比如bss段上某个地址(我们叫它
fake_stack),你通过输入,把下一步想执行的ROP链提前写到那儿。 - 第二步:第一次溢出的时候,把
ebp覆盖成fake_stack的地址,返回地址覆盖成leave; ret那个gadget。 - 第三步:程序执行
leave; ret。leave会把rsp指向fake_stack,pop rbp随便弹一个;然后ret从fake_stack上取地址跳过去。 - 第四步:恭喜你!栈已经“搬”到bss段上了,接下来你的ROP链想多长就多长,爱咋咋地。
看到没?核心就一句话:用一次leave ret,把rsp劫持到你能掌控的内存区域,是不是没那么吓人?我当初卡在“为什么leave能改rsp”上卡了一晚上,现在想想真是……唉,不说了,都是泪。
新手最容易踩的几个坑,我替你踩过了
- 坑一:fake_stack地址没对齐,有的题对栈对齐有要求,不对齐直接崩,你连报错都看不懂,记得检查一下。
- 坑二:bss段不可写,不是所有bss都能写啊兄弟,先看看权限,别瞎迁移。
- 坑三:gadget找错了。
leave; ret要找干净的,别找个带乱七八糟副作用的。 - 坑四:忘了程序基址,PIE开着呢,你不泄露基址,迁个寂寞。
我跟你讲,我第二次做栈迁移题的时候,自信满满写了个exp,结果跑起来直接segment fault,盯着屏幕愣了十分钟,最后发现是把fake_stack写成了只读段……那叫一个气啊!
练手建议:别光看,动手啊!
入门阶段,我强烈建议你去刷几道经典的栈迁移题,不用多,三五道就够你形成肌肉记忆了,先找没开PIE的、csu gadget齐全的、bss可写的,一步步调,gdb跟着走,看rsp到底跑哪儿去了,别嫌麻烦,调试才是最好的老师。
等你把最基础的“迁移到bss”玩溜了,再去碰什么“迁移到堆”、“迁移到栈上其他位置”的花活,步子迈大了,容易扯着……你懂的。
最后唠叨两句
栈迁移这东西,说穿了就是换個地方继续执行你的ROP链,入门难,是因为你还没习惯“指针跳来跳去”的思维方式,一旦通了,你会发现后面的题反而越来越顺。
我写这篇怎样栈迁移入门教程,就是想告诉你:别怕,别怂,别一上来就否定自己,谁还不是从“这啥玩意儿”一步步走过来的呢?你只要把leave ret吃透,把fake_stack布置好,剩下的就是耐心调试。
行了,今天就唠到这儿,你要是还有啥不明白的,或者调exp调到头秃,欢迎来评论区吐槽,咱一起进步,别一个人硬扛,真的,加油!