不完美笔记
首页项目归档照片墙音乐说说杂谈友链关于
封面

计算机原理实验:栈溢出——从栈帧分析到构造 shellcode

写作时间:2026-09-29 11:10:00
# 计算机原理
# 栈溢出
# shellcode
# WinHex
# OllyICE
# 实验笔记

实验要求

课程提供了一份带栈溢出漏洞的 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。

在内存里搜索这个操作码

  1. 按 Alt + M 打开 memory map(内存映射)
  2. 双击代码段
  3. 右键 → 查找 → 二进制字串,输入 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 的声明, 但查资料后发现光看这个不准,更稳妥的方式是动态地量:

  1. 在 IDA 里定位 main 函数 (技巧:按 Ctrl+X 找交叉引用,找到调用 main 的位置,双击进去,再按空格切汇编视图)
  2. 转到 OllyICE 的 004011A0 附近
  3. 一步步运行到 call _main,按 F7 跟进去
  4. 此时观察 ESP 的值

实验里量到:

ESP        = 0x0012FF7C
Buffer     = 0x0012FF28
第二个buffer = 0x0012FF2C

于是:

0x0012FF7C - 0x0012FF2C = 0x50 = 80 字节

但还要给第一个 ReadFile 留 4 个字节,所以最终是:

80 + 4 = 84 字节

这和 IDA 里读到的 54h(84)刚好对上——两种方法互相验证, 这时候心里才真正有底。

体会:静态分析给假设,动态调试验证假设。 只看 IDA 会怀疑,只看调试器会迷路,两者对上才敢下结论。

坑三:我的机器码和别人的不一样

这是我当时最大的困惑。

我发现自己的 jmp esp 地址、call WinExec 的机器码, 都和参考资料、甚至和同学的不一样,一度陷入混乱。

后来才想明白两件事:

  1. jmp esp 的地址依赖系统 DLL 的加载基址,不同环境必然不同
  2. call WinExec 的机器码,取决于它在内存中的相对位置, 因为 call 后面跟的是相对偏移量而不是绝对地址

所以正确做法是:

在 jmp esp 的后面接着写你的汇编指令, 让它们处于正确的相对位置上,这时候生成的机器码才是能用的。

理解这一点之后,payload 才终于跑通。


实验总结

这是这门课里我花时间最多、也最有收获的一个实验。

技术上的收获:

  • 栈溢出利用的完整链条:确定缓冲区大小 → 覆盖返回地址 → 用 jmp esp 跳转 → 执行 shellcode
  • 大小端转换不是理论,是写错一个字节就全废的实际问题
  • NOP 滑板的作用
  • 相对寻址(call 的偏移量)为什么会导致"同样的代码、不同的机器码"

更重要的收获:

说实话,这个实验让我意识到自己对计算机原理的理解还停留得很表面。 很多指令当时是"照着做能跑通",但为什么这么写、为什么这个顺序,并不能立刻讲清楚。

尤其是卡住的时候——查找栈大小、理解指令含义、搞懂机器码为什么不同—— 这些都不是看一遍就能会的,是在反复试错里慢慢清晰的。

这也正是我把这些实验整理出来的原因: 当时是照着步骤做完的,现在重新写一遍,才算真的想明白了。


📌 本文由大学时期的课程实验报告整理而来,已移除姓名、学号、班级、指导教师等个人信息, 并把原文中带有年级标识的课程资料路径改为泛称。 原始报告中的截图因可能包含个人环境信息(文件路径、用户名)未一并收录。

avatar

Winter21c

暗处提灯,静处守门;功成不必在我,安宁自在人间

RECOMMENDED

我是如何修改她的 blog 的

2026-09-29 10:45:00

红米 K20 Pro 的 Arch Linux 移植手记

2026-10-02 17:25:00

计算机原理作业:直接映射高速缓存的两道题

2026-09-29 11:15:00

Table of Contents