前言
P2IM在固件MMIO建模方面属于较为开创性的工作,作者基于自己对于嵌入式固件的观察提出了针对寄存器行为模式进行建模的实现思路。本篇文章需要关注的一些关键点:
- 外设接口等效属性(P2IE:Processor-Peripheral Interface Equivalence) : 文章提出的一种属性,是为了定义怎样的仿真才是一种好的仿真
- 抽象模型的定义:四类寄存器抽象模型,对寄存器在运行过程中的行为模型进行了总结
- 探索性执行技术:探索性执行的目的是为了找到一个合适的SR(状态寄存器)值,这对于固件的正常执行十分重要
- 数据集:P2IM提供了一组检验框架仿真能力的数据集,在之后的µEmu以及Fuzzware中都在此数据集上进行了实验作为对比
- 局限性:作为较早的建模方案,P2IM自身存在许多局限性
出发点
嵌入式固件通常是一个软件整体,包含外围设备驱动程序、微型操作系统或系统库,以及一组专用逻辑或应用程序,文章注意到由于硬件限制, MCU 供应商很少使用 Linux 等通用操作系统来构建 MCU 固件,而是倾向于为 MCU 设计专用操作系统,或者只是使用精简的系统库来代替独立操作系统(裸机固件)。
以往针对这类裸机固件的仿真方案存在一些问题,文章将其总结为两个硬件挑战和两个软件挑战。
硬件挑战
-
硬件依赖性 —— 针对硬件在环方案
- 固件与真实外设的I/O将引入很大的时间成本
- 直接影响的Fuzz的并行测试,因为每条并行线都需要准备一套真实外设,不可能买这么多设备
-
外设多样性 —— 针对完全仿真方案
- 海量类型的外设导致去手工定制这些外设的仿真是不现实的
注意:这里指的完全仿真是指手动实现对于固件需要外设的仿真器模块,有些厂商会开发自己的仿真器以方便调试使用,但是不具备通用性
软件挑战
- 固件OS的多样性 —— 针对一些基于OS的测试方案
- 固件操作系统的类型十分多样,并且存在大量没有操作系统的裸机固件
- 一些研究针对unix-based的固件或系统调用进行模糊测试实际上忽略了固件系统的异构特质,不具备通用性
- 合适的模糊测试接口
- 传统PC软件的输入接口定义明确且统一(例如通过文件I/O或者标准输入输出接口交互),模糊测试接口较易识别
- 固件尤其是裸机固件很少有可视的界面跟人直接交互,一般通过外设来获取自己需要的输入,这时怎样找到合适的模糊测试接口也是一项挑战
外设接口
-
固件与外设交互的接口
- 内存映射I/O:通过访存实现配置、控制、传输
- 中断:一种异步交互方式
- DMA:直接内存访问,P2IM认为固件一般只跟外设交换少量的数据,不常使用,因此回避了对DMA的建模
实际上P2IM的团队在次年就发表了针对DMA建模的研究工作DICE(IEEE’21)
-
外设接口等效性属性(P2IE:Processor-Peripheral Interface Equivalence)
P2IM定义的一个衡量仿真效果的属性,满足P2IE意味着:
- 仿真器仿真处理器或外围接口,而不是固件使用的外围设备本身
- 仿真接口在对固件执行的影响方面与固件预期的外围设备等效
简单理解就是仿真的外设跟真的一样,都能让固件正常工作
-
经验测试
P2IM提出了一个经验测试,仿真过程中只要不出现以下情况就说明仿真成功:
- 崩溃:因为仿真器的错误操作(访问非法内存等),固件直接崩溃无法运行
- 停滞:固件一直等待外围设备状态改变但仿真器无法识别和处理它时,可能会发生暂停
- 跳过:与暂停相类似的情况下,(在暂停了很长时间之后)固件可能最终放弃等待和跳过操作,导致部分固件代码无法访问
关于暂停和跳过的一个简单例子:在从ADC(模数转换器)之类的外围设备读取数据之前,固件需要等待设置MMIO寄存器位,来通知固件数据已准备就绪。如果仿真器未能设置这些位,固件将暂停,而不会显示任何错误迹象。另外有可能在长时间等待之后,固件逻辑会选择简单地跳过操作,导致一部分功能代码没法访问到
架构概述
P2IM的工作流主要包括三个阶段:
- 抽象模型定义:根据经验设计一组抽象内存模型 -> 定义了一个Java类
- 模型实例化:根据固件运行时信息将抽象模型实例化 -> 用具体数据将类实例化成Java对象
- 具体模型运行:拿到具体模型后开始执行固件进行模糊测试 -> 使用这个Java对象提供的功能
抽象模型定义
定义抽象模型是P2IM唯一需要手动工作的地方:
- 抽象模型是研究人员按经验定义的内存模型
- 抽象模型需要能够正确的处理固件对内存的访问,并且提供正确的响应值
- P2IM的抽象模型主要包括访问模式(怎样识别一个寄存器的类型?)和处理策略(当固件访问外设寄存器时应该怎样响应?)
- 文章定义了四类寄存器粒度的抽象模型CR、SR、DR、C&SR
- 注意以下四种抽象模型的定义都是根据作者的经验,将研究过程中发现的外设寄存器在工作过程中的行为模式抽象概括成了一个行为模型
-
控制寄存器 CR
- 访问模式:RMW模式。固件首先读取 CR,然后修改其中的配置参数,最后将值写回寄存器
- 处理策略:返回之前存入的值(之前存的啥就返回啥)
- 误分类:固件可能直接写入CR而不遵守RMW模式,这会导致CR被误分类为DR
文章认为CR误分类为DR影响不太大,因为控制寄存器的值一般是一次性的配置,在之后的运行过程中不经常被修改。当然这是对问题的简化,这里的处理可能会导致固件仿真失败
-
状态寄存器 SR
- 访问模式:
- 第一种:第一次访问时无条件读取。如果对于寄存器的第一次访问是无条件读取,并在之后的条件中使用了这个值做判断,将此寄存器识别为一个SR
- 第二种:轮询模式。固件会在一段时间内尝试连续读取SR值,轮询通常是为了等待SR给固件提供一个合适的状态值
- 处理策略:
- 读取:采用探索性执行推断出一个固件能够接受的寄存器值
- 写入:固件写入SR不影响执行,直接丢弃即可
- SR的特殊性:SR对于固件运行至关重要,在许多情况下,如果没有设置好必要的 SR 标志,固件将停止启动或无限停止;另一方面,设置错误的 SR 位会导致固件崩溃。因此对于SR的处理必须十分谨慎,P2IM专门为SR设计了探索性执行策略来推断SR值。
- 误分类:固件的第一次访问可能不是读取操作,这将会导致SR被误分类为DR
对于这种误分类,P2IM可能会通过轮询模式对寄存器的类型进行纠正,具体来说就是当系统发现固件连续读取SR的值时,P2IM会将寄存器的类别纠正为SR
- 访问模式:
-
数据寄存器 DR
- 访问模式:
- 第一种:检查了关联的SR后再读取DR
- 第二种:直写,直接写入DR不检查任何SR
- 处理策略:
- 读取:理想的模糊测试接口 (之后的工作都将DR作为模糊测试的接口)
- 写入:直接丢弃,不影响固件的执行
- 理想的模糊测试接口:
- 数据寄存器将给固件提供需要的数据输入,类似通用OS中的标准输入等,DR可以作为Fuzz的接口提供模糊输入给固件
- DR可以结合很多动态分析手段(模糊测试、污点分析等)
- 访问模式:
-
混合寄存器 C&SR
混合寄存器的位分两部分,一部分是控制位(相当于CR),另一部分是状态位(相当于SR)。这样设计可以提高寄存器位的利用率,但是混合寄存器也将导致外设的固件的设计变得更加复杂,有利有弊。对于现代MCU来说,他们有足够的内存地址空间,P2IM认为只在少许特殊情况下会使用这种设计。
- 访问模式:
- CR位在外设配置阶段以RMW模式被访问
- SR位在外设操作阶段按SR模式被访问
- 处理策略:
- P2IM认为混合寄存器的CR与SR位在使用时间和分布空间上不重叠
- 在访问CR位时先被识别为控制寄存器,随后在外设操作阶段访问SR位时被纠正为状态寄存器
- 对于SR位的访问需要使用到探索性执行
- 访问模式:
中断触发
-
中断仿真
-
什么是中断?
- 中断本质上也是给固件的输入(异步输入,在任何时候都可能到来)
- 中断将某些硬件事件通知给固件并触发相应的中断服务程序ISR
-
为什么要进行中断仿真?
-
中断例程也属于固件代码的一部分,其中也可能有漏洞,触发ISR能够提高模糊测试的代码覆盖率,发现更多bug
-
中断有时会给固件提供一些必要信息:例如外设使用中断向固件发出“数据已经准备好了”这样的信号,然后才能调用相应的ISR从DR中读取输入数据。
如果不能提供正确的中断可能会导致固件进入错误的执行状态
-
-
-
中断建模
- P2IM将中断抽象建模成一系列基于时序的输入,每个输入对应一个启用的中断。当这样的输入进来时,仿真器生成匹配的中断并将其分派给固件
- P2IM 允许根据不同的模糊测试策略(例如纯随机生成、种子突变等)定制中断的顺序和时间
- P2IM使用一个简单的中断触发策略:启用的中断以固定间隔以循环方式触发(例如,在每执行 1,000 个基本块之后)
- 使用基本块计数(而不是绝对时间,例如使用时钟跳数)来测量中断间隔的好处在于允许中断序列的确定性重放,从而产生可重现的模糊测试/测试结果。
假阳性问题
- 仿真过程中的假阳性:
- 定义:仿真中的假阳性是指仿真过程中的输入触发了固件崩溃,但是真实环境中这种可能不会存在这种输入
- 中断假阳性:外设在运行过程中只会以特定的模式触发中断,但是仿真过程中是随机触发的,有些中断时序在真实场景中不存在
- 输入假阳性:模糊测试在庞大的搜索空间中探索可能触发固件bug的值,但是实际上有些值在真实环境中不会产生,对应的bug也不能复现
- 价值
- 作为通用固件测试框架就是为了发现更多的bug,因此假阳性输入应该被保留
- 假阳性问题意味着在真实环境下不会产生,但是在一些特定的条件下,例如受到攻击、遭到控制,可能会产生一些意外的输入和中断时序。
- 假阳性标识出了脆弱的代码片段,这些代码受到攻击的可能性仍然很大
模型实例化
-
抽象模型实例化
-
实例化过程是全自动化的
-
实例化过程是按需进行的:模糊测试过程在遇到未建模或未处理的外设访问时就会调用模型实例化过程
-
实例化会将固件特定的信息添加到抽象模型中,这些信息包括
- 外设寄存器的类型以及他们在内存中的位置
- 各自的访问处理策略
- 开启的中断以及中断触发策略
实例化模型不需要任何关于外设的先验知识,是自动化的过程
-
-
具体过程
- 指令执行
- 根据访问类型执行操作:
- 中断配置:更新打开的中断列表
- 外设寄存器访问:
- 判断是否需要对寄存器类别进行识别或纠正
- 对于SR的处理需要启动探索性执行,其他寄存器按照自身处理策略处理即可
- 其他访问:判断模型是否稳定,稳定后就会结束一轮实例化过程
- 检查是否需要执行中断
- 继续执行
-
访问处理策略
- CR和DR的响应比较好处理,直接根据抽象模型的定义处理即可
- 对于SR以及C&SR的控制位的处理将复杂很多
对于CR和DR来说,他们的响应规则很简单,所有DR和CR都使用抽象模型中定义的规则处理即可。但是SR的处理策略将会因为不同的SR以及同一SR在不同的代码位置被访问发生改变,
探索性执行
-
思路:
- 当固件执行遇到新的 SR 访问点时,P2IM 暂停执行并进行状态快照
- 生成多个并行工作线程,并发搜索得到一个 SR 的最佳值
- 将最佳值返回给固件并恢复原始固件执行
-
存在的困难:
Q1:搜索空间怎样设计?
Q2:什么时候停止搜索?
Q3:怎样定义最优值?
Q4:怎样减少探索性执行的频率?
-
Q1 搜索空间构造
- 经验性结论:SR中的位/标志通常是独立的并且一次只检查一个标志位
- 搜索空间:32 + 1个候选值,包括32个one-hot值和一个全零值
- 33个线程并行执行,在结束时选择一个优胜者作为最佳候选值
-
Q2 终止条件设计
- 问题:结束太早可能还没用上SR的值,结束太晚引入代价太大并且可能导致固件执行停滞
- 终止条件:在工作线程将返回下一级被调用者时终止,这样做是由于
- 经验:固件通常在同一函数中读取并判断是否要继续进一步的I/O操作
- 实验测试:这种终止条件运行良好,开销较低。多数线程在到达这个终止点之前就退出了(因为SR值分配的不合理)
-
Q3 最优值的选取
- 选取的标准:
- 线程没有崩溃或停止
- 如果所有线程都崩溃或停止,选择崩溃不是因为SR值错误设置而引起的线程
- 当找到多个同样好的SR值时,P2IM将随机选择一个作为最佳值
- 选取的标准:
-
Q4 减少探索性执行的频率
-
问题:固件执行时,每次访问SR都会导致探索性执行
-
SR分组:
- 对于同一个位置的SR访问,可以使用相同的处理策略
- 通过SR分组,类似的SR访问可以重用相同的处理策略,这样随着P2IM实例化过程的进行可以逐渐减少探索性执行的频率
-
位置标识:P2IM使用四元组(r, cs, bbl, conf)标识一个访问位置
- r:状态寄存器SR
- cs:SR访问时的函数调用栈签名
- bbl:SR访问时的基本块ID
- conf:根据CR值简单生成的外设配置哈希
使用conf是因为P2IM考虑了可能存在的SR和CR之间的关联:例如固件仅在通过CR启用接收器时才会检查USART的数据接收标志
-
实现
- 使用QEMU作为基本仿真器:添加了2,202 行 C 代码(主要用于动态固件执行检测)
- 使用AFL作为基本模糊器:173 行用于模糊器集成的 C 代码,它没有内置支持或对 MCU 固件的先验知识
- 1,199 行 Python 代码用于实现P2IM 探索性执行部分
- 中断识别和触发逻辑是基于 QEMU 的虚拟中断控制器 (NVIC) 实现的
- 借助TriforceAFL实现模糊器集成,通过DR访问将模糊器产生的输入引导至固件执行
P2IM的代码结构是比较简单的,在针对QEMU的代码拓展部分是代码设计的主要难点,这一部分需要对于QEMU源码有比较深入的理解
评估
测试集
-
P2IM设计了一组测试集,这组测试集也在之后的全仿真工作中被广泛使用
-
测试对象
- 外设访问操作 —— 对通用外设支持
- 不同的MCU Soc —— 适用于广泛的MCU
- 多种OS和系统库 —— 具备操作系统无关性
-
样本
-
样本包括70个不同的示例固件或测试用例,每个都代表了外设、OS和微控制器的独特组合
-
固件启动后,将简单的执行定义在表中的基本外设操作
-
-
结果
-
外设寄存器访问次数统计
- P2IM收集了测试期间访问的外设和寄存器的统计数据,结果表明单个外设操作通常会导致对不同种类的相关或依赖外设的多次访问。例如,在 GPIO 测试期间访问的外设的最小数量是 3,I2C 的最大数量是 15。
- 此外,多个寄存器访问与单个外设操作相关联。例如,GPIO 操作涉及的寄存器的最小数量为 9,ADC 的最大数量为 68。这些统计数据表明,即使是简单的外设操作也可能涉及其他外设和许多寄存器的复杂链,突出了 P2IM 的价值以及自动建模和处理 MCU 外设的需要。
-
寄存器识别准确率
- P2IM测量了寄存器识别和分类的准确性。首先实验从 MCU 数据表中手动提取了实际寄存器类型,然后将P2IM的寄存器分类输出与实际类型进行比较。图 5 c) 显示了外设汇总的结果,范围从 76% 到 92%(即,24% 到 8% 的已识别寄存器被错误分类)。
- P2IM在这些外设的仿真的表现上比较的均衡,没有表现得特别好或特别差的。这表明 P2IM 的准确性在不同类型的外围设备之间没有太大差异,系统能够在忽略外设类型或内部结构的情况下进行仿真。
-
P2IM的仿真率
- P2IM在单元测试上的仿真结果是 79%(55)的测试通过(即满足 P2IE),而 21%(15)失败
- 两个失败的主要原因:
- 外设驱动程序未能遵循正确/通用的寄存器访问模式时,可能会发生寄存器错误分类
- 一些外设多路复用单个中断,这会导致 P 2 IM 触发不正确的中断
-
总的来说,实验表明 P2IM 在大量示例固件上工作得相当好。它允许大多数固件在不支持 MCU 外围设备的仿真器上执行,而不会出现任何崩溃、停顿或操作跳过。此外,P2IM被证明与固件的目标MCU架构以及操作系统无关。
-
真实固件
-
复杂的真实固件样本
-
P2IM选择了 10 个用于不同用途的真实 MCU 设备固件,从无人机到工业控制系统。它们是成熟的固件,包含所有常见的固件组件,包括内核(例如,调度程序、中断处理程序、系统库)、驱动程序、控制台、应用程序逻辑等。它们共同涵盖 4 个 MCU 模型(按照收入排名来自 3 个顶级 MCU 供应商]),4 个操作系统和一组不同的外围设备。此外,这些固件中使用的底层 SoC 通常用于其他嵌入式或物联网设备。
-
这些真实固件样本涵盖的类型包括:
- 自平衡机器人
- PLC(可编程逻辑控制器)
- 网关(Gateway)
- 无人机设备
- CNC(Grbl 铣削控制器)
- 回焊炉
- 控制台
- 转向器(智能汽车的自动驾驶)
- 烙铁
- 热压机
-
-
实验结果
- 寄存器分类精度比测试集还高
真实固件驱动程序的设计往往会更加规范
- 模型实例化速度较快(十分钟以内)
考虑到典型的模糊测试会话通常持续数天或更长时间,消耗的时间(在最坏的情况下为 10 分钟)是可以接受的。
局限性
寄存器误分类
- 在针对 10 个真实固件进行测试时,作者手动检查了所有被 P2IM 错误分类的寄存器。我们逐条列出了它们对固件执行的潜在影响,并在表 4 中列出了每种影响的错误分类寄存器的数量。在表 4 中,最后一列显示固件已读取的寄存器总数。中间两列显示了 P2IM 为每个固件错误分类的寄存器数量。错误分类的寄存器根据它们对固件模糊测试的负面影响(即减慢模糊测试Type1或减少覆盖范围Type2)分为两类。
-
TYPE I—— SR误分类为DR
- 影响:减缓模糊测试过程
- 误分类前是通过探索性执行来推断合适的SR值,速度较快
- 被误分类为DR后将使用模糊测试来推断SR的值,搜索空间大,速度慢
-
原因:某些外设驱动没有遵循P2IM定义的寄存器访问模式
有些情况下,这种访问方式是合理的,比如固件为了清除潜在的外设错误,在读取 SR(以检查外设状态)之前先写入 SR,由于DR存在先写访问模式,这将会导致SR被错误分类为DR
- 影响:减缓模糊测试过程
-
TYPE II —— DR误分类为CR
-
影响:
1.覆盖不了一些代码路径
有些依赖于DR的代码路径到达不了了,因为CR的值是固定的不受模糊器控制
2.导致仿真的失败
由于CR值的固定导致了可能产生了错误的外设输入给固件,导致固件的崩溃(正常情况下模糊器可以通过变异找到一个合适的DR值)
-
原因:与第一种情况类似,也是P2IM提供的访问模式失效了,外设驱动并没有按照这种行为模式工作
在一些情况下,这种访问模式是合理的,比如某些 GPIO 外设通过一个 DR 公开多个引脚。要将数据写入引脚,驱动程序必须遵循 RMW 模式以避免覆盖其他引脚,这会导致DR被错误分类为 CR。这体现出P2IM在寄存器分类方法上的局限性。
-
代码覆盖率
-
P2IM在复杂真实固件上的代码覆盖率
-
结论:覆盖率显著提升,但是还是不够高
-
造成这种情况的原因
-
存在僵尸代码:写了但是没有使用上的代码
-
模糊器太基础:只用了最简单的AFL
-
假挂起情况:出现了两次假挂起的情况,一次是由于DR被错误分类为了CR;另一次是出现了DMA操作,P2IM不予处理
-
输入保持:作者发现不仅是输入值,输入持续的时间也会影响固件逻辑的执行
例如,烙铁和回流焊炉固件不断从 GPIO 读取数据,同时执行由相同 GPIO 值/信号的持续时间决定的不同操作。现有的模糊器在不考虑输入持续时间的情况下生成输入,作者认为这种输入保持也是一种固件模糊测试的特有挑战。
-
崩溃检测机制:需要更成熟的错误检测方案
-
讨论
-
直接内存访问(DMA):P2IM 对处理器外围接口进行建模,包括寄存器和中断,但是本工作不模拟直接内存访问 (DMA)。DMA允许外围设备直接访问 RAM,然后为固件提供输入。缺乏 DMA 支持是P2IM的一个限制。由于 DMA 的复杂性和特定于外设的性质,可以说,如果不考虑内部外设设计,就不可能对 DMA 进行建模,这违背了 P2IM 的设计原则——通常适用于广泛的外设和 MCU 设备。尽管如此,DMA 的使用取决于各个固件的设计和架构。实验观察到,大多数研究和测试的 MCU 都支持 DMA,然而,在测试的 10 个真实固件中只有 1 个实际使用 DMA。
其实他们自己做了关于DMA建模的研究:DICE(21年发表在IEEE上)
-
模糊测试以外的固件分析:虽然P2IM的工作最初是受到固件模糊测试面临的开放挑战的启发,但它并不是专门为支持模糊测试而设计的。不需要固件完全准确输出的其他类型的动态固件分析可以使用本框架来实现硬件独立性和可扩展性。例如,数据或代码可达性分析,如污点分析和某些调试任务,也可以使用P2IM。特别是,concolic 固件执行可以使用P2IM来生成更真实的具体输入(即,非崩溃/停顿),减少符号值的数量,并避免一些不可行的代码路径。
可以拓展符号执行、污点分析等技术
-
ARM以外的架构:本文分析了 3 种使用非 ARM 架构的物联网设备 MCU包括Tmega328P (A VR)、PIC32MX440F256H (MIPS) 和 FE310-G000 (RISC-V)。分析表明,P2IM的设计和抽象模型并不特定于 ARM 架构。它们可以扩展以支持其他架构,例如 AVR、MIPS 和 RISC-V。所有这些非 ARM 体系结构都为类似于 ARM 的外围设备定义了特定的内存映射区域。它们还遵循 P2IM 识别的类似寄存器类别(CR、SR、DR 和 C&SR)。此外,在这些非 ARM 架构上配置和操作外围设备的过程遵循 P2IM 用于识别和处理外围 I/O 的相同约定和模式。
可以用于广泛的架构