87、漏洞逆向分析(一):栈溢出漏洞分析、Shellcode提取
栈溢出,这名字听着挺唬人。说白了,就是程序往栈里写数据时,没管住手,写超了。
我刚开始搞逆向那会儿,觉得栈溢出就是个老古董。直到有一次,我在分析一个嵌入式设备的固件时,发现一个看起来人畜无害的 strcpy 调用。嗯,就是它,让我折腾了整整一个周末。从那以后,我再也不敢小看任何一个栈溢出漏洞。
栈溢出的本质
先看一个最简化的例子。假设程序里有个函数,它长这样:
void vulnerable_func(char *input) {
char buffer[64];
strcpy(buffer, input); // 这里没检查长度
// ... 其他代码
}
你想想看,buffer 只有 64 字节。如果 input 给了 100 字节,那多出来的 36 字节就会覆盖栈上后面的数据。后面是什么?是函数的返回地址。
覆盖了返回地址会怎样?函数执行完 ret 指令时,CPU 会从栈上弹出返回地址,然后跳过去执行。如果我们把返回地址改成我们想要的值——比如指向一段恶意代码的地址——那程序的控制权就到我们手里了。
核心要点:栈溢出的目标就是覆盖返回地址,劫持控制流。
实战:从崩溃到控制
我记得有一次分析一个 Windows 下的老旧服务程序。它监听某个端口,接收数据后直接拷贝到一个固定大小的缓冲区。我随手发了一串 AAAA... 过去,程序直接崩了。用调试器一看,EIP 寄存器变成了 0x41414141。
这就是经典的栈溢出触发点。接下来要做三件事:
- 确定偏移量——到底要填多少个字节才能覆盖到返回地址?
- 定位返回地址位置——找到返回地址在栈上的精确偏移。
- 构造 payload——把返回地址替换成我们想要的地址。
偏移量怎么找?我习惯用 pattern_create 和 pattern_offset 这两个工具。生成一段不重复的字符串,发过去,看 EIP 被覆盖成了什么值,然后反查偏移。
# 生成 200 字节的测试字符串
msf-pattern_create -l 200
# 假设 EIP = 0x69423569
msf-pattern_offset -q 0x69423569
# 输出: [*] Exact match at offset 76
偏移是 76。也就是说,前 76 字节是填充,第 77-80 字节就是返回地址。
小技巧:我一般会多测两次,确保偏移量准确。有时候编译器优化会导致栈布局变化,多验证一次不吃亏。
Shellcode 提取
找到返回地址位置后,下一步就是让程序跳转到我们的 shellcode 上。shellcode 就是一段机器码,执行后能给我们一个 shell,或者执行任意命令。
提取 shellcode 有两种方式:
- 手写汇编——用汇编写功能,然后编译成机器码。适合对指令集很熟的人。
- 用工具生成——比如
msfvenom,直接生成现成的 shellcode。
我个人更推荐先用工具,等熟悉了再手写。毕竟实战中时间就是一切。
# 生成 Windows 下执行 calc.exe 的 shellcode
msfvenom -p windows/exec CMD=calc.exe -f python
# 输出类似这样:
buf = b""
buf += b"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0"
buf += b"\x64\x8b\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b"
# ... 省略
提取出来的 shellcode 是一串十六进制字节。我们需要把它放到 payload 里,然后让程序跳转到它的起始地址。
注意:shellcode 里不能有坏字符。比如 \x00(空字节)会截断字符串,导致 strcpy 提前结束。我遇到过好几次,shellcode 明明没问题,但就是执行不了,最后发现是坏字符在作怪。
完整 payload 构造
一个典型的栈溢出 payload 长这样:
[填充数据] + [返回地址] + [NOP sled] + [Shellcode]
- 填充数据:76 字节,随便填,只要不包含坏字符。
- 返回地址:指向 NOP sled 的起始位置。
- NOP sled:一串
\x90指令,什么都不做,只是滑到 shellcode。 - Shellcode:真正的恶意代码。
为什么要加 NOP sled?因为栈地址可能不太精确。有了 NOP sled,只要返回地址落在 sled 范围内,就能滑到 shellcode。这大大提高了成功率。
# Python 示例
padding = b"A" * 76
ret_addr = b"\x12\x34\x56\x78" # 假设的跳转地址
nop_sled = b"\x90" * 32
shellcode = b"\xfc\xe8\x82..." # 上面生成的
payload = padding + ret_addr + nop_sled + shellcode
避坑指南:我曾经在构造 payload 时,忘记考虑字节序。x86 是小端序,地址要反过来写。比如地址 0x12345678,在 payload 里要写成 \x78\x56\x34\x12。这个坑我踩过不止一次。
知识体系图
下面这张图总结了栈溢出分析的核心流程:
实战中的注意事项
搞栈溢出分析,有几个点我每次都会反复确认:
- 坏字符列表——不同的协议、不同的函数,坏字符都不一样。常见的坏字符有
\x00、\x0a(换行)、\x0d(回车)。我一般会先发一串包含所有字节的测试数据,看哪个字节被截断了。 - 栈地址的稳定性——有些程序开了 ASLR,栈地址每次都不一样。这时候需要找一条
jmp esp或call esp的指令,用它的地址作为跳板。这个技巧叫「ret2esp」,后面会详细讲。 - Shellcode 的大小——缓冲区有限,shellcode 不能太大。我一般控制在 200-300 字节以内。如果功能复杂,可以考虑分阶段加载。
个人习惯:我每次构造完 payload,都会先在调试器里单步跑一遍,确认返回地址确实被覆盖了,跳转也正确。这一步能省掉后面很多调试时间。
总结
栈溢出分析,说白了就是三步:找偏移、控 EIP、塞 shellcode。听起来简单,但实际坑不少。坏字符、字节序、ASLR、栈对齐……每一个细节都可能让你功亏一篑。
不过话说回来,正是这些细节让逆向工程变得有意思。每次成功拿到 shell 的那一刻,嗯,那种成就感是无可替代的。
核心要点回顾:
- 栈溢出的本质是覆盖返回地址,劫持控制流
- 用 pattern_create/offset 确定偏移量
- Shellcode 提取可以用 msfvenom 快速生成
- Payload 结构:填充 + 返回地址 + NOP sled + Shellcode
- 注意坏字符、字节序、ASLR 等细节
公众号:蓝海资料掘金营,微信deep3321