详细见:栈迁移原理介绍与应用 - Max1z - 博客园 (cnblogs.com)

其实际上的作用就是控制esp指向已构造好的payload区(覆盖量不够的情况,可先构造好一段payload到特定的内存段上);

原理

利用栈平衡的操作;

32位调用函数时,会有如下操作发生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//第一步:操作执行流
push eip+4 //保存函数返回地址入栈
mov eip, Func //执行流进入函数
---
//第二步:构造新栈帧
push ebp //保存上层函数栈底ebp
mov ebp, esp //将ebp指向旧ebp的值,也是此函数的栈底
add esp, xxh //增长栈顶构成新栈帧
---
//第三步:还原上层栈帧形态 Leave
mov esp, ebp //让esp重新指向此函数的栈底
pop ebp //将此时esp指向的旧ebp值弹出给ebp,此时ebp指向旧栈底,且esp指向ret的地址
//retn
pop eip //将此时ret地址的值弹出给执行流,此时esp指向旧栈顶

第三步的即为第二部的逆运算,可以称这步为Leave,如果能够劫持在函数里的旧ebp值,就可以使得 pop ebp 到一个可控的地方,栈顶的位置是由ebp的值而控制:mov esp,ebp;从而可能影响esp,从而控制进程流;

流程

如下图所示:

current

意思是在Leave中,需要颠倒1,2行的内容;

如何颠倒,很简单,用gadget思想;

即首先控制旧ebp和ret地址,让ret地址返回到新的一组Leave中去,此时可以把esp的值控制为第一次的ebp的值,使得栈顶转移到另一处内存空间,达成栈迁移;

执行流程:

  1. 使用gadget寻找新一组的Leave地址:NewLeaveAddr,以及目标栈顶位置:AimAddr;
  2. 覆盖旧址ebp为 AimAddr-4 (64位-8,因为第二次执行Leave时,会再次pop ebp,使得esp下降,即往高地址走一格)
  3. 覆盖ret地址为 NewLeaveAddr;

执行之后,新的栈顶指向AimAddr(此时还未执行pop eip),栈底指向AimAddr-4处的数值;

运用情况

这个技术运用于栈溢出返回字节不够时的情况,此时只用覆盖ebp和ret地址就行;

能够使用该技术的情景:

  1. 存在leave ret gadget;
  2. 存在可执行 payload 的内存段;

一般而言,能执行shellcode的地方直接ret就行了,不需要这么复杂,不能执行指令的片段上,此时需要运用在栈上,使得控制的栈帧介于输入的变量缓冲区上,把可覆盖区域尽量拉长,利用已填写的 payload 再次实现经典栈溢出;

使用实例

code

上图为一个栈帧,此时最左侧为变量数组下标0处,也是控制esp指向的目标位置;

  1. 确定旧ebp与该变量的偏移,因为可以通过格式化字符串泄露旧ebp内容,从而动态地计算出此时此刻变量在栈中的地址:AimAddr;
  2. 找到gadget NewLeaveAddr,此时覆盖ebp处为变量地址,ret地址为gadget地址;
  3. 可知当执行之后,esp会减少一格到此变量下标1处,且此时(pop eip)即ret还没执行;

聪明如你,当现在的情况即是执行ret的时刻,那么后面的内容也就是传统栈溢出所需要填充的内容了;

这个时候就可以在变量上面直接地填写,不需要在变量溢出后填写;