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

这就是经典的栈溢出触发点。接下来要做三件事:

  1. 确定偏移量——到底要填多少个字节才能覆盖到返回地址?
  2. 定位返回地址位置——找到返回地址在栈上的精确偏移。
  3. 构造 payload——把返回地址替换成我们想要的地址。

偏移量怎么找?我习惯用 pattern_createpattern_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。这个坑我踩过不止一次。

知识体系图

下面这张图总结了栈溢出分析的核心流程:

栈溢出漏洞分析流程 1. 触发崩溃 发送超长输入 2. 确定偏移 pattern_create/offset 3. 定位返回地址 EIP 控制验证 4. 构造 Payload Payload 组成 填充数据 (76字节) 不含坏字符 返回地址 小端序排列 NOP Sled \x90 填充 Shellcode 恶意代码主体

实战中的注意事项

搞栈溢出分析,有几个点我每次都会反复确认:

  • 坏字符列表——不同的协议、不同的函数,坏字符都不一样。常见的坏字符有 \x00\x0a(换行)、\x0d(回车)。我一般会先发一串包含所有字节的测试数据,看哪个字节被截断了。
  • 栈地址的稳定性——有些程序开了 ASLR,栈地址每次都不一样。这时候需要找一条 jmp espcall esp 的指令,用它的地址作为跳板。这个技巧叫「ret2esp」,后面会详细讲。
  • Shellcode 的大小——缓冲区有限,shellcode 不能太大。我一般控制在 200-300 字节以内。如果功能复杂,可以考虑分阶段加载。

个人习惯:我每次构造完 payload,都会先在调试器里单步跑一遍,确认返回地址确实被覆盖了,跳转也正确。这一步能省掉后面很多调试时间。

总结

栈溢出分析,说白了就是三步:找偏移、控 EIP、塞 shellcode。听起来简单,但实际坑不少。坏字符、字节序、ASLR、栈对齐……每一个细节都可能让你功亏一篑。

不过话说回来,正是这些细节让逆向工程变得有意思。每次成功拿到 shell 的那一刻,嗯,那种成就感是无可替代的。

核心要点回顾:

  • 栈溢出的本质是覆盖返回地址,劫持控制流
  • 用 pattern_create/offset 确定偏移量
  • Shellcode 提取可以用 msfvenom 快速生成
  • Payload 结构:填充 + 返回地址 + NOP sled + Shellcode
  • 注意坏字符、字节序、ASLR 等细节

公众号:蓝海资料掘金营,微信deep3321