• 关于栈迁移的原理(AI给的喵~):

1. 核心思想:欺骗 CPU 的“导航仪” (esp)

在 CPU 眼里,它根本不知道什么是“变量 s”,什么是“栈底”。它只认一个死理:寄存器 esp(栈顶指针)指在哪里,哪里就是当前的栈。 程序每次执行 pop 或者要拿局部变量时,都是基于 esp 的位置去拿的。

栈迁移的终极目标,就是通过漏洞,强行把 esp 的值,修改成我们变量 s 的地址。 这样 CPU 就会把 s 当成新的栈,去里面读取我们事先准备好的 ROP 链(比如 system("/bin/sh"))。

2. 拆解指令:leave 和 ret

为了完成迁移,我们需要用到程序正常退出函数时必然会执行的两条汇编指令:leaveret。把它们的底层动作拆开看,就是破局的关键:

  • leave 其实是两个动作的结合体:

    • mov esp, ebp (把 ebp 的值赋给 esp。通俗点说,把栈顶指针强行拉到栈底)
    • pop ebp (把当前栈顶的数据弹出来,存入 ebp。同时 esp 向下移动 4 个字节)
  • ret 其实就是一个动作:

    • pop eip (把当前栈顶的数据弹出来,当成下一条要执行的指令地址。同时 esp 移动 4 字节)

3. 执行流程情况 (Double leave; ret)

假设你现在已经利用 read 溢出了,并且精心构造了下面的内存布局:

【当前栈的布局 (32位)】
+----------------------+
| 变量 s 的空间 (40字节) | <-- 里面写满了你伪造的 ROP 链 (比如 system 地址等)
+----------------------+
| saved ebp (4字节)    | <-- 你把它覆盖成了【变量 s 的真实内存地址】!
+----------------------+ <-- 原本的 ebp 停在这个位置
| ret addr (4字节)     | <-- 你把它覆盖成了【程序里另一处 leave; ret 指令的地址】!
+----------------------+

当 vul 函数正常执行到末尾,准备结束时,神奇的连环反应开始了:

第一波:函数正常的 leave

  • mov esp, ebp:esp 来到了存放 saved ebp 的位置。
  • pop ebp:注意!此时栈顶的数据是你填写的【变量 s 的真实内存地址】。这个地址被弹进了 ebp 寄存器。

    • 此时情况:ebp = 变量 s 的地址。esp 往下移,指向了 ret addr 的位置。

第一波:函数正常的 ret

  • pop eip:栈顶的数据是你填写的【另一处 leave; ret 的地址】。CPU 把这个地址吸入大脑,准备去执行它。

    • 此时情况:CPU 被你骗去执行你指定的 Gadget 了。

第二波:你构造的 Gadget leave (高潮来了!)

CPU 开始执行你安排的那个 leave:

  • mov esp, ebp:极其致命的一步!因为上一波操作中,ebp 已经被你换成了【变量 s 的地址】。这条指令一执行,esp 直接飞到了变量 s 的开头!

    • 劫机成功!此时的栈顶(esp)已经彻底迁移到了你的变量 s 上!
  • pop ebp:把 s 的前 4 个字节弹给 ebp(这 4 个字节通常用来当垫脚石,随便填 4 个 'A' 就行)。esp 往下移 4 字节。

第二波:你构造的 Gadget ret

  • pop eip:此时 esp 指向的是变量 s 里的第 5-8 个字节。CPU 把这 4 个字节弹出来当成下一条指令去执行。
  • 如果你在这里写的是 system 函数的地址,那么恭喜你,系统命令已经开始执行了!