Jetset:针对嵌入式系统的目标导向固件 Rehosting 技术
文献来源: Usenix Security 2021
作者团队: Evan Johnson (UCSD), Maxwell Bland (UIUC), YiFei Zhu (UIUC), Joshua Mason (UIUC), Stephen Checkoway (Oberlin College), Stefan Savage (UCSD), Kirill Levchenko (UIUC)
在物联网和嵌入式设备无处不在的今天,固件安全分析变得尤为重要。然而,安全研究人员在分析固件时经常面临一个令人头疼的难题:固件严重依赖特定的硬件外设,离开物理设备往往寸步难行。
在 USENIX Security 2021 上,来自 UCSD、UIUC 等高校的研究团队提出了一种名为 Jetset 的新方案,通过符号执行自动推断外设行为,成功实现了针对目标代码路径的固件“重托管”(Firmware Rehosting)。
为什么固件分析这么难?
分析固件的终极目标通常是触发特定的代码区域:比如到达用户态初始化阶段、触发网络协议栈、访问系统调用接口,或者仅仅是为了让固件跑起来以便进行模糊测试(Fuzzing)。
目标是:固件重托管(Firmware Rehosting)—— 即在没有物理硬件的情况下执行固件。
但在传统的模拟器(如 QEMU)中直接运行固件通常会失败。因为固件会通过 MMIO(内存映射I/O) 或 I/O 寄存器与 GPIO、传感器、通信接口等硬件外设进行交互。如果模拟器中缺少这些设备模型,固件执行就会立刻崩溃或陷入死循环。
现有方案的局限性
在实现重托管时,研究人员常常面临**“模拟准确度”与“实现难度”**之间的权衡。现有的几种主流方法都存在明显的局限:
- 不进行重托管的纯静态/动态分析:需要源码或特定平台的支持,适用性极低。
- 硬件在环(Hardware-in-the-loop):将模拟器与真实硬件连接。虽然保真度高,但需要购买物理设备,无法规模化扩展,且有时候硬件根本买不到。
- 全量重托管(Full Rehosting):利用硬件抽象层(HAL)、执行踪迹或系统知识构建完整的模拟器。缺点是严重依赖这些辅助信息,而这些信息往往很难获取。
- 基于 Fuzzing 的部分重托管:通过模糊测试来推断设备行为。缺点是难以扩展到复杂的固件,且无法处理诸如“校验和”或“魔术值(Magic values)”这类复杂的约束条件。
Jetset 的核心洞见:固件本身就是“说明书”
面对上述困境,Jetset 提出了一个反直觉但非常关键的洞见:
我们根本不需要 100% 完美的硬件保真度。
对于安全分析师来说,他们只关心特定的目标代码区域能否被执行。因此,外设的行为只需要满足一个条件:足以让程序的执行向前推进即可。
Jetset 的核心假设是:固件的代码中“隐式”地编码了它所期望的设备行为。
举个简单的例子:
if (status == GPIO_READY) { ... }从这段代码就可以看出,要让程序继续执行,设备(模拟器)只需要在被读取时返回 GPIO_READY 这个值即可。
因此,Jetset 的思路是:既然固件有要求,那我们就用符号执行(Symbolic Execution)来推断出这些设备应该返回什么值。
Jetset 的工作原理
Jetset 的输入包括:固件二进制文件、入口点、目标地址(你希望执行到的地方)以及内存布局(RAM 与 MMIO 的范围)。它的最终输出是为 QEMU 生成的合成外设模型。
整个工作流程分为两个主要阶段:
阶段一:外设推断 (Peripheral Inference)
在这个阶段,Jetset 通过对固件进行符号执行,推断出目标设备寄存器需要满足的值。
- 推断 I/O 约束:Jetset 将所有对 MMIO 地址空间的读取操作设为“符号变量”,而 Flash 和内存的初始内容则保持为具体值。通过探索依赖设备行为的各种可能路径,收集约束条件。
- 目标导向搜索:利用控制流图(CFG)来引导符号执行,直奔目标地址。
- 中断注入:在很多复杂的嵌入式系统中,目标代码(例如某个内核线程)和入口点通常不在同一个上下文中运行。由于设计良好的系统通常对中断的精确时序不敏感,Jetset 会周期性地注入中断,从而成功触发并进入目标地址所在的代码。
阶段二:外设合成 (Peripheral Synthesis)
拿到阶段一收集到的路径约束后,Jetset 开始合成轻量级的外设模型。
- 约束求解:Jetset 使用 Z3 SMT 求解器,将符号化的 MMIO 值具象化(Concretize),生成一系列满足所有约束的实际读取序列。
- 生成合成设备:Jetset 按照 MMIO 地址对 I/O 踪迹进行划分。每个地址都会获得一个属于自己的“返回值队列”。当固件读取该地址时,模型会按顺序从队列中消耗返回值;一旦队列耗尽,模型就会不断重复返回队列中的最后一个值。
巧妙的搜索策略
为了能在庞大的代码状态空间中找到通往深层安全关键点的路径,Jetset 结合了多种搜索策略:
- 禁忌搜索(Tabu Search):编码特定领域的知识,优化路径优先级并改善回溯机制。
- 上下文敏感的距离计算:
- 调用栈距离(Callstack Distance):衡量执行需要沿当前调用栈向上返回多远。
- 调用链距离(Callchain Distance):衡量执行需要向下进行多少次函数调用才能接近目标。
- 交替决策:在“探索新路径”和“利用启发式算法深入”之间取得平衡。
- 智能回溯:当执行进入不太可能成功启动的错误状态(Error states)时,Jetset 会果断终止当前路径并进行回溯。
实现与评估效果
Jetset 的实现结合了 angr(进行了二次开发)和 Z3 SMT 进行符号执行,并使用 QEMU 作为最终的运行模拟器。整个系统包含约 5500 行 Python 代码和 2000 行 C 代码。
研究团队在四款不同的真实设备(涵盖 ARM, i386, ColdFire 架构)上进行了测试,取得了令人瞩目的成果:
| 测试目标 | 架构 | 核心测试成果摘要 |
|---|---|---|
| Raspberry Pi 2 | ARM | 成功使用合成的外设在 QEMU 中启动了 Linux 内核;其系统调用 Fuzzing 的行为特征与真实硬件及人工编写的 QEMU 模型几乎完全一致。 |
| BeagleBoard-xM | ARM | 成功推断出足够的外设行为,一路执行到了第二阶段 Bootloader 的入口。(注:后续崩溃是因为重放的串口数据不是有效的可执行代码)。 |
| CMU-900 | i386 | 成功启动了航电设备的 RTOS 系统,再现了接近人工逆向模拟器的行为,并借此发现了一个真实的提权漏洞(Privilege Escalation)。 |
| SEL-751 | ColdFire | 成功处理了极其复杂的 FPGA 相关检查并启动了固件,证明了符号执行在处理复杂约束上远超基于 Fuzzing 的方法。 |
对比竞品 P²IM
与之前知名的基于 Fuzzing 的重托管工作 P²IM 相比:
- Jetset 成功重托管了 P²IM 公开基准测试中的全部 9 个目标。
- Jetset 的引导式符号执行在面对更大、更复杂的固件时,扩展性显著更强。
- P²IM 的实现甚至无法运行原作者提供的那些较大的 Cortex-A 目标固件,而 Jetset 做到了。
结论与局限性
Jetset 的成功证明了四个关键论点:
- 轻量级的外设重放机制出人意料地有效。
- 引导式符号执行具备良好的可扩展性。
- “纯固件(Firmware-Only)”的重托管是完全可行的。
- 合成模拟器确实能赋能真实世界的安全研究(发现了 0-day 漏洞)。
当然,任何技术都不是完美的,Jetset 目前也存在一些局限性:
- 路径正确性问题:推断出的路径可能在逻辑上可以通过,但在真实物理世界中并不合理。
- 有限的外设模型:队列式的返回值模型虽然有效,但在处理某些极其复杂的交互时可能依然不够。
- 不支持 DMA(直接内存访问):这是目前静态/符号执行分析固件时普遍面临的难题。
总的来说,Jetset 为嵌入式固件的安全分析提供了一条极其高效的自动化新路径。它告诉我们:在安全分析中,“够用就好”的实用主义往往能爆发出巨大的威力。

