实验要求
课程提供了一份带栈溢出漏洞的 exe 和配套材料。
任务:用 WinHex 编写一个文件, 使得该 exe 在打开这个文件时,自动打开"蜘蛛纸牌"。
听起来像是恶作剧,但这其实是缓冲区溢出利用最经典的入门形态—— 把一个"让程序执行我想要的东西"的完整链路走通一遍。
实验环境:Windows XP 虚拟机 + VMware + WinHex + IDA + OllyICE
一、先搞清楚栈有多大
溢出利用的第一步永远是:目标缓冲区到底有多少字节?
把程序拖进 IDA,按空格切到汇编视图,找到缓冲区声明。会看到类似:
Buffer = dword ptr -54h
这行的含义是:在栈帧上为名为 Buffer 的变量分配了 54h 的空间。
0x54 = 84(十进制)
所以缓冲区大小是 84 字节。 超过这个长度写进去的数据,就会覆盖到相邻的栈空间。
⚠️ 这里我一开始直接照抄了
54,后来才发现被坑了——54h是十六进制,等于十进制 84。 详见下面"踩过的坑"一节。
二、用 WinHex 构造溢出文件
打开 WinHex:文件 → 新建,大小设 1KB 就够。
按照 84 字节的缓冲区,文件内容这样安排:
| 偏移 | 内容 | 作用 |
|---|---|---|
| 开头 | 54(十六进制) |
对应 IDA 里看到的 54h 栈空间标识 |
| 之后 | 空 3 个字节 | 对齐用 |
| 接着 | 80 个 A |
填充物,用来撑满缓冲区 |
| 末尾 | B |
标记位,用来验证是否真的溢出 |
为什么只填 80 个 A 而不是 84 个? 因为前面已经占掉了 4 个字节(
54+ 3 个空字节), 补充 80 个正好凑满 84。这是实验里需要自己算清楚的地方。
那个末尾的 B 是故意的探针——如果程序崩溃时 EIP 变成了 0x42424242(即 BBBB),
就证明我们确实控制住了返回地址。这一步就是"验证是否溢出"。
保存为 none1,放到 C:\ 下。
三、确认溢出确实发生了
打开 OllyICE:文件 → 打开,选择那个实验程序,
在参数栏里填入刚才保存的文件路径(C:\none1)。
一路按 F8 单步执行。
程序会报错崩掉——这个报错正是好消息,它证明栈溢出已经发生了。
四、找 jmp esp 的地址
要让程序跳到我们写的数据上执行,需要一个跳板。
栈溢出利用里最经典的跳板就是 jmp esp 指令:
它会让执行流跳到 ESP 当前指向的位置——而那个位置,正好在我们的控制之下。
找指令的操作码
在 OllyICE 里随便找个地方写一行 jmp esp,可以看到它的机器码是 FF E4。
在内存里搜索这个操作码
- 按 Alt + M 打开 memory map(内存映射)
- 双击代码段
- 右键 → 查找 → 二进制字串,输入
FFE4
搜到的地址就是 jmp esp 在内存中的位置。
📌 这个地址因环境而异。我在实验里不同时间搜到过
0x7C86467B、7c961eed等不同值, 它取决于系统 DLL 的加载基址。实际利用时要以自己当前环境搜到的为准, 照抄别人的地址一定会失败。
别忘了大小端转换
x86 是小端序(Little Endian):低字节存在低地址。
所以如果搜到的地址是 7c961eed,写进文件时的字节顺序要反过来:
地址:7c 96 1e ed
写入:ed 1e 96 7c ← 反着写
这一步如果搞错,程序会跳到一个完全不存在的地址,直接崩。
一个小细节
文件开头的两个 00 字节要改成 90(即 NOP 指令,什么都不做)。
NOP 是"滑板",让执行流平稳地滑到我们的代码上,而不是撞进一堆 00 里。
五、构造 payload:让程序去开蜘蛛纸牌
现在要写的是跳转之后执行的代码。目标是调用 Windows API 里的 WinExec。
WinExec 怎么用
WinExec(LPCSTR lpCmdLine, UINT uCmdShow);
- 第一个参数:要执行的命令行字符串的地址
- 第二个参数:窗口显示方式
在 x86 的调用约定里,函数参数是通过压栈传递的,而且顺序是从右往左。 所以要先压第二个参数,再压第一个。
汇编代码
add eax, 0c ; 让 eax 指向字符串的正确位置
push 1 ; 第二个参数:窗口类型 1(正常显示)
push eax ; 第一个参数:命令行字符串的地址
call WinExec ; 调用
为什么是 add eax, 0c?
这里的 eax 被当作指向 shellcode 的指针。
加 0xC(12)是为了跳过前面那段,精确指向存放命令行字符串的位置。
具体加多少,取决于你的 payload 里字符串排在第几个字节—— 这是需要自己数出来的。
拿到机器码
在 OllyICE 里输入上面四条指令,得到机器码:
83 C0 0C 6A 01 50 E8 AD 3E F3 FF
目标程序名的机器码
要打开的 spider.exe,逐字符查 ASCII:
73 70 69 64 65 72 2E 65 78 65
s p i d e r . e x e
拼装回 WinHex
把之前填的 B 换成上面这串机器码,后面接上 spider.exe 的机器码。
⚠️ 注意:机器码在内存里是从后往前读的,涉及地址的部分同样需要大小端转换。
保存文件,在 OllyICE 里以 C:\none1 作为参数运行——
蜘蛛纸牌弹出来了。 🃏
六、踩过的坑(这部分才是真正的收获)
坑一:54h 不是 54
我一开始按字面把 54 当十进制用,怎么试都不对。
后来才反应过来 54h 里的 h 就是十六进制的标记:0x54 = 84。
低级的错误,但卡了很久。
坑二:不知道栈大小怎么定
最初完全不知道从哪下手。后来是先在 IDA 里直接找到 Buffer 的声明,
但查资料后发现光看这个不准,更稳妥的方式是动态地量:
- 在 IDA 里定位
main函数 (技巧:按Ctrl+X找交叉引用,找到调用main的位置,双击进去,再按空格切汇编视图) - 转到 OllyICE 的
004011A0附近 - 一步步运行到
call _main,按 F7 跟进去 - 此时观察
ESP的值
实验里量到:
ESP = 0x0012FF7C
Buffer = 0x0012FF28
第二个buffer = 0x0012FF2C
于是:
0x0012FF7C - 0x0012FF2C = 0x50 = 80 字节
但还要给第一个 ReadFile 留 4 个字节,所以最终是:
80 + 4 = 84 字节
这和 IDA 里读到的 54h(84)刚好对上——两种方法互相验证,
这时候心里才真正有底。
体会:静态分析给假设,动态调试验证假设。 只看 IDA 会怀疑,只看调试器会迷路,两者对上才敢下结论。
坑三:我的机器码和别人的不一样
这是我当时最大的困惑。
我发现自己的 jmp esp 地址、call WinExec 的机器码,
都和参考资料、甚至和同学的不一样,一度陷入混乱。
后来才想明白两件事:
jmp esp的地址依赖系统 DLL 的加载基址,不同环境必然不同call WinExec的机器码,取决于它在内存中的相对位置, 因为call后面跟的是相对偏移量而不是绝对地址
所以正确做法是:
在
jmp esp的后面接着写你的汇编指令, 让它们处于正确的相对位置上,这时候生成的机器码才是能用的。
理解这一点之后,payload 才终于跑通。
实验总结
这是这门课里我花时间最多、也最有收获的一个实验。
技术上的收获:
- 栈溢出利用的完整链条:确定缓冲区大小 → 覆盖返回地址 → 用
jmp esp跳转 → 执行 shellcode - 大小端转换不是理论,是写错一个字节就全废的实际问题
- NOP 滑板的作用
- 相对寻址(
call的偏移量)为什么会导致"同样的代码、不同的机器码"
更重要的收获:
说实话,这个实验让我意识到自己对计算机原理的理解还停留得很表面。 很多指令当时是"照着做能跑通",但为什么这么写、为什么这个顺序,并不能立刻讲清楚。
尤其是卡住的时候——查找栈大小、理解指令含义、搞懂机器码为什么不同—— 这些都不是看一遍就能会的,是在反复试错里慢慢清晰的。
这也正是我把这些实验整理出来的原因: 当时是照着步骤做完的,现在重新写一遍,才算真的想明白了。
📌 本文由大学时期的课程实验报告整理而来,已移除姓名、学号、班级、指导教师等个人信息, 并把原文中带有年级标识的课程资料路径改为泛称。 原始报告中的截图因可能包含个人环境信息(文件路径、用户名)未一并收录。
