3. 固件漏洞挖掘:静态分析、动态分析与模糊测试入门
固件漏洞挖掘,说白了就是跟藏在二进制里的“地雷”较劲。我做了这么多年固件安全,最深的体会是:没有银弹。静态分析、动态分析、模糊测试,这三板斧各有各的脾气,你得知道什么时候用哪把刀。
这一章,我就带你把这三种方法的核心逻辑捋一遍。不扯太深的理论,重点是你上手就能用的东西。
3.1 静态分析技术:不运行代码,先看“面相”
静态分析,就是对着二进制文件直接开刀。你不需要跑设备,不需要搭环境,拿到固件就能开始翻。我个人习惯,拿到一个新固件,第一件事就是跑一遍静态分析,先摸清它的底细。
3.4.1 文件系统提取与识别
大部分固件都是打包好的文件系统镜像。你得先把它拆开,才能看到里面的配置文件、脚本和二进制程序。
- binwalk:这工具我几乎天天用。它能自动识别固件里的文件系统类型和偏移量。
- dd:配合 binwalk 的结果,用 dd 把特定分区切出来。
- unsquashfs / jefferson:解压 SquashFS 或 JFFS2 文件系统。
实战命令示例:
# 扫描固件,识别文件系统
binwalk firmware.bin
# 提取指定偏移量的分区
dd if=firmware.bin of=rootfs.squashfs bs=1 skip=0x123456
# 解压 SquashFS
unsquashfs rootfs.squashfs
我曾经遇到过一个固件,binwalk 死活识别不出来。后来发现是厂商在文件头前面加了一段自定义的垃圾数据。手动跳过之后,才看到里面的 SquashFS 镜像。嗯,这种“小把戏”其实挺常见的。
3.1.2 字符串与硬编码秘密
解压完文件系统,第一件事就是翻字符串。很多漏洞,其实就藏在那些不该出现的明文里。
- 硬编码凭据:用户名、密码、Token、API Key。
- 调试后门:telnetd、debug shell、未授权的 SSH 密钥。
- 敏感路径:/tmp/update.sh、/var/run/backdoor.pid。
我的小技巧:用 strings 配合 grep 过滤关键词,比如 "password"、"secret"、"key"、"admin"。但别只看这些明显的词。我见过有人把密码藏在 "p@ssw0rd" 或者 "s3cr3t" 这种变体里。多想想攻击者的思路。
3.1.3 反汇编与交叉引用
字符串只是线索。真正要挖漏洞,你得看代码逻辑。这时候就需要反汇编器了。
- Ghidra:开源,功能强大,我最近的主力工具。
- IDA Pro:商业工具,插件生态丰富,但贵。
- Radare2 / Rizin:命令行流,适合自动化脚本。
静态分析的核心,是找“危险函数”。比如 strcpy、sprintf、system、popen。你想想看,如果这些函数的参数来自用户输入(比如 HTTP 请求、MQTT 消息),那基本就是缓冲区溢出或命令注入的温床。
注意:静态分析只能告诉你“这里可能有漏洞”,但不能证明它一定能被利用。因为有些危险函数可能被安全的上层逻辑保护着。所以,静态分析的结果,需要动态分析来验证。
3.2 动态分析技术:让代码跑起来,看它怎么“死”
静态分析是纸上谈兵,动态分析才是真刀真枪。你得让固件在模拟器或真实硬件上跑起来,然后观察它的行为。
3.2.1 固件模拟:用 QEMU 跑起来
不是所有固件都能直接模拟。但大部分基于 Linux 的嵌入式固件,都可以用 QEMU 的用户模式或系统模式跑起来。
- 用户模式模拟:只模拟单个二进制程序,适合分析 Web 服务、后台守护进程。
- 系统模式模拟:模拟整个硬件平台,适合需要完整内核环境的场景。
用户模式模拟示例:
# 假设你提取出了 ARM 架构的 httpd 程序
# 使用 QEMU 用户模式运行它
qemu-arm-static ./bin/httpd
我记得有一次,我模拟一个 MIPS 架构的路由器固件。QEMU 启动后,Web 服务一直报错。折腾了半天,发现是固件里有个硬件 watchdog 定时器,模拟器里没有对应的硬件,导致系统不断重启。最后我 patch 掉了 watchdog 的初始化代码,才顺利跑起来。这种坑,你迟早也会遇到。
3.2.2 运行时监控:看它调用了什么
程序跑起来之后,你得监控它的行为。常用的手段有:
- strace:跟踪系统调用。看它打开了哪些文件,连接了哪些网络地址。
- ltrace:跟踪库函数调用。看它调用了哪些危险函数。
- GDB:动态调试。设置断点,查看内存和寄存器状态。
避坑指南:我曾经在分析一个 UPnP 服务时,用 strace 发现它在处理 SSDP 请求时,会调用 system() 执行一个拼接的字符串。这就是典型的命令注入漏洞。如果你只做静态分析,可能只会看到 system() 这个函数,但不知道它的参数是否可控。动态分析能帮你确认这一点。
3.2.3 网络流量分析
很多固件漏洞,最终是通过网络触发的。所以,抓包分析也是动态分析的重要一环。
- tcpdump / Wireshark:抓取网络流量,分析协议格式。
- Burp Suite:专门分析 HTTP/HTTPS 流量,适合 Web 接口。
- 自定义 Fuzzer:构造畸形数据包,发送给目标服务。
3.3 模糊测试入门:让机器帮你找漏洞
静态和动态分析,都需要你手动去猜漏洞点。但模糊测试不一样——它让机器自动生成大量畸形输入,然后观察目标会不会崩溃。
3.3.1 模糊测试的基本原理
模糊测试的核心就三步:
- 生成输入:基于模板或协议规范,生成大量变异数据。
- 发送输入:把数据发送给目标程序或设备。
- 监控异常:检测目标是否崩溃、挂起、或产生异常输出。
你想想看,人工测试一天可能测几百个用例。但一个 Fuzzer 一秒钟就能生成几万个用例。这就是效率的碾压。
3.3.2 常用 Fuzzer 工具
| 工具 | 适用场景 | 特点 |
|---|---|---|
| AFL (American Fuzzy Lop) | 本地二进制程序 | 基于覆盖率引导,效率高 |
| LibFuzzer | 库函数测试 | 集成在 LLVM 中,适合单元测试 |
| Boofuzz | 网络协议 Fuzz | Python 编写,易于扩展 |
| Mutiny | 网络协议 Fuzz | 基于 PCAP 重放,适合未知协议 |
一个简单的 Boofuzz 脚本示例:
from boofuzz import *
def main():
session = Session(
target=Target(connection=SocketConnection("192.168.1.1", 80, proto="tcp"))
)
define_protocol(session)
session.fuzz()
def define_protocol(session):
s_initialize("HTTP Request")
s_static("GET /")
s_string("FUZZ", fuzzable=True) # 这里会被变异
s_static(" HTTP/1.1\r\n")
s_static("Host: 192.168.1.1\r\n")
s_static("\r\n")
if __name__ == "__main__":
main()
3.3.3 模糊测试的局限性
模糊测试不是万能的。它有几个明显的短板:
- 覆盖率问题:如果代码路径很深,或者需要特定的状态才能触发,Fuzzer 可能永远走不到那里。
- 协议复杂性:对于有状态、有校验和的协议,Fuzzer 生成的畸形包可能直接被协议栈丢弃,根本到不了目标函数。
- 资源消耗:跑一个 Fuzzer 可能需要几天甚至几周的时间,而且需要大量的 CPU 和内存。
注意:我曾经用 AFL 跑一个嵌入式 Web 服务器,跑了三天,一个 crash 都没找到。后来我手动分析了一下,发现 Fuzzer 生成的输入大部分都在 HTTP 解析的早期阶段被过滤掉了。我调整了输入模板,加入了合法的 HTTP 头部,才让 Fuzzer 穿透到更深层的处理逻辑。所以,Fuzzer 不是傻瓜式工具,你得懂它的脾气。
3.4 三种技术的协同:我的工作流
静态分析、动态分析、模糊测试,不是互相替代的关系,而是互补的。我个人的工作流是这样的:
- 先用静态分析摸清结构:提取文件系统,搜索硬编码秘密,反汇编关键二进制,找出所有可能的攻击面(Web 接口、UPnP、MQTT、自定义协议)。
- 再用动态分析验证攻击面:模拟固件,用 strace 和 GDB 观察关键函数的输入输出,确认哪些输入点是可控的。
- 最后用 Fuzzer 批量挖掘:针对确认的攻击面,编写 Fuzzer 模板,进行大规模测试。
这个流程,我用了很多年,效果一直不错。你刚开始做的时候,可能会觉得静态分析太慢,或者 Fuzzer 跑不出结果。别急,慢慢来。每个漏洞的发现,背后都是大量的试错和耐心。
我的建议:别一开始就想着挖 0day。先从已知漏洞的 CVE 复现开始练手。比如去搜索某个路由器型号的历史漏洞,下载对应的固件版本,用我上面说的方法去复现它。当你成功复现一个漏洞时,那种成就感会推着你继续往下走。