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