固件逆向取证:从一坨二进制,到一个可以被解释的系统
一份面向实际分析的固件逆向指南。
重点不在“认识多少工具”,而在于如何建立从实物设备、存储介质、固件镜像、文件系统、可执行程序、数据流到真实行为的完整证据链。
目录
一、先定一个锚点:你究竟在逆向什么 二、固件逆向的核心不是解包,而是建立证据链 三、从硬件到固件:先把软件从设备里拿出来 四、拿到固件后,不要急着打开 Ghidra 五、识别固件结构:先问“边界在哪里” 六、熵:判断“看不懂”到底是哪一种看不懂 七、解包之后,真正的分析才开始 八、先识别架构,否则反汇编全是噪声 九、Ghidra 不是答案生成器,它是推理环境 十、不要从入口函数往下硬啃,要从“锚点”反向追踪 十一、秘密分析:不要问“有没有密钥”,要问“秘密从哪里来” 十二、区分编码、混淆、压缩、加密和哈希 十三、动态分析:让程序自己暴露答案 十四、当软件分析撞上硬件边界:别在固件里找不存在的答案 十五、协议分析:程序不是孤岛 十六、一个更有效的分析框架:四张图 十七、常见突破口:别只盯着“漏洞” 十八、工具链:工具应该按问题分组,而不是按名气排序 十九、一次完整分析应该长什么样 二十、最容易浪费时间的五种方式 二十一、学习路线:不要按工具学,要按能力学 二十二、最后:固件逆向真正训练的是什么 附录 A:一份实战检查表 附录 B:参考工具与资料
很多人第一次接触固件逆向,会自然地把它理解成:
拿到一个
.bin,用 Binwalk 解包,再把可执行文件扔进 Ghidra。
这当然是常见流程,但它只描述了动作,没有描述真正的问题。
固件逆向真正要做的事情是:
把一个不透明的二进制对象,逐步还原成一个能够被解释、验证和推理的系统。
你最终想回答的,通常不是“这个文件里有什么”,而是这些问题:
设备启动时到底做了什么? 哪段代码决定某个功能是否开启? 某个标识、配置、密钥或令牌从哪里来? 数据经过了哪些函数、算法和协议? 一个安全假设到底成立在数学层、实现层,还是只存在于文档里? 某个行为是固件自身完成的,还是由安全芯片、外部 MCU、云端或工厂参数参与完成的?
因此,固件逆向最重要的能力不是“会用工具”,而是知道下一步应该证明什么。
本文不围绕某个品牌或设备,也不把固件逆向包装成神秘的“黑客术”。它讨论的是一套通用方法:如何从真实设备或未知二进制出发,把软件从硬件里拿出来,恢复结构,定位关键逻辑,并用静态与动态证据完成闭环。
本文默认分析对象是你自己拥有、公开提供或已获得授权研究的固件。
一、先定一个锚点:你究竟在逆向什么
固件不是一个统一格式。
“固件”只是一个非常宽泛的名字。它可能是一份升级包,也可能是一整块 Flash 的原始镜像;可能装着完整 Linux 系统,也可能只是几十 KB 的裸机代码。
你拿到的两个文件虽然都叫 firmware.bin,内部结构可能完全不同。
一个典型 Linux 嵌入式固件可能是:
firmware.bin├── vendor header├── bootloader├── kernel├── device tree├── root filesystem├── configuration└── checksum / signature
一个 MCU 固件则可能只是:
firmware.bin├── vector table├── executable code├── read-only data├── initial values└── padding
还有一种更常见的现实情况:
upgrade.pkg└── encrypted container └── compressed image └── signed partition table ├── bootloader ├── kernel ├── rootfs └── application
所以第一条原则是:
不要一上来就“解包”。先确认你手里究竟是什么。
这一步看似基础,却决定后面几乎所有判断。
二、固件逆向的核心不是解包,而是建立证据链
我更愿意把完整逆向过程写成下面这样:
实物设备 / 官方升级包 / 已有镜像 ↓识别硬件、芯片、接口与存储介质 ↓选择最小侵入的提取路径 ↓获得原始字节并校验完整性 ↓识别容器、分区与文件系统 ↓恢复 CPU 架构与运行环境 ↓定位关键程序、配置和数据 ↓建立调用关系与数据流 ↓提出假设 ↓动态验证 ↓得到能够复现的结论
真正成熟的分析,不是:
“我在这个文件里看到一个字符串,所以它一定是密码。”
而是:
“这个字符串被函数 A 引用;函数 A 在启动阶段由 B 调用;参数来自配置分区;最终进入认证函数 C。通过运行时跟踪,可以确认它确实参与了认证计算。”
两者区别非常大。
前者是线索。
后者才是证据。
因此整篇文章只有三个不可放弃的锚点:
先确认对象结构,再深入代码。
任何结论都尽量形成“来源 → 处理 → 使用”的数据链。
静态分析提出假设,动态分析负责验证;不要把猜测当结论。
后面所有工具,都只服务于这三个位置。
三、从硬件到固件:先把软件从设备里拿出来
前面的流程默认了一个很容易被忽略的前提:
你手里已经有固件文件。
可现实里经常不是这样。
你面对的可能只是一台真实设备、一块 PCB、一颗看不懂型号的芯片,甚至只是一个能正常工作的黑盒。设备可以启动、联网、执行功能,但你并没有:
firmware.binupdate.imgrootfs.squashfs
这时固件逆向的第一步,不是 Binwalk,也不是 Ghidra。
而是:
先确定软件住在哪里,再想办法把它完整、可靠地拿出来。
真正完整的链路应该是:
实物设备 ↓识别主控、存储和调试接口 ↓判断软件存放在哪里 ↓选择最小侵入的提取方式 ↓获得原始镜像 ↓确认镜像是否完整、是否加密、是否包含多个分区 ↓进入软件逆向
这一步既是硬件分析,也是固件分析的入口。
3.1 先看板子:不要急着焊线
拿到设备以后,第一件事不是立即接编程器。
先观察。
你需要建立一张最初的硬件地图:
PCB├── 主 SoC / CPU / MCU├── Flash / EEPROM / eMMC / NAND├── RAM├── 电源管理├── 无线芯片├── 安全芯片├── 其他 MCU / DSP├── UART 测试点├── JTAG / SWD 测试点└── 其他未知接口
重点看:
芯片丝印; 封装类型; PCB 上的测试焊盘; 排针; 未焊接接口; 丝印文字,例如 TX、RX、GND、JTAG、SWD;存储芯片附近的走线关系。
很多时候,一张清晰的 PCB 照片加芯片型号搜索,就已经能回答几个关键问题:
CPU 是什么架构?系统可能跑 Linux 还是裸机?外部 Flash 容量多大?是否有独立安全芯片?是否存在第二颗 MCU?
这些信息会直接改变后续路径。
例如:
Linux SoC + SPI NOR + eMMC
和:
Cortex-M MCU + 内部 Flash
不是同一种分析对象。
前者可能有:
Bootloader + Kernel + RootFS + App
后者可能只是:
Vector Table + Code + Data
所以不要把“拆机”理解成物理动作。
它真正的意义是:
恢复系统边界。
3.2 固件通常住在哪里
设备中的软件可能存在多个位置。
最常见的是:
外部 SPI NOR Flash
常见于:
路由器; IoT; 各类嵌入式 Linux 设备; 启动固件; 小型系统。
特点通常是:
容量较小8 引脚封装常见读取相对直接可能包含 Bootloader / Kernel / RootFS
但不要看到 8 引脚芯片就直接认定是 Flash。
先确认型号和数据手册。
NAND Flash
常见于容量较大的嵌入式设备。
相比 NOR,它更复杂,因为原始数据可能涉及:
PageBlockOOBECCBad Block
这意味着:
读出整个芯片的原始字节,不等于已经得到可直接解包的固件镜像。
你可能还需要处理:
页结构; 坏块; ECC; OOB; UBI / UBIFS。
eMMC
很多 Linux 设备和消费电子会使用 eMMC。
它通常不只是一个“固件文件”,而更像一块完整磁盘:
eMMC├── boot0├── boot1├── RPMB└── user area ├── partition 1 ├── partition 2 ├── rootfs ├── data └── ...
因此分析目标可能不是一个 .bin,而是一整张块设备镜像。
MCU 内部 Flash
有些微控制器根本没有外部存储芯片。
程序直接烧录在芯片内部。
这时提取路径可能依赖:
SWD; JTAG; Bootloader; 厂商下载协议; 调试器。
但如果启用了读保护,能否读取取决于具体芯片、安全配置和保护等级。
EEPROM / NVRAM / OTP
这里不一定存放完整程序,但经常保存:
设备序列号; 校准数据; 工厂参数; 密钥材料; 配置; 身份信息。
所以一个很重要的判断是:
“主固件在哪里”和“秘密在哪里”可能是两个不同问题。
你可能已经完整拿到了主 Flash,却仍然找不到某个关键值,因为它根本存放在另一颗芯片里。
安全芯片或安全区域
有些系统会把秘密放在:
Secure Element; TPM; TrustZone 环境; SoC eFuse; OTP; Secure Enclave; 受保护的 Key Storage。
这时主固件可能只包含:
调用接口算法流程句柄密钥编号
却没有真正密钥。
如果逆向到最后发现:
主 CPU 只向安全组件提交请求安全组件内部完成敏感运算
那说明你的分析已经找到了真正的安全边界。
这不是“没找到”。
而是:
答案不在主固件里。
3.3 提取固件,不是只有“拆芯片”这一条路
一个常见误区是:
从硬件拿固件,就要拆机、夹芯片、上编程器。
实际上成熟的顺序应该反过来:
优先选择信息损失最少、侵入程度最低、可重复性最高的方法。
通常可以按下面的顺序尝试。
路径一:官方升级包
这是最便宜的入口。
来源可能包括:
厂商官网; 设备 Web 管理页面; App 下载缓存; OTA 更新; 本地升级文件。
优点:
容易获取不会损坏设备便于重复分析通常已经包含主要软件
缺点:
可能只是增量包可能经过加密可能有签名可能缺少工厂分区可能不含 Bootloader
所以官方升级包适合回答:
厂商准备更新什么?
却不一定能回答:
设备上完整存在什么?
路径二:从正在运行的系统里导出
如果设备已经提供:
Shell; SSH; Telnet; 串口控制台; 调试终端; 已授权维护接口;
那么可以直接从运行系统观察:
/proc/mtd/proc/partitions/sys/lsblkmountdf
这些信息可能告诉你:
分区叫什么大小是多少挂载在哪里底层设备是什么
在有合法权限的研究环境中,可以从块设备或 MTD 设备读取镜像。
这条路径的优势很大:
你不是猜分区结构,而是让正在运行的系统直接告诉你。
但也有局限:
某些区域无法从用户态访问; 安全分区可能受保护; 运行时读取可能得到逻辑视图,而不是原始物理布局; 某些内容会在启动时解密,磁盘上和内存中的形态不同。
路径三:UART 串口
UART 往往是硬件逆向最值得先检查的接口之一。
它可能提供:
Bootloader 日志Kernel 日志启动参数分区信息芯片型号错误信息Shell调试菜单工厂命令
即使没有 Shell,启动日志也可能直接暴露:
CPU内存大小Flash 类型MTD 分区Kernel command lineRootFS 类型启动失败位置
这会极大缩小分析范围。
但 UART 不是“看到三个焊盘就直接接”。
至少要先确认:
GND; TX; RX; 电平; 波特率。
最重要的是电平。
不要默认所有设备都是 3.3V。
可能存在:
1.8V3.3V5V其他电平
错误电平不仅会通信失败,还可能损坏设备。
因此合理顺序是:
识别 GND ↓测量空闲电压 ↓判断可能电平 ↓只监听 TX ↓确认波特率和日志 ↓必要时再连接 RX
先听,再说。
这是一个很好的硬件分析原则。
路径四:JTAG / SWD
JTAG 和 SWD 可能提供更深层的调试能力:
暂停 CPU读取寄存器查看内存设置断点读取 Flash单步执行
它们尤其常见于:
MCU; 嵌入式开发板; 调试阶段设备; 工厂测试环境。
但能否真正使用,取决于:
接口是否暴露引脚是否确认调试是否禁用芯片是否启用读保护安全启动状态调试认证机制
所以不要把 JTAG 当成:
插上就能把固件导出来。
它只是一个潜在入口。
路径五:直接读取外部存储芯片
如果设备使用:
SPI NOR; EEPROM; NAND; eMMC;
可以考虑直接读取存储。
这里又分两种方式。
在板读取(in-circuit)
芯片不拆下,直接在 PCB 上连接。
优点:
风险较低操作方便不用热风拆焊
问题是:
主控可能同时驱动总线其他器件会影响信号供电路径复杂读取结果可能不稳定
所以读完以后不要只看“工具提示成功”。
至少应:
读取多次 ↓分别计算哈希 ↓比较结果
例如:
dump1.bindump2.bindump3.bin
如果三个 SHA-256 不一致,就还不能把任何一个当成可靠原始镜像。
离板读取(off-board)
把芯片拆下后读取。
优点:
电气环境更可控不受板上其他器件干扰
缺点:
操作风险更高可能损伤焊盘或芯片复原成本更高
因此它不应该是第一选择。
合理原则是:
能不拆就不拆;能通过软件拿到,就不要先动热风枪。
路径六:Chip-off 是最后手段,不是仪式感
真正困难的设备可能最终需要:
拆芯片直接读取重建数据结构
但“拆下来读”解决的只是物理访问问题。
后面仍然可能面对:
加密; ECC; OOB; 坏块; 分区表; Secure Boot; 设备唯一密钥; 多芯片协作。
因此:
拿到原始字节,不等于拿到明文固件。
这是硬件到软件之间最重要的一道认知边界。
3.4 一台设备里可能有不止一份固件
很多真实设备不是:
一颗 CPU+一份 firmware.bin
而更像:
主 SoC├── Linux 固件├── Bootloader└── RootFS控制 MCU└── MCU Firmware无线芯片└── Radio Firmware安全芯片└── Internal Applet / Secure Storage
甚至还有:
显示控制器; 电机控制器; FPGA; DSP; 基带; 独立网络模块。
因此,当你研究某个功能时,不应该只问:
主固件里有没有这个函数?
而应该问:
到底是哪颗处理器真正执行了这个功能?
假设流程是:
主 CPU ↓发送命令 ↓串口 / SPI / I²C ↓副 MCU ↓执行真正动作
你在主固件里可能只能看到:
send_command(0x37, data);
真正算法在另一颗芯片里。
这也是为什么硬件地图很重要。
没有系统边界,就容易在错误的二进制里找答案。
3.5 拿到文件以后,先判断你拿到的是哪一层
假设你成功读出:
dump.bin
先不要庆祝。
它可能是:
完整芯片镜像单一分区升级容器压缩包加密区逻辑文件系统原始 NAND 页数据某个 MCU 的代码区
你需要继续回答:
起始地址是什么?总长度是否合理?是否完整?是否重复读取一致?有没有 ECC / OOB?有没有多个分区?是否存在全 0xFF 或全 0x00 填充区?有没有可识别文件头?是否可能经过压缩或加密?
这一步决定了你下一步应该:
直接解包手工切片处理 NAND恢复分区表寻找解密流程还是重新提取
所以真正的链条不是:
硬件 ↓固件 ↓Ghidra
而是:
硬件 ↓识别系统 ↓选择入口 ↓读取原始数据 ↓验证读取可靠性 ↓恢复数据结构 ↓确认目标二进制 ↓逆向
3.6 一个实用的提取决策树
面对未知设备,可以按这个顺序思考:
能否获得官方升级包?├── 能 → 先下载、校验、分析└── 不能 ↓设备是否提供合法维护接口或 Shell?├── 是 → 先获取分区和运行环境信息└── 否 ↓PCB 上是否存在 UART?├── 是 → 先监听启动日志└── 否 ↓是否存在 JTAG / SWD 或已知调试接口?├── 是 → 判断保护状态和可读范围└── 否 ↓是否存在外部 Flash / eMMC / NAND?├── 是 → 优先考虑非侵入或在板读取└── 否 ↓判断是否为 MCU 内部 Flash、受保护存储或其他封装
这个顺序背后的原则很简单:
先拿最便宜的证据,再逐步增加侵入性。
不要一开始就做成本最高、风险最大的事情。
3.7 提取阶段真正要保存什么
从硬件拿固件,不应该只留下一个 dump.bin。
建议至少记录:
设备型号硬件版本PCB 版本芯片型号芯片丝印照片接口位置电压连接方式读取工具读取参数原始 dump多次 dump 的哈希提取日期是否修改过设备
如果是多芯片设备,可以这样组织:
case/├── photos/├── board-map/├── datasheets/├── dumps/│ ├── spi-nor/│ ├── emmc/│ └── mcu/├── hashes/├── extracted/├── notes/└── analysis/
原因很现实。
几个月以后,你很可能已经忘了:
这个
final_dump_v2_ok.bin到底是从哪颗芯片、用什么参数、在什么状态下读出来的?
而逆向最怕的,就是证据来源断掉。
四、拿到固件后,不要急着打开 Ghidra
4.1 保存原始样本
原始样本永远不要直接修改。
建议至少保留:
case/├── original/│ └── firmware.bin├── working/├── extracted/├── notes/└── hashes.txt
计算哈希:
sha256sum firmware.bin
作用并不复杂:确保后来分析的仍是同一个样本。
当你开始切片、解包、修补文件头、修改校验和之后,如果没有原始镜像和哈希,很容易失去基准。
4.2 先看最便宜的信息
第一轮分析追求的不是深入,而是快速建立轮廓。
file firmware.bin
xxd -l 256 firmware.bin
strings -a -n 6 firmware.bin | head
你可能立即看到:
文件系统名字; Linux 版本; U-Boot 标识; 编译器信息; 芯片型号; URL; 错误日志; 配置路径; 函数名; 压缩格式; 厂商自定义头部。
这一步的价值不在于“找到秘密”,而在于建立最初的地图。
例如:
Squashfs filesystemLinux version 5.xBusyBox/etc/init.d/ARM
这已经足够形成一个初步判断:
这是一个 Linux 类嵌入式固件 ↓存在 SquashFS 根文件系统 ↓大概率能恢复 shell 脚本、配置和 ELF 程序 ↓后续重点应放在启动链、服务、配置和关键二进制
这比盲目地把整个固件扔进反编译器有效得多。
五、识别固件结构:先问“边界在哪里”
固件解析的第一个真正难点,是找到边界。
你需要回答:
哪一段是头部? 哪一段是压缩数据? 哪一段是内核? 哪一段是文件系统? 是否存在多个分区? 有没有加密区? 有没有签名或校验字段?
常用工具之一是 Binwalk。它的核心作用不是“神奇解包一切”,而是根据文件特征识别嵌入的数据、压缩流、文件系统以及其他结构。
基础扫描:
binwalk firmware.bin
自动提取:
binwalk -e firmware.bin
递归提取:
binwalk -Me firmware.bin
但要注意:
Binwalk 的输出是候选解释,不是真理。
一个字节序列偶然匹配 magic signature,并不代表那里一定存在真正的文件。
因此看到:
123456 Squashfs filesystem
你还应该继续验证:
偏移是否合理? 长度是否越界? 是否能成功挂载或提取? 文件系统内部是否具有一致结构? 前后数据是否符合预期?
必要时手工切片:
dd if=firmware.bin of=part.bin bs=1 skip=123456
逆向中很重要的一种习惯是:
工具负责提出候选解释,人负责判断解释是否成立。
六、熵:判断“看不懂”到底是哪一种看不懂
某一段数据高度随机,并不自动等于“被加密”。
它可能是:
加密数据; 高压缩率数据; 已压缩文件系统; 伪随机填充; 厂商自定义编码; 真正的随机数据。
这时可以看熵(Entropy)。
直观理解:
规律很多 → 熵较低随机性强 → 熵较高
Binwalk 可以辅助观察熵变化。
真正有价值的是边界:
低熵区 ↓突然进入高熵区 ↓长时间保持 ↓再次回到低熵区
这通常说明固件内部存在不同性质的数据区域。
但熵只能告诉你:
“这一段看起来很随机。”
它不能直接告诉你:
“这一段是 AES-256-CBC 加密。”
因此一个更合理的判断链是:
高熵 ↓排除已知压缩格式 ↓检查文件头和容器结构 ↓寻找解密函数、密钥来源或升级流程 ↓动态验证
不要看到高熵就写“固件被加密”。
这是固件分析里非常典型的过度结论。
七、解包之后,真正的分析才开始
很多教程把“成功解出 SquashFS”当作重大突破。
实际上,对于 Linux 类固件,这往往只是拿到了目录。
比如:
squashfs-root/├── bin/├── etc/├── lib/├── sbin/├── usr/├── var/└── www/
接下来不要漫无目的地翻文件。
应该先建立系统地图。
7.1 先找启动链
重点看:
/etc/inittab/etc/init.d//etc/rc*/etc/systemd//etc/config/
你真正想知道的是:
设备上电 ↓谁成为第一个用户态进程 ↓启动哪些脚本 ↓脚本启动哪些服务 ↓服务读取哪些配置 ↓关键功能在哪个程序里
举例:
/etc/init.d/S50service ↓/usr/bin/device_service ↓读取 /etc/device.conf ↓访问 /dev/xxx ↓监听 TCP 端口
到这里,你已经从“一堆文件”得到了一条行为链。
7.2 文件系统里最值得先看的位置
第一遍通常优先检查:
/etc//etc/init.d//etc/config//usr/bin//usr/sbin//bin//sbin//lib//www//var/
重点搜索:
passwordpasswdsecrettokenapikeyprivatecertificatekeydebugfactoryseriallicenseencryptdecryptauth
例如:
grep -RniE 'password|secret|token|apikey|private|encrypt|decrypt' squashfs-root/
但仍然要记住:
搜到敏感字符串,不等于发现漏洞。
你必须判断它是什么:
示例配置? 默认值? 哈希? 公钥? 证书? 测试数据? 真正的共享秘密? 是否在运行时被使用?
一个公开证书和一个私钥,安全意义完全不同。
一个默认密码和一个从未被启用的测试字符串,也完全不同。
八、先识别架构,否则反汇编全是噪声
拿到一个可执行文件后,不要急着导入 Ghidra。
先确认:
文件格式; 32 位还是 64 位; CPU 架构; 大端还是小端; 是否动态链接; 是否剥离符号; 依赖哪些库。
例如:
file ./usr/bin/device_service
readelf -h ./usr/bin/device_service
readelf -d ./usr/bin/device_service
常见架构包括:
ARM / AArch64 MIPS / MIPS64 x86 / x86_64 PowerPC RISC-V Xtensa
嵌入式领域还有大量 MCU 架构和 DSP 架构。
对于 ELF 文件,通常比较容易识别。
真正麻烦的是裸二进制:
firmware.bin
它没有:
ELF header; section table; symbol table; 明确加载地址。
这时候需要从其他证据推断:
芯片型号; 向量表; 指令特征; 字节序; 地址常量; 固件升级文档; PCB 上的 SoC 丝印; SDK 或开源代码。
一个错误的架构选择,会让反汇编器生成大量“看起来像代码”的垃圾。
所以遇到完全不可读的反汇编时,先别怀疑自己。
先怀疑:
架构、字节序、加载地址是不是错了?
九、Ghidra 不是答案生成器,它是推理环境
Ghidra 能做反汇编、反编译、交叉引用、控制流分析和脚本自动化。
但反编译出来的 C 伪代码,不是原始源码。
比如:
undefined4 FUN_001024ac(int param_1, char *param_2)
这里的信息很少。
逆向的工作,是逐步把它改造成:
int verify_license(DeviceContext *ctx, const char *license)
这不是“改个名字好看”。
这是在建立认知模型。
一个有效的 Ghidra 分析过程通常是:
发现字符串 ↓查看 XREF ↓进入引用函数 ↓理解参数 ↓重命名函数 ↓重命名变量 ↓定义结构体 ↓追踪上游调用者 ↓追踪下游数据流
例如你发现字符串:
authentication failed
先看谁引用它。
然后看到:
if (result != 0) { log("authentication failed");}
继续追踪 result:
result = FUN_00108210(ctx, input);
进入函数,发现:
hash(input, temp);return compare(temp, stored_value);
再继续追踪 stored_value 从哪里来。
最后可能得到:
配置分区 ↓读取设备参数 ↓解码 ↓存入内存结构体 ↓认证函数读取 ↓参与比较
这才是完整的逆向。
十、不要从入口函数往下硬啃,要从“锚点”反向追踪
大型程序可能有几千甚至几万个函数。
从 main() 开始逐行看,往往效率极低。
更好的方式是寻找锚点。
常见锚点包括:
错误字符串; 日志文本; 文件路径; URL; 命令名称; 协议字段; JSON 键名; 算法常量; 设备节点; 系统调用; 导入函数; 已知库函数。
比如你关心配置加载。
搜索:
/etc/device.conf
然后:
字符串 ↓XREF ↓open() ↓read() ↓parse_config() ↓数据写入结构体 ↓其他函数读取结构体
你关心网络认证:
"Authorization""token""login""/api/auth"
然后从这些位置逆推。
你关心密码实现:
AESSHAHMACEVP_mbedtls_wolfSSL
或者识别算法特征常量和 S-Box。
真正高效的逆向往往不是顺着程序读,而是:
先找到和问题最相关的可见痕迹,再沿交叉引用向上下游扩展。
十一、秘密分析:不要问“有没有密钥”,要问“秘密从哪里来”
这是固件安全分析最容易产生误解的地方。
很多人认为:
找到密钥 = 破解
有时候成立,有时候完全不成立。
先区分几种情况。
11.1 固件中存在硬编码秘密
例如:
static const uint8_t key[] = { 0x91, 0x27, 0x3a, ...};
如果这段数据确实参与敏感运算,那么需要继续确认:
是否所有设备共用? 是否只用于测试? 是否用于解密本地数据? 是否用于网络认证? 是否有设备级派生过程? 是否可以远程利用? 是否只是公开材料,例如公钥?
真正的问题不是“字符串像不像密钥”,而是:
它具有什么安全权力。
11.2 固件里只有算法,没有秘密
例如:
ciphertext ↓AES-256 ↓key 从安全芯片读取
你可以完全恢复算法,却仍然拿不到密钥。
这不叫失败。
因为你已经明确了真正边界:
固件不是秘密来源安全边界在安全芯片
接下来的研究对象已经发生变化。
11.3 密钥由设备信息派生
例如概念上:
device_id +factory_secret +KDF ↓device_key
这时需要继续追踪:
device_id从哪里来? factory_secret在哪里? KDF 是什么? 每台设备是否不同? 是否存在固定盐或固定种子?
注意:知道派生算法,不一定意味着能恢复密钥。
关键仍然是:
输入中是否包含真正未知且无法读取的秘密。
11.4 密钥在运行时出现
有些秘密不静态存在于固件镜像中,但运行时必须进入内存。
例如:
安全存储 ↓启动时解封 ↓进入 RAM ↓用于解密 ↓清理
静态搜索找不到,不代表不存在。
这也是动态分析、调试和内存观察的价值所在。
十二、区分编码、混淆、压缩、加密和哈希
很多误判来自把这五件事混在一起。
编码
目的是表示数据。
例如:
Base64HexTLVASN.1
通常可逆,不需要秘密。
混淆
目的是增加理解成本。
例如:
XOR字符串拆分控制流打乱变量重命名
不一定提供真正密码学安全。
压缩
目的是减少体积。
例如:
gzipxzLZMAzstd
高压缩数据通常也具有较高熵。
加密
目的是在没有密钥时隐藏内容。
例如:
AESChaCha20
核心是密钥。
哈希
目的是把任意输入映射到固定长度摘要,通常设计为单向。
例如:
SHA-256SHA-3
不能因为看到 32 字节数据,就断定它是“256 位密钥”。
它也可能是:
SHA-256 摘要; 随机 ID; ECC 参数; 校验值; 数据结构的一部分。
逆向里最危险的错误之一,是根据长度直接给数据贴标签。
十三、动态分析:让程序自己暴露答案
静态分析回答的是:
代码可能做什么。
动态分析回答的是:
这一次运行时,它实际做了什么。
两者缺一不可。
常见动态观察对象:
函数调用; 文件访问; 网络连接; 系统调用; 参数; 返回值; 内存; 设备节点; 日志; IPC; 环境变量。
Linux 用户态程序在兼容条件下,可以借助 QEMU 用户模式运行其他 CPU 架构的程序。
概念上:
x86_64 主机 ↓qemu-arm / qemu-mips ↓ARM 或 MIPS 用户态程序
完整系统仿真则尝试恢复:
CPU+内存+内核+根文件系统+部分硬件设备
但这里必须保持现实感:
不是所有固件都能直接 QEMU 一键启动。
常见障碍:
自定义内核; 私有驱动; 未支持的 SoC; 特殊外设; watchdog; 安全芯片; 硬件寄存器; 厂商私有设备节点。
因此动态分析通常有三个层次。
13.1 单程序运行
只运行一个目标二进制。
适合:
解析器; 网络服务; 算法模块; 数据处理程序。
13.2 部分环境模拟
提供:
rootfs; 动态库; 配置; 必要设备节点的替代实现。
目标不是恢复整个设备,而是让关键服务运行起来。
13.3 完整系统仿真
模拟内核和系统环境。
适合更复杂的行为验证,但成本也最高。
一个重要原则是:
不要为了“完整仿真”而完整仿真。只模拟足以回答当前问题的部分。
如果你只想知道一个配置解析函数如何工作,让目标进程跑起来就够了。
没必要先重建整个硬件世界。
十四、当软件分析撞上硬件边界:别在固件里找不存在的答案
前面已经讲过如何通过 UART、JTAG、SWD 和存储芯片把固件拿出来。
这里讨论的是另一个问题:
当你已经拿到主固件,为什么有些答案还是找不到?
原因往往不是逆向能力不够,而是问题跨出了主 CPU 的软件边界。
现实设备可能是:
主 SoC ↓驱动 / IPC / 总线 ↓副 MCU ↓安全芯片 ↓传感器 / 执行器
你在 Linux 主固件里看到的,也许只是:
send_command(0x42, payload);
真正的判断、算法或秘密可能在另一颗芯片里。
因此,当某条数据流突然“消失”时,不要无限追问不存在的函数。
先问:
它是否通过 /dev 节点离开用户态?是否进入 ioctl?是否通过 I²C / SPI / UART 发给其他芯片?是否调用 TEE / Secure Element 接口?是否读取 eFuse / OTP / 独立 EEPROM?
一个典型边界可能是:
Application ↓Library ↓ioctl() ↓Kernel Driver ↓SPI ↓Secure MCU
如果你只分析 Application,自然找不到最终实现。
UART 在这里的价值:观察另一端说了什么
UART 不只用于提取启动日志。
它也可能是两个组件之间的运行时通信链路。
你需要关注:
谁发起通信?消息有没有固定帧头?长度字段在哪里?有没有校验?有没有请求/响应关系?某个用户动作对应哪条消息?
这里的目标已经不是“拿固件”。
而是:
恢复组件之间的协议。
JTAG / SWD 在这里的价值:观察活着的状态
如果目标 MCU 可调试,JTAG 或 SWD 的价值可能是:
暂停在关键函数观察寄存器观察 RAM确认某个分支确认某个值何时出现
也就是说,它们从“提取入口”变成了“动态证据来源”。
安全芯片:有时真正的秘密从未进入主固件
某些系统的设计就是:
主 CPU 提交数据 ↓安全芯片内部计算 ↓只返回结果
主 CPU 可能永远拿不到真正密钥。
这时即使你完整逆向:
BootloaderKernelRootFSApplication
也不会找到目标秘密。
真正的结论应该是:
主固件只持有调用逻辑,不持有密钥本体;安全边界位于独立安全组件。
这是一个有效结论,而不是分析失败。
什么时候应该停止在主固件里继续找
可以用三个问题判断:
继续分析更深的主固件代码,会不会改变可见的数据流?
关键数据是否已经明确跨过了硬件接口?
真正决定结果的组件是否已经变成另一颗芯片?
如果答案明确,那么应该停止。
继续在错误边界里“深挖”,只会增加噪声。
这也是系统逆向很重要的一条原则:
先确认计算发生在哪里,再研究计算如何发生。
十五、协议分析:程序不是孤岛
一个设备的关键行为可能跨越多个组件:
Host software ↓Network / USB / Serial ↓Main processor ↓Secondary MCU ↓Secure element
如果只逆向主处理器固件,可能永远找不到答案。
所以需要问:
数据从哪里进入? 谁处理第一层协议? 谁做验证? 谁保存秘密? 谁最终执行动作?
例如:
输入数据 ↓网络服务 A ↓IPC ↓守护进程 B ↓驱动 ↓MCU
你在 A 中看到的可能只是转发逻辑。
真正算法在 B。
真正秘密又在 MCU。
因此固件逆向最终经常变成:
系统逆向。
固件只是入口。
十六、一个更有效的分析框架:四张图
面对复杂固件,我建议至少维护四张图。
16.1 结构图
回答:
固件由什么组成?
Firmware├── Header├── Bootloader├── Kernel├── RootFS├── App└── Data
16.2 启动图
回答:
系统如何从上电走到业务功能?
Boot ROM ↓Bootloader ↓Kernel ↓init ↓startup scripts ↓services
16.3 数据流图
回答:
某个关键数据从哪里来,经过什么处理,去了哪里?
Input ↓Parser ↓Decoder ↓Validator ↓Storage / Action
16.4 信任边界图
回答:
系统究竟相信谁?
External input ↓[validation boundary] ↓Main firmware ↓[privileged boundary] ↓Secure component
很多安全问题,本质上都发生在边界处:
不该信的数据被信了; 不该共享的秘密被共享; 不该由普通组件承担的安全职责,被普通组件承担; 文档声称存在边界,实际代码没有。
十七、常见突破口:别只盯着“漏洞”
固件分析并不等于 CVE 搜索。
真正有价值的突破口通常包括下面几类。
17.1 默认配置
例如:
默认凭据; 调试模式; 测试接口; 宽松权限; 不安全服务。
17.2 硬编码秘密
重点不是“看见 key 字样”,而是确认:
秘密 ↓由谁读取 ↓参与什么运算 ↓影响什么安全边界
17.3 不安全更新链
要问:
是否验证完整性? 是否验证签名? 谁持有验证材料? 是否允许降级? 是否绑定设备型号? bootloader 是否参与验证?
这里需要区分:
checksum ≠ cryptographic signature
CRC 能发现偶然损坏,不等于能抵抗恶意修改。
17.4 调试和工厂逻辑
生产固件中可能残留:
factory mode; engineering mode; hidden menu; debug commands; test accounts; diagnostic services。
但同样需要验证它们是否真正可达。
一个函数存在,不代表正常运行路径会调用它。
17.5 第三方组件
固件中经常包含:
BusyBox; OpenSSL; mbedTLS; Web server; 数据库; 压缩库; 多媒体库。
识别版本可以帮助判断已知风险,但不要简单地做:
版本号匹配 CVE =设备一定可利用
真正需要确认:
代码是否真的存在? vulnerable function 是否编译进去? 运行时是否可达? 配置是否满足触发条件? 厂商是否 backport 了补丁?
版本匹配只是线索。
十八、工具链:工具应该按问题分组,而不是按名气排序
固件结构识别
常用:
filexxdhexdumpstringsbinwalk7zdd
目标:
确认文件结构、边界、压缩格式和嵌入对象。
文件系统处理
常见对象:
SquashFSUBIFSJFFS2CramFSext*FAT
工具依文件系统而定,例如:
unsquashfsubireaderjeffersonmount
目标:
恢复尽可能接近设备运行时的目录结构。
二进制基础分析
filereadelfobjdumpnmstringsldd(仅在适用环境)
目标:
识别架构、ABI、依赖、符号和程序基本属性。
逆向工程
常用:
GhidraIDABinary Ninjaradare2 / Cutter
目标:
恢复函数、控制流、调用关系、数据引用和程序语义。
对于多数个人研究者,Ghidra 已经足够作为主力工具。
动态运行与仿真
QEMUgdb / gdb-multiarchstraceltracechroot容器化辅助环境
目标:
验证程序实际行为,而不是继续猜。
自动化分析
常见项目包括:
EMBAFACT
它们可以帮助自动完成部分:
固件提取; 静态扫描; 组件识别; 漏洞线索整理; SBOM; 报告生成。
但自动化系统最适合做:
扩大搜索面。
它不能替代:
确认某个具体结论是否成立。
十九、一次完整分析应该长什么样
假设你拿到一个未知固件:
update.bin
正确流程不应该是:
看到设备 ↓随便找一个 .bin ↓binwalk -Me ↓grep password ↓打开 Ghidra
而更接近下面这样。
第零步:确认软件从哪里来
先回答:
我面对的是实物设备、官方升级包,还是已有镜像?主程序在哪颗芯片?是否还有副 MCU、安全芯片或独立存储?最小侵入的提取路径是什么?
能从官方升级包开始,就不要先拆芯片。
能从运行系统获得分区信息,就不要先猜。
如果必须从硬件读取,则保留:
芯片型号连接方式电压读取参数多次 dump哈希
第一步:确认样本
记录:
文件名来源大小SHA-256获取日期对应设备/版本
第二步:建立外层结构
回答:
是否存在厂商头?是否压缩?是否加密?是否签名?有几个分区?每段偏移是多少?
形成:
0x000000 Header0x001000 Bootloader0x080000 Kernel0x400000 RootFS0xF00000 Data
这一步的产物不是截图。
是地图。
第三步:恢复文件系统
识别:
init systemstartup scriptsservicesconfigurationweb interfaceexecutableslibrariescertificates
形成第一版组件清单。
第四步:定义问题
不要说:
我要全面逆向整个固件。
这通常意味着没有目标。
应该说:
我要确认设备身份参数从哪里读取。
或者:
我要确定升级包是否经过签名验证。
或者:
我要知道认证结果由哪个模块决定。
目标越明确,逆向越快。
第五步:从可见锚点进入
例如:
字符串文件路径日志API 路由配置字段设备节点导入函数算法特征
建立 XREF。
第六步:恢复数据流
例如:
/etc/device.conf ↓load_config() ↓decode_value() ↓DeviceContext.secret ↓verify_request()
到这里,结论已经越来越接近系统真实逻辑。
第七步:动态验证
确认:
文件是否真的被读取; 函数是否真的被执行; 参数是否符合静态判断; 关键数据是否真的参与运算; 不同输入是否产生预期差异。
第八步:写结论
一个好的逆向结论应该能够回答:
发现了什么?证据是什么?影响范围是什么?哪些部分已经确认?哪些仍然只是推测?如何复现?
例如:
程序
service_a在启动时读取/etc/device.conf中的字段device_token。该值经函数decode_token()处理后写入全局上下文,并在verify_request()中参与 HMAC 比较。静态交叉引用和运行时文件访问记录均支持这一判断。
这比:
“固件里发现了 token,可能存在硬编码密钥。”
强得多。
二十、最容易浪费时间的五种方式
1. 从 main 开始逐行看
大型二进制里极低效。
先找与问题有关的字符串、路径、API、函数和数据。
2. 迷信自动解包
自动提取失败,不代表固件无法分析。
可能只是:
私有文件头; 错误长度; 轻量混淆; 多层容器; 自定义校验。
3. 看到高熵就认定加密
高熵也可能是压缩。
必须继续验证。
4. 看到敏感字符串就认定漏洞
字符串只是线索。
必须证明它被谁使用、如何使用、有什么权限。
5. 追求“完全理解整个固件”
多数时候没必要。
真正有效的方法是:
明确问题 ↓找到锚点 ↓恢复相关数据流 ↓验证 ↓停止
当更深层分析不会改变当前结论和行动空间时,就应该停止。
逆向不是考古竞赛。
目标不是把每个函数都改名。
二十一、学习路线:不要按工具学,要按能力学
第一阶段:看懂系统
先掌握:
Linux 基础; 文件系统; 进程; 权限; 启动流程; 网络基础; Shell; C 语言。
你要能回答:
一个 Linux 程序如何启动?如何加载动态库?如何读取配置?如何访问设备?如何监听网络?
第二阶段:看懂二进制
掌握:
CPU 基础; 寄存器; 调用约定; 栈; 堆; ELF; 汇编; 反编译。
不需要一开始就成为汇编专家。
先做到:
能把反编译代码和底层指令对应起来,知道反编译器什么时候可能骗你。
第三阶段:掌握固件结构
学习:
Bootloader; Kernel; Device Tree; RootFS; Flash; 分区; 固件升级。
你会逐渐发现:
“固件逆向”其实不是单纯的软件逆向,而是计算机系统知识的交叉应用。
第四阶段:动态验证
学习:
QEMU; GDB; strace; 调试; 仿真; 环境修复。
这一步会让你从:
我觉得代码是这样运行。
进入:
我已经看到它确实这样运行。
第五阶段:建立自己的证据习惯
最终真正拉开差距的,不是工具。
而是记录。
建议每个项目至少维护:
目标已知事实当前假设支持证据反对证据待验证项最终结论
不要只记命令。
要记:
为什么执行这条命令,以及结果改变了什么判断。
二十二、最后:固件逆向真正训练的是什么
表面看,固件逆向是在做:
解包反汇编反编译调试
但真正训练的是另一种能力:
面对一个完全不透明的系统,不靠文档承诺,而靠证据逐步恢复它真实的结构和行为。
一个成熟的逆向者看到未知固件,不会立即问:
用什么工具打开?
而会先问:
我现在已经知道什么?什么仍然未知?哪个未知量最关键?我能从哪里得到最便宜的证据?什么结果可以证伪当前假设?
这也是为什么固件逆向既不是纯粹的“破解”,也不是单纯的工具操作。
它更像一种技术侦查:
从痕迹建立结构从结构寻找路径从路径提出假设从行为验证结论
最后得到的不是“我看懂了一些代码”。
而是:
我知道这个系统为什么会这样工作,也知道我的结论依赖哪些证据。
这才是固件逆向真正值得学习的地方。
附录 A:一份实战检查表
硬件与提取
记录设备型号、硬件版本和 PCB 版本 拍摄主板与芯片丝印 识别主 SoC / MCU 识别外部 Flash / NAND / eMMC / EEPROM 检查是否存在副 MCU 或安全芯片 检查 UART / JTAG / SWD 测试点 优先寻找官方升级包 判断是否能从运行系统获得分区信息 选择最小侵入的提取方式 多次读取并比较哈希 区分完整芯片镜像、单分区、升级包和逻辑文件系统 记录提取工具、电压、连接方式和参数
样本
保存原始文件 计算 SHA-256 记录来源、版本、设备信息 建立 working copy
结构
file查看文件头 stringsBinwalk 扫描 检查熵 确认分区边界 验证提取结果
文件系统
确认文件系统类型 恢复目录 检查启动脚本 检查配置 检查网络服务 检查 Web 资源 检查证书与密钥材料 建立可执行文件清单
二进制
确认 CPU 架构 确认字节序 确认 ELF / raw binary 查看动态依赖 查看字符串 选择关键程序 建立 XREF 恢复函数和变量语义 绘制调用链 绘制数据流
动态分析
明确要验证的假设 判断单程序运行是否足够 判断是否需要部分环境 判断是否需要完整系统仿真 记录文件访问 记录网络行为 记录系统调用 观察关键参数 对比静态结论
输出
区分事实与推测 每个重要结论有证据 说明影响范围 说明未解决问题 给出复现路径 保留分析日志和哈希
附录 B:参考工具与资料
以下项目适合继续深入,具体命令与版本应以各项目当前官方文档为准。
Ghidra
https://github.com/NationalSecurityAgency/ghidraBinwalk
https://github.com/ReFirmLabs/binwalkQEMU Documentation
https://www.qemu.org/docs/master/OWASP IoT Security Testing Guide — Firmware
https://owasp.org/owasp-istg/03_test_cases/firmware/EMBA Firmware Security Analyzer
https://github.com/e-m-b-a/embaFACT — Firmware Analysis and Comparison Tool
https://github.com/fkie-cad/fact_coreOpenOCD
https://openocd.org/U-Boot
https://github.com/u-boot/u-boot
这篇文章只给出了一条主线:从二进制恢复系统,从系统追踪数据,从数据验证行为。
工具会变,文件格式会变,芯片会变,但这条主线通常不会变