Forming Faster Firmware Fuzzers(SAFIREFUZZ)论文中文版(带一些基础解释)
作者:
Lukas Seidel——Qwiet AI
Dominik Maier——TU Berlin(柏林工业大学)
Marius Muench——VU Amsterdam and University of Birmingham
本文收录在第32届USENIX安全研讨会论文集中。
前言
本文提出了一个模糊测试工具——SAFIREFUZZ,该工具主要是针对模糊测试的底层进行了改进,在rehosting(重托管)基础上提出了near-native rehosting(近原生化的重托管),以此提升模糊测试效率。值得注意的是,该工具专为二进制ARM架构的嵌入式固件设计,并且该工具只能运行在armhf(即32位arm架构)系统上,所以复现起来相对比较困难(我复现了两周还没有复现出来,如果有小伙伴复现出来了希望可以指导一下我)。
预备知识
考虑到会有很多刚刚接触这个领域的人想要看懂这篇论文(比如我),所以这里我会提前向大家介绍一些知识点,在文章中会有我自己的一些注释,可能和预备知识重复也可能对预备知识有补充(是我自己在读的时候做的,懒得一个一个删了),希望这些可以更好的帮助大家读懂这篇文章。
知识点
rehosting(重托管): rehosting 是指在虚拟化或模拟环境中运行嵌入式系统的固件,而非依赖其原始硬件平台的技术。它的目标是使固件能够在不同于其设计环境的系统上运行,同时保持其功能行为的完整性。
——Rehosting我认为和我们通常所说的仿真很像,但是他们的实现方式和目标不一样,Rehosting:通过硬件抽象层(HAL)或高层仿真(HLE),将目标固件中的硬件依赖部分映射到宿主系统的类似功能。Emulation:使用低层仿真(如QEMU)逐条翻译目标架构的指令,并严格模拟硬件的寄存器、外设行为,确保每一步执行结果与原硬件一致。
覆盖信息:在模糊测试中,覆盖信息是指程序在执行时所经过的代码路径或代码块的信息。具体来说,覆盖信息告诉我们程序在运行某个输入时,哪些代码路径被执行了,哪些没有执行。
——覆盖信息对于模糊测试非常重要,因为它能帮助模糊测试工具了解哪些部分的程序已经被测试过,哪些还没有被触及,从而决定下一步的测试策略。
模糊测试:模糊测试是一种自动化的测试技术,通过向目标程序输入大量随机或畸形的数据来发现潜在的漏洞。例如AFL++是一个非常流行的模糊测试工具,专门用于自动化地发现安全漏洞。它通过不断修改输入数据并监控程序的行为(如崩溃或异常)来工作。
——当某个输入产生新的、独特的覆盖信息时,它会被添加到模糊测试语料库中,并随后进行变异以生成新的测试用例,以便进行进一步的模糊测试。
插桩(instrumentation):是一种在程序运行之前或运行过程中,向代码中添加额外逻辑(代码片段)的技术。插桩的目的是对程序的行为进行监控、分析或修改。插桩通常是在程序的原始代码中插入附加代码,这些代码不会影响程序的核心功能,而是用于收集信息或修改程序运行的方式。例如:监控程序的执行流、记录运行时信息(如函数调用次数、执行时间等)、捕获错误或异常、进行安全性检查
hook或hooking:指的是在程序中插入自定义代码的过程,以便在特定的操作发生时能够捕捉并修改它们的行为。它是一个常见的技术,广泛用于调试、监控、篡改、增强功能或进行安全分析。作用包括:拦截函数调用、插入自定义代码、修改执行流程等。
Basic Block(基本块):是程序代码中一组具有以下特性的连续指令集合:只有一个入口点:控制流只能从基本块的第一个指令进入,不能从其他指令跳入。只有一个出口点:控制流只能从基本块的最后一条指令退出,而不会在中间被跳转到其他地方。中间的指令都是顺序执行的:从基本块的第一条指令到最后一条指令,控制流不会中断。
Rust:Rust 是一种现代的系统编程语言,旨在提供高性能、安全性和并发性,特别适用于需要控制内存和硬件的低级编程。
文章正文
摘 要
近年来,用于评估嵌入式系统固件安全性的一个趋势是重托管[注释1] ,即在虚拟化环境中运行固件,而不是在原始硬件平台上运行。固件重托管的一个重要应用场景是模糊测试,用于动态发现安全漏洞。然而,现有的前沿实现存在由模拟器引起的高开销问题,导致执行速度低于理想水平。为此,我们提出了一种近原生的重托管方法:在与目标设备共享指令集家族的高性能系统上,将嵌入式固件作为Linux用户态进程运行。我们通过SAFIREFUZZ实现了这一方法,这是一种针对ARM Cortex-M固件进行吞吐量优化的重托管与模糊测试框架。SAFIREFUZZ能够处理仅包含二进制的单片固件镜像,并通过高级仿真(HLE)和动态二进制重写,将其以低开销运行在性能更强大的硬件上。通过复现HALucinator(一种基于HLE的最新二进制固件重托管系统)的实验,我们证明SAFIREFUZZ在24小时模糊测试期间,平均可以提供690倍的吞吐量提升,同时覆盖最多多达30%的基本块[注释2] 。
引言
嵌入式系统已变得无处不在。这些专用计算设备被广泛应用于日常生活的各个领域,例如汽车系统、网络设备、医疗设备、智能家居设备等。随着它们的互联性不断增强,保护其机密性、完整性和可用性变得愈发重要。与传统计算机不同,嵌入式设备通常不运行完整的操作系统,而是使用一个单片的软件栈来处理系统的方方面面:内存管理、中断处理、用户数据处理和硬件交互。这种被称为设备固件的东西往往向外部来源(可能由攻击者控制)暴露了许多复杂功能,例如驱动程序和自定义解析器,这些来源包括无线传输数据或通过TCP包传入的数据。鉴于嵌入式系统的重要性和广泛的攻击面,对其固件的安全性进行测试至关重要。
一种常见的安全性分析方法是模糊测试。这是一种自动化过程,模糊测试工具通过提供半随机或潜在畸形的输入来触发目标程序的边界条件漏洞。由于其有效性,该领域已成为一个日益受关注的研究课题。近年来,重托管(rehosting)技术得到了广泛应用,它通过创建虚拟执行环境使嵌入式设备的固件能够进行模糊测试。现代模糊测试工具通过利用目标程序最后一次执行的反馈信息来选择进一步变异的输入,从而生成能够被目标接受的输入。随着输入逐步覆盖目标程序的更多代码,每次执行的吞吐量自然会下降。由于每个测试用例在目标上运行的时间变长,模糊测试工具每秒能够执行的测试用例数量减少,从而减缓了状态空间的进一步探索。因此,提高执行速度成为增强模糊测试效率的重要方向之一。在相同时间内用更少时间运行相同输入,可以为模糊测试工具节省时间,进一步探索目标程序。
在本文中,我们提出了SAFIREFUZZ,一种新的高效重托管和模糊测试方法,专为仅含二进制代码的ARM嵌入式固件设计。不同于以往工作中使用通用模拟器进行固件重托管的方法,我们采用了一种称为近原生重托管的技术。我们的方法核心在于利用强大的服务器或桌面级ARM计算设备,这些设备的执行模式和指令集与嵌入式系统足够相似。基于这一观察,我们构建了一个动态二进制重写引擎,能够在更强大的系统上直接运行固件,同时动态插入模糊测试所需的检测代码。为了处理硬件交互,我们采用了由HALucinator提出的高级仿真(HLE)方法,用基于硬件抽象层(HAL)的钩子[注释3] 替换硬件交互。正如我们所展示的,与现有的重托管框架相比,我们的方法显著提高了执行速度,从而提升了模糊测试效率,并发现了此前未检测到的漏洞。
特别是,我们对SAFIREFUZZ与两种近期重托管方法进行了比较:一种是基于高级仿真的HALucinator方法,另一种是基于外设建模的Fuzzware方法。评估结果表明,与HALucinator相比,SAFIREFUZZ可实现高达690倍的速度提升,与Fuzzware相比则高达147倍。此外,在24小时模糊测试中,SAFIREFUZZ能够额外发现多达30%的基本块。
总结而言,我们的贡献包括:
我们提出了SAFIREFUZZ:一个高性能的近原生重托管框架,用于交互式运行嵌入式ARM固件。
我们证明了该框架在高效模糊测试ARMv7-M二进制固件镜像中的适用性。为此,我们将进程内模糊测试与动态二进制重写技术紧密结合,并引入硬件抽象层(HAL)函数钩取。
我们通过为HALucinator测试集中的12个固件样本实现模糊测试框架,对SAFIREFUZZ进行了评估,并与最近的重托管方法进行了性能比较。此外,我们从头开始重托管了两个新样本。评估结果表明,近原生重托管在性能上明显优于基于通用模拟器的重托管方法。
背景
嵌入式系统与固件
嵌入式系统的特点在于其使用固件来驱动设备的硬件,并提供更高层次的功能。为了与硬件进行交互,固件通常使用以下几种方式之一:
- 内存映射输入/输出(MMIO):将物理内存中的一段范围分配给每个外设。每个范围被划分为MMIO寄存器。通过访问这些寄存器,固件可以直接与外设交互,例如读取外部数据或打开LED灯。
- 端口映射输入/输出(PMIO):与MMIO类似,不同之处在于通过IO端口启用专门的指令进行交互。
- 直接内存访问(DMA):允许固件在外设与主内存之间传输数据时绕过CPU。与其让CPU在整个数据传输过程中阻塞,CPU只需要与专用的DMA控制器外设交互以启动过程,数据传输的其余部分由DMA控制器处理。
- 中断(Interrupts):外设通过中断指示特定事件的发生(例如新数据的到达)。根据设备的中断控制器的配置,固件随后会在指定的中断服务程序(ISR)中恢复执行。
ARM Cortex-A/M
ARM 是嵌入式系统中最流行的指令集架构家族之一 [12]。特别是,32位的 ARMv7-M 和 ARMv7-A 变种因其低成本和能效高而广泛使用。ARMv7-A 主要针对更复杂的嵌入式系统,具有两种不同的执行模式:
- ARM模式:处理器执行的指令大小固定为四个字节,且四字节对齐。
- Thumb模式:指令由两字节或四字节组成, resulting 两字节的对齐方式使代码能够更密集地打包,这对于资源受限的嵌入式系统来说是有利的 [3]。这两种模式可以通过目标位置的最不重要位进行切换,1表示在Thumb模式下执行,0表示在ARM模式下执行。
与此不同,ARMv7-M专门针对微控制器,仅实现了Thumb-v2指令集。更近的指令集家族ARMv8-A和ARMv9-A引入了AArch64扩展,提供了64位指令集。虽然实现这些家族的CPU通常用于移动和桌面设备,但该扩展支持在较低的异常级别(如EL1和EL2)上使用64位操作系统或虚拟机时执行32位模式。
模糊测试
模糊测试是一种流行的自动化漏洞发现方法,能够揭示多种不同的漏洞,包括但不限于内存损坏错误(如缓冲区溢出、双重释放、使用后释放)和逻辑错误(如整数溢出、无限循环,甚至竞争条件)。
在覆盖率引导模糊测试中,模糊测试工具使用执行反馈来确定有趣的输入。为此,模糊测试工具向目标程序添加插桩(instrumentation)[注释4] ,如果源代码可用,则在编译时插桩,否则会在二进制级别进行插桩。插桩会将覆盖信息[注释5] 反馈到模糊测试引擎,例如,通过跟踪已执行的分支生成位图。当某个输入产生新的、独特的覆盖时,它会被添加到模糊测试语料库中,并随后进行变异以生成新的测试用例。[注释6]
动机
Rehosting(固件虚拟执行环境的自动创建)结合了多种方法,以克服嵌入式系统中外设(可能未知)仿真相关的挑战 [24]。早期的研究依赖硬件环路(hardware-in-the-loop)仿真,将未知访问委派给物理设备 [35, 37, 48],但无硬件的rehosting逐渐成为嵌入式系统覆盖率引导模糊测试的实际标准 [20, 25, 31, 33, 40, 45, 46, 51]。
Scharnowski等人 [46] 将无硬件rehosting方法用于克服未知外设行为的方式分为三类:
(1) 高层次仿真(High-Level Emulation, HLE):通过钩取硬件抽象层(HAL)的库函数来消除固件中的硬件访问。
(2) 基于模式的MMIO建模:通过启发式方法分类MMIO寄存器,然后使用预定义模型响应访问请求。
(3) 基于符号执行的方法:通过符号执行实时解析值以推进固件执行。
此外,最近的研究 [52] 提出了基于规范的仿真,从数据手册和设备文档中提取MMIO外设模型。
我们在表1中调查了近期的rehosting系统,并发现这些方法均已用于实现固件的模糊测试。然而,除了需要源代码访问的Para-Rehosting [38] 外,所有调查的rehosting系统都依赖现有仿真器来虚拟化固件。我们推测,这种依赖可能限制了模糊测试的效率,因为现成的仿真器是通用工具,设计初衷并非针对模糊测试优化。
更快固件模糊测试的途径
深入分析表1揭示了另一个细节:所有调查的基于仿真的rehosting系统都直接扩展了QEMU或构建在QEMU基础上。这使得QEMU的仿真方法成为固件rehosting的事实标准。尽管QEMU提供了广泛的扩展性和多种ISA的支持,但仅依赖其仿真能力可能会对模糊测试的效果造成妥协。
尤其是,在对现有技术进行分析后,我们发现了以下几个普遍存在的性能瓶颈:
[R1] 二进制提升与重新编译(Binary Lifting & Recompilation):QEMU将客户机代码提升为其内部中间表示TinyCode,然后应用指令插桩,并即时(JIT[注释7] )将每个块编译为宿主机架构代码。尽管此方法支持多种指令集,但大多数rehosting工作仅针对ARM架构。因此,我们认为高吞吐量固件模糊测试应考虑直接二进制翻译或二进制重写等替代策略以提升性能。
[R2] 内存访问调度的高开销:单片固件通常驻留在单一的平坦地址空间。然而,QEMU是为部署MMU的更复杂系统[注释8] 开发的。为在所有系统上实现这种仿真,其(指QEMU)SoftMMU机制调度内存访问,这带来了显著的性能开销 [17]。如果能直接访问客户机内存而无需间接操作,将大大提升客户机执行速度,从而促进模糊测试。
[R3] 基本块缓存与链接:QEMU的核心性能优化之一是能够缓存已翻译的代码块,并将多个代码块的执行链接在一起。然而,这种优化在AFL-QEMU的早期版本中并未实现 [11]。尽管在AFL++ [26] 中被主线化,但我们发现许多rehosting方案仍基于旧版开发,缺乏此优化严重阻碍了模糊测试性能。
[R4] 缺乏内嵌模糊测试:迄今为止,rehosting方案的模糊测试引擎均运行在单独的进程中。然而,与将模糊器嵌入同一进程相比,这种做法导致了不必要的内核交互和上下文切换。
我们注意到一些瓶颈已被部分研究解决。例如,FirmWire [33] 部署了基本块缓存与链接优化,而Frankenstein [45] 使用了QEMU的用户模式,消除了SoftMMU的需求。然而,据我们所知,目前尚无研究系统性地解决所有这些瓶颈,并探索高性能固件模糊测试的可能性。
设计
概述
SAFIREFUZZ 是一种高效的固件rehosting 和模糊测试执行引擎,通过克服第3节提到的瓶颈实现了优化。其核心是我们提出的一种技术,称为近原生 rehosting。与通过提升[注释9] 和重新编译固件(R1)来仿真不同,我们利用某些 ARMv8-A 内核提供 AArch32 和 Thumb 指令集变体的用户空间兼容性这一事实。因此,我们可以通过二进制插桩直接在高性能内核(ARMv8-A内核)上执行大部分固件代码。
在用户空间中,我们镜像嵌入式设备的内存布局,这样重写指令无需额外逻辑来分派内存访问,绕过了(R2)。此外,我们的重写方法优化了已插桩代码块的缓存,从而最小化引擎开销(R3)。最后,我们将模糊测试逻辑嵌入到与引擎和重写固件相同的进程空间中,以减少与宿主操作系统的交互(R4)。接下来,我们将详细描述 SAFIREFUZZ 的核心引擎、动态重写方法及解决 rehosting 挑战的方案。
Rehosting 和重写引擎
SAFIREFUZZ 的引擎负责执行目标固件、处理 rehosting 相关问题、重写指令,并插入模糊测试的插桩代码。为处理未知硬件外设问题,我们参考了 Clements 等人 [20] 提出的高层次仿真(HLE)方法,因为hooking对应的 HAL 可以方便地嵌入我们的重写方法中。
引擎使用固件特定的测试框架,该框架在目标执行开始前初始化内存范围并注册 HAL hooks,并通过重写指定入口点的第一个基本块启动执行。引擎按需翻译基本块,同时添加模糊测试的插桩代码,必要时将执行转移到已注册的hooks,并部署中断近似机制。
虽然上述任务都很重要,但我们注意到,应尽量减少在引擎代码或操作系统中花费的时间。因此,引擎仅在基本块的直接前块首次执行时重写该基本块,并缓存已插桩的块。在一个块的初始重写和执行期间,可能需要多次跳转到引擎。但一旦一个基本块第一次完整执行,引擎会消除该块回跳到自身的所有不必要跳转。这样,后续块的重写和分支解析只需进行一次。插桩的开销对一次引擎运行是固定的。
基本块重写
最简单的动态重写方法是直接替换指令。虽然已有一些解决方案能够替换单一指令 [22],但我们认为在一般情况下,这种方法是不可行的,尤其是在 ARMv7-M 上,因为替换后的指令可能比原始指令更大。此外,我们还需要插入额外的指令来进行插桩和钩子操作。这会改变指令之间的相对位置。由于 ARM 上许多指令是基于 PC 相对地址操作的,且跳转目标无法保证被保留,因此在这种情况下,替换指令是不切实际的。尽管通过控制流恢复启发式算法 [44] 计算动态分支的跳转目标及所需的修改是可能的,但静态推断寄存器内容则是非平凡的。因此,我们选择在运行时动态地对二进制文件进行插桩。由于每个基本块只会被重写一次,因此这种方法的一次性开销可以忽略不计。我们在算法 1 中描述了重写过程,并在图 1 中进一步说明了基本块级别的重写过程。
我们近原生重托管方法的一个优点是,大多数指令根本不需要重写,因为在我们使用的 Cortex-A 核心上的 AArch32 执行状态与固件所构建的 Cortex-M 完全匹配。Cortex-M 处理器使用 Thumb-v2 指令,其中大多数可以在支持 AArch32 模式的 ARMv8-A 目标平台上原生执行而无需偏差。唯一需要重写的两类指令是修改 PC 的指令和 PC 相对内存访问指令。这与通常使用的基于处理器仿真的重托管技术形成对比,在这些技术中,一种架构的指令会在另一种架构上执行,并且所有指令都需要进行翻译。
函数钩取(Function hooking)[注释10]
当引擎重写一个新的基本块时,它会检查当前地址是否注册了一个钩子。用户可以提供通常是用高级编程语言编写的函数,引擎会发出一个跳转到用户提供的代码的指令,用户的代码将在基本块执行时执行。SAFIREFUZZ中的钩子位置被设计为仅限于函数钩子。由于所有我们修改的(寄存器)状态都在执行的固件和用户代码之间共享,因此在为新目标编写钩子时,按每条指令进行钩住会更加复杂。
此外,我们发现,在硬件抽象层(HLE)重托管中,函数钩子在所有情况下都足够用于替代硬件交互。这一限制使我们能够获得运行时性能,因为只要固件遵循常见的ARM调用约定,我们就可以在保存和恢复状态的过程中做出某些假设。我们的方法引入的开销是最小的:上下文保存和恢复,包括跳转到钩子,最多包括五条指令。这些钩子,也称为处理程序(handlers),旨在模拟固件某一部分的行为。它们替代固件执行过程中对硬件抽象层(HAL)函数的调用,从而屏蔽固件中的外设访问。这个过程是基于HLE的系统中外设管理的核心,而我们的近原生方法并没有引入固有的缺陷。常见的实现功能包括中断的模拟和处理、在模糊测试过程中接收外部数据并使其可供固件使用,或将内存分配器替换为一个消毒版内存分配器,以提高安全漏洞的可观察性。由于我们在方法中模拟了HAL上的函数,跨固件重用变得更加容易。使用相同系统库或为相同微控制器开发的固件镜像通常共享相同的硬件抽象。哪些固件函数被哪个处理程序钩住,由用户在测试环境(harness)中决定(见5.3节)。固件的测试环境可以看作是代码中的领域知识规范。它们高度依赖于特定的处理器或微控制器型号,并且必须处理固件的特性,从设置入口点到提供正确映射的内存。
中断近似
许多嵌入式设备依赖中断来传递信号和处理异步外部事件。例如,SysTick 定时器被部署在许多 ARM 微控制器中,单片实时操作系统(RTOS)利用其轮询 MMIO 寄存器并调度新任务。因此,我们的 rehosting 解决方案需要模拟中断以准确执行此类固件。
为此,我们实现了基于时钟的中断。尽管理论上可以翻译中断并让 Cortex-A 主系统处理它们,但嵌入式设备上的中断通常由外部外设触发,其中断服务例程(ISR)会通过 MMIO 访问它们,因此在任何情况下都需要重写。先前的 rehosting 工作表明,中断近似足以模拟固件行为 [25],并且它对模糊测试有利,我们的基于计时器的计数器提高了确定性,从而生成可复现和可分析的程序轨迹。
相比于 HALucinator,其通过基本块级计数器触发定时器,我们的方法使用间接调用级计数器和手动时钟更新钩子。在执行的特定点更新计时器并触发中断,而非钩取每个基本块,从而避免了不必要的性能开销。
实现
引擎内部
我们使用 Rust 编程语言实现了 SAFIREFUZZ。引擎的核心包含 1481 行源代码,另外 1716 行用于实现硬件抽象层(HAL)处理程序,而评估的 14 个目标的 harness 总共有 2360 行代码。反汇编是通过 Capstone 实现的,而通过 Keystone 来组装修改后的或新的指令。该引擎负责所有任务,以便在外部环境中执行固件。因此,它执行了许多仿真器需要处理的任务。
分支解析:当引擎重写一个以静态分支结尾的基本块时,它会发出一个回调到引擎。在第一次执行该基本块时,这个跳转(也叫慢路径)会被替换为一个静态分支,从而解析原始固件地址到重写基本块的新位置。由于这些分支目标计算只需要执行一次,删除跳转到引擎的步骤可以减少后续执行的开销。这个优化通过确保每个基本块只执行一次慢路径函数,并在后续跳过它们,大大提升了性能。如果指令以无法静态解析的方式直接修改 PC(如使用寄存器内容),引擎需要在运行时解析目标。对于这些指令,如 BX、BLX 和 MOV 指令,目标基本块的正确地址会在运行时解析。解析过程包括:如果目标地址位于重写代码范围内,则认为是尾调用,并将地址与 1 进行 OR 操作确保我们保持在 Thumb 模式,并返回这个地址。如果目标地址不在该范围内,则执行缓存查找。引擎会重写新的基本块(如果尚未缓存),并返回目标地址。
上下文切换:每次跳转回引擎时,都需要保存和恢复上下文。对于用户定义的钩子,操作非常简单:引擎会将 r1 到 r11 寄存器推入栈中,钩子执行完毕后再弹出。这种方式只引入了微不足道的开销,并且使得访问函数参数和返回值的方式更加自然,而不像 Unicorn 这样的仿真器需要使用 API 调用来读写寄存器内容。
内存访问:分支之后,PC 相对的内存访问是引擎在重写基本块时需要修改的第二大类指令。在 ARMv7-M 中,小块数据通常与加载它们的基本块共存,并使用 PC 相对的 LDR 指令加载。由于在重写基本块时很难推断数据段的大小,因此我们静态解析地址,并将 PC 相对的加载指令替换为绝对加载指令。
缓存:为了减少引擎内部时间,尤其是在热点路径执行期间,我们缓存了多个数据点。由于 Capstone 和 Keystone 反汇编和组装指令非常耗时,因此我们会尽可能重用已经处理过的代码块。例如,当我们将原始代码站点的分支替换为指向重写块的分支时,机器码指令会编码一个偏移量而不是绝对地址,这些指令具有很高的重用潜力。
处理器缓存维护:由于我们不仅仅一次性发出新的指令,还会修改已经发出的基本块,我们必须处理 ARMv7-A 核心的非统一缓存架构。这些核心有分开的指令和数据缓存,可能导致自修改代码时出现问题。在每次重写指令后(即覆盖已经执行过的指令),我们需要清空相应的缓存范围,以确保新的指令被正确加载。
模糊测试工具
SAFIREFUZZ 的一个核心组成部分是模糊测试工具。我们选择使用 LibAFL 提供的变异后端,并使用其磁盘上存储的语料库来存储输入队列和找到的目标。Harness 作为引擎的入口点,定义了模糊测试工具如何 与引擎交互。首先,它从 LibAFL 获取模糊输入,然后启动一次固件执行。
作为反馈机制,我们结合了覆盖图观察器(map observer),它跟踪覆盖图的状态,并在每次条件分支时更新。执行时间和超时信息也作为反馈机制的一部分。调度策略则偏向于小的测试用例。变异操作遵循 AFL 的“havoc”方法,包括位翻转、整数覆盖、块删除和块复制。我们的框架还提供了一个选项,允许指定一个模糊器用来变异新输入的令牌文件。AFL 中的令牌是特定领域的字节序列(如 HTML 或 XML 标签),有助于模糊测试工具生成有意义的输入。
Harness
SAFIREFUZZ 中的 Harness 在编译时提供,与框架的其余部分一致,都使用 Rust 编写。为了在保持可用性的同时实现灵活的配置,框架暴露了一套接口,Harness 开发人员需要实现这些接口,供引擎在重新定位过程中的各个部分调用。这些接口包括但不限于:初始化函数(仅在引擎初始化时调用,用于设置内存段并将固件镜像复制到正确的位置)和重置函数(用于处理内存恢复、重置计时器和自定义分配器)。
评估
在我们的评估中,我们旨在回答以下三个研究问题:
RQ1. SAFIREFUZZ 与当前的固件模糊测试技术相比如何?
RQ2. SAFIREFUZZ 的核心性能提升和现存障碍是什么?
RQ3. SAFIREFUZZ 能否识别出之前未知或未检测到的漏洞?
为回答 RQ1,我们从之前的固件安全性和重宿主研究中选择了 12 个固件目标【20,25,30,31,42,51】,并将 SAFIREFUZZ 的模糊测试效果与 HALucinator【6】和 Fuzzware【46】在不同配置下进行比较。基于这些结果,我们提供了对 SAFIREFUZZ 性能的详细分析(RQ2)。最后,为回答 RQ3,我们首先讨论 SAFIREFUZZ 在这 12 个固件样本中发现的未检测到的漏洞,然后将我们的方法应用于两个新目标。
除非另有说明,所有实验均在一台运行 64 位 Ubuntu 18.04 的 HoneyComb LX2 ARM 工作站上执行。此系统配备 16 个 ARM Cortex-A72 核心,主频最高可达 2 GHz,32 GB DDR4 内存(频率为 3200 MT/s),以及一块 128 GB 的 m.2 SSD。
实验设置
目标选择:我们根据其在先前研究中的普遍性选择目标,特别关注 HALucinator【20】,因为这是最新的基于 HAL 的二进制重宿主框架。所有选定的评估目标均为 Cortex-M 核心编译。SAFIREFUZZ 的具体挂载逻辑参照了 HALucinator 的钩挂和实现,以确保在提供相同输入时表现出语义上相同的行为。我们在表 2 中提供了目标的概述,并在附录 A.3 中详细说明了四个示例目标的硬件抽象层挂载函数。
模糊测试工具设置:我们将 SAFIREFUZZ 与 HALucinator【20】和 Fuzzware【46】进行比较,这两者分别是当前基于 HLE 和外设建模的模糊测试工具的代表。为了与 HALucinator 比较,我们使用 hal-fuzz,它是一个面向模糊测试的开源版本【6】。此版本基于 UnicornAFL【10】,并使用传统的 AFL【1】作为模糊测试后端。为探索更现代的模糊测试工具的影响并增加与 SAFIREFUZZ 的可比性,我们替换了基于遗留 AFL 的后端,改用 libAFL 作为变异引擎。我们应用了与 SAFIREFUZZ 相同的配置参数和变异策略,并将此设置称为 HALucinator libAFL。对于 Fuzzware,我们使用其基于 AFL++【26】的模糊测试后端(版本为 AFL++v3.14c)。不幸的是,我们在 ARM 平台上运行 Fuzzware 时遇到了复杂的错误,这严重限制了模糊测试性能。在与 Fuzzware 作者确认 ARM 主机不受支持后,我们转而在 x86 主机上运行 Fuzzware。具体而言,我们在一台 AMD EPYC 7662 服务器上运行 Ubuntu 18.04 虚拟机(配置为 64 核,196 GB 内存)。对于每个模糊测试工具和目标,我们进行了五次 24 小时的运行,每个模糊测试过程固定到一个指定核心上。
种子选择:除了 Fuzzware,我们为所有设置使用 HALucinator 提供的种子。对于 Fuzzware,我们使用空种子,这是由于模糊测试工具的输入语义不同:与基于 HAL 的抽象不同,Fuzzware 的输入编码了与不同 MMIO 寄存器的交互,无法使用已知的文件格式作为种子输入。
一致性:由于不同框架的实现差异,在基本块级别上无法保证完全一致的执行流。我们通过比较消息日志和退出地址,确保 HALucinator 和 SAFIREFUZZ 模拟系统的功能性行为相同。我们执行了 hal-fuzz 存储库提供的所有样本输入,并验证了 HALucinator 和 SAFIREFUZZ 的日志结果是否匹配。此外,我们随机选择了来自模糊测试队列的各种输入(包括崩溃输入),生成了这些输入的消息日志,并确保其跟踪和退出地址相同。
指标:我们用于比较的指标是:(1) 每秒执行次数和 (2) 基本块覆盖的总量。对于 (1),我们计算每次模糊测试运行的总执行次数,并除以 24 小时的时间预算。对于 (2),我们在各工具中重放测试用例,但对于 SAFIREFUZZ,为了更好地比较,我们在 hal-fuzz 中重放发现的测试用例。由于 Fuzzware 和 hal-fuzz 都基于 Unicorn,这使我们能够收集已翻译的块,然后进一步筛选出 Ghidra【5】的 SimpleBlockModel 定义的实际基本块。
对于 Fuzzware,我们在重放测试用例时忽略了 HAL 功能中其他框架挂载的子路径。这使我们能够识别出未被我们的基于 HLE 方法执行的基本块数量。
与现有技术的比较
表 3 概述了实验结果,图 3 则直观展示了随时间推移的覆盖率。以下内容讨论了 SAFIREFUZZ 相较 HALucinator 和 Fuzzware 在执行速度和覆盖的基本块方面的表现。关于发现崩溃的附加分析及与其他再托管工具的比较,请参考附录 A.1。
- SAFIREFUZZ 与 HALucinator
在除 P2IM PLC 固件以外的所有目标上,我们的框架与 HALucinator 相比显示出了显著的性能提升。对于执行速度和覆盖率的指标,Mann-Whitney U 检验显示除了 P2IM PLC 目标的覆盖率外,其余所有目标在统计上显著不同,p < 0.05。对于 P2IM PLC 目标,所有框架的模糊测试效率都较低:目标固件中存在极易触发的崩溃,且大部分输入会导致无限循环并因此超时。在相同环境下运行时,我们的框架在执行速度上实现了高达 1000 倍的提升。尽管如此,即使在最糟糕的情况下,我们的框架也在大多数目标上提供了改进。
- SAFIREFUZZ 与 HALucinator-libAFL
当用LibAFL取代HALucinator中的遗留AFL时,覆盖率显著提高,但在24小时运行期间,仍在几乎所有情况下被我的框架超越。Mann-Whitney U检验表明,除了ST PLC、WYCINWYC以及UDP Echo Client样本的覆盖率外,差异均具有统计学意义。在这些情况下,HALucinator-libAFL的总体覆盖率接近或优于SAFIREFUZZ。正如预期的那样,与HALucinator相比,HALucinator-libAFL的执行速度差异保持基本不变。然而,覆盖范围随时间的变化模式通常与SAFIREFUZZ类似,只是显著滞后。鉴于两个框架使用相同的模糊测试后端,这种结果并不令人惊讶。
- SAFIREFUZZ 与 Fuzzware
SAFIREFUZZ在执行速度方面优于Fuzzware,并且在大多数情况下发现了更多的基本块,WYCINWYC和P2IM Drone样本除外。在7个目标中,Fuzzware的表现明显逊色于其他框架。我们怀疑这与自动化水平的差异有关:我们使用了Fuzzware的genconfig功能来生成模糊测试桩(harness)。因此,自动生成的模糊测试桩可能在仿真初期便遇到阻碍,从而无法进入目标的主要逻辑阶段。而HALucinator和SAFIREFUZZ通过基于HLE的钩子(hooking)绕过了这些阻碍。
对于6LoWPAN接收器/发射器、ST PLC、WYCINWYC的覆盖范围,以及SAMR21的执行速度,本实验中呈现的结果在Mann-Whitney U检验中不具有统计学意义。对于6LoWPAN样本,使用Fuzzware进行的实验仅包括单次运行,但其发现的未覆盖基本块数量较高,甚至超过了SAFIREFUZZ。而在WYCINWYC的情况下,Fuzzware在24小时运行后期达到了与SAFIREFUZZ相似的覆盖范围。
在执行速度方面,Fuzzware在SAMR21样本中几乎没有覆盖到任何块,因此提前终止执行,导致吞吐量极高。然而,SAFIREFUZZ在发现基本块并深入探索目标的同时,也提供了类似的吞吐量。
在不同固件目标上的模糊测试结果显示,执行速度与发现的目标(如崩溃和超时)之间存在较强的相关性。对于每次崩溃或超时,目标进程需重启,导致模糊测试性能显著下降。
性能分析
使用我们的框架对不同固件目标进行模糊测试的结果显示,执行速度与发现的目标(即崩溃和超时)之间存在强相关性。例如,在对6LoWPAN接收器目标进行模糊测试时,四次运行的平均执行速度为每秒559次,平均发现18197个目标。异常值运行仅产生了3666个目标,执行速度为每秒1536次。对于所有测试目标,都可以观察到类似的结果,尽管输入长度和有效性等其他因素与路径复杂性相关,也会影响性能。
一旦模糊器找到第一个崩溃,吞吐量会急剧下降。每次目标进程崩溃或超时时,进程都必须重新启动,且引擎必须重新执行所有繁重的工作。基本块的分析与重写,尤其是(反)汇编指令,比在热路径中执行固件更为昂贵,在热路径中,大部分时间花费在目标上,而不是引擎中。为了估计重启带来的性能损失,我们使用WYCINWYC固件创建了一个微基准测试。在有效的XML输入上首次执行该固件需要1.06秒,十次运行的平均值。随后对相同输入的运行以每秒6100次重启的速度执行,即每次仅需要0.00016秒。虽然有多种方法可以解决频繁重启带来的高昂开销(例如,快照技术),但我们认为这并不是一个重大问题,因为现实中的模糊测试通常不会包含数百次崩溃。
使用我们的框架进行的模糊测试实验显示,探索的路径和执行的基本块,以及目标和执行次数的多样性相对较高。这可能是由于所用模糊器后端的非确定性所致。在目标中崩溃较少或没有崩溃时,分歧会减小。与其他框架相比,覆盖的基本块数量部分剧增的现象可以通过在24小时内测试输入的数量增加来解释。覆盖率的增加不是线性的,这也是预期中的结果,因为线性地发现更多程序的新部分需要指数级更多的执行次数【14】。这使得像SAFIREFUZZ所提议的增强模糊测试性能的新方法变得更加重要。
漏洞
在我们的实验过程中,我们收集了使用SAFIREFUZZ发现的目标。此外,我们为两个新的固件样本创建了测试工具。我们在表4中报告了最小化的崩溃,并在以下内容中强调了值得注意的崩溃。
WYCINWYC。该固件作为固件模糊测试的基准,评估模糊器在到达易受攻击的代码路径并更重要地通过引入人工漏洞来检测故障的能力。它在XML解析器中暴露了五个合成插入的内存损坏,每个对应不同类型的漏洞。SAFIREFUZZ找到了所有五种错误类型,证明了我们的框架能够检测各种类型的损坏。我们的替代分配器替换方案通过跟踪已使用和已释放的内存指针,使我们能够发现双重释放。通过利用保护页并依赖主机系统的MMU,我们可以更可靠地发现段错误甚至堆溢出。对于系统内模糊测试设置,识别此类内存损坏通常更加困难,因为许多嵌入式设备没有MMU,且可用的仿真器通常依赖于增加开销的SoftMMUs。
6LoWPAN接收器/发射器。我们重新发现了Contiki-NG [23]中原本由HALucinator发现的多个漏洞,这些漏洞嵌入在6LoWPAN接收器目标中。最显著的是数据子部分中的越界写入,可用于PC控制,从而实现远程代码执行(CVE-2019-8359)。其他漏洞包括6LoWPAN片段处理中的整数溢出,导致缓冲区溢出,进而访问未映射的内存,导致固件崩溃(CVE-2019-9183)。值得注意的是,与以前的工作相比,我们的方法还发现了一个在发射器上触发CVE-2019-8359的路径。这表明,SAFIREFUZZ能够找到先前的工作未能检测到的漏洞,而这些工作也曾对相同的固件进行过模糊测试(HALucinator [20],Fuzzware [46]和uEmu [51])。
JPEG解码器。为了测试SAFIREFUZZ是否能够在其他固件中发现漏洞,我们遵循HALucinator的方法并编译了一个示例应用程序。特别是,我们针对一个使用LibJPEG [8]来解码和可视化嵌入在设备插入SD卡上的图像的应用程序进行模糊测试。我们的模糊测试发现了两个以前未知的漏洞。第一个漏洞是由关键错误例程引起的段错误,该例程在开始解析损坏的输入图像后未能终止程序,而是静默地继续执行。随后,未进行任何检查来避免访问和解引用未初始化结构体中的指针。我们将第二个漏洞追溯到颜色转换函数中缺少的边界检查。输出缓冲区有硬编码的大小,但错误的例程使用解码图像的宽度来遍历扫描线并写入缓冲区,从而大大超过了栈上缓冲区的限制。
STM32 Sine。我们测试的第二个额外目标是电动机逆变器的开源固件 [9]。在我们的模糊测试实验中,我们探索了终端接口的一个重要部分,该接口解析并处理各种命令,通过CAN总线通信更改硬件内部参数。SAFIREFUZZ发现了与更新CAN配置中某些参数枚举相关的崩溃。接口ID从内存中检索并用作内存偏移量。破坏该值会导致任意内存写入。然而,在撰写本文时,我们尚未确认这次崩溃是否为真正的漏洞,因为其根本原因部分位于硬件配置中,这可能会被我们的HAL钩子错误报告。
讨论
初始运行的性能。我们的方法在模糊测试过程中非常快速,一旦所有代码块被动态翻译。然而,在初次执行过程中,大多数固件的基本代码块是未知的,因此模糊器会花费大量时间在引擎内进行重新组装和缓存。因此,初期的执行比后续执行慢几个数量级。由于每次重启都会重置 SAFIREFUZZ 的缓存,因此进一步的性能提升可以通过不在超时时退出,或者将缓存写入共享内存或磁盘来实现。当接收到 SIGALRM 信号时,处理方式可以是引擎仅向 LibAFL 报告超时退出代码,而不是重新启动整个进程,并手动重置执行状态。在崩溃时恢复,例如 SIGSEGV,类似的做法并不容易实现。因为运行可能已经破坏并腐蚀了我们的进程状态,固件执行发生在引擎的同一进程空间内。因此,任何未定义行为可能也会影响引擎的状态或代码。为了解决这个问题,可以像 qemuafl [26] 的实现一样,将缓存追踪到当前进程外部。转向完全静态重写是另一种选择,但并不一定能提高模糊测试性能:重组的时间仅仅被移动到开始阶段,假设我们使用相同的技术,最终生成的二进制文件不会更快。此外,我们会失去动态插装时可用的宝贵运行时信息。
快照技术。由于我们针对嵌入式固件,因此并未实现内存快照。近年来,这项技术在模糊测试中得到了越来越多的应用。通过此技术,可能在固件启动过程之后拍摄内存快照,这样引擎可以跳过该部分,并使用快速重置代替完整重启。对于我们的用例,在本研究过程中,所调查固件的启动例程只有几百条指令。由于这部分代码量非常小,我们决定,快照和内存重置的额外开销并不值得。然而,未来可能有兴趣进行评估,因为它可以实现有状态的模糊测试 [39],并可能在崩溃时实现更快的重置。
人工工作量。尽管我们的工具在将新目标适配到系统时,施加了与其他基于 HLE 的重主机引擎相同的限制,但随着用户提供的钩子在此生态系统中的巨大可重用性,入门门槛随着时间的推移逐渐降低。许多常见且流行的嵌入式平台共享其 HAL,而这些钩子的实现仅需一次:例如,针对 STM32 板的固件,我们实现了总共 18 个通用的 HAL 函数,其中没有一个函数在少于两个目标中使用,典型的目标最多使用这些函数中的 10 个。此外,由于现有众多基于 HLE 的系统,现成的 HAL 存根已经存在。在外围设备管理和函数钩接功能方面,我们的引擎提供了与许多其他基于 HLE 的系统兼容的功能。对于我们的实验,我们将 HALucinator 的 Python 实现中的多个 HAL 模拟钩子移植到 Rust 中,几乎没有什么难度。HALucinator 的作者 [20] 也认为,尽管这种方法需要一定的人工工作,但它使得基于 HLE 的方法能够处理自动化固件系统,而 P2IM [25] 之类的系统无法做到。在本研究中,我们主要专注于提高执行速度。未来,减少适配新目标到我们模糊测试工具所需的人工工作量可能是一个值得追求的目标。自动识别和挂接 HAL 函数将有助于分析更广泛的固件。将我们接近本地重主机的高性能方法与 Scharnowski 等人的 MMIO 建模技术 [46] 结合,提高通用性,似乎非常有前景。
硬件平台。我们为提高模糊测试性能所采用的许多仿真特定技术,可以适应其他领域甚至是其他 CPU 架构。支持额外的指令集架构(ISA),如 RISC-V,主要是通过扩展引擎的动态重写部分来进行新的翻译传递。然而,我们方法的核心概念对 ARM 环境至关重要:一方面,世界上大部分嵌入式软件提供了大而有趣的攻击面,运行在 ARMv7-M 内核的 MCU 上,另一方面,强大的 ARMv8 CPU 被广泛应用,实施 ARMv7-A 指令集作为 AArch32 执行模式的一部分。原生执行为非常低功耗设备编译的软件的大部分指令,在远强于其的 CPU 上,至此在 ARM 环境下是独特的。将我们的框架应用于更强大的 ARM 核心是合乎逻辑的下一步。开发和测试是在相对低性能的 CPU 上进行的。苹果公司最近通过推出 M1 系列 [2],使得强大的基于 AArch64 的核心变得广泛可用并受欢迎。然而,在我们的实验中,我们确认 M1 芯片不支持 32 位 ARM,ARMv8 实现普遍开始逐步取消支持。然而,32 位支持仍在广泛产品中得到支持,从廉价开发板(如嵌入在 Raspberry Pi 4 中的 Cortex-A72 核心)到高端消费产品(如 2022 年 Thinkpad X13s 中使用的 Cortex-X1 核心),以及现代服务器级 CPU,如 Ampere eMAG 处理器。
相关工作
动态二进制重写。使用动态二进制重写为其他软件创建虚拟执行环境是一个众所周知的概念。例如,原始的 VMWare Workstation 实现 [15] 通过系统级的 x86 到 x86 翻译和陷阱与仿真方法为 x86 系统提供虚拟化功能,用于敏感操作的仿真。类似地,QEMU [13] 允许通过动态二进制翻译来仿真不同的硬件平台。然而,这些方法都不是针对低层固件模糊测试而量身定制的。设计上,性能上会有一定的折衷,并且来自客体的硬件访问可能需要复杂的仿真后端。相比之下,SAFIREFUZZ 的接近本地重主机方法使得可以在具有不同 ISA 变种的更强大主机上运行针对嵌入式 ISA 变种的代码,只要这两个变种属于同一家族(例如,ARMv6-M 和 ARMv8-A)。然而,许多框架也探讨了二进制重写用于模糊测试,如 F R IDA 的 Stalker 模式 [29] 或 AFL++ 的 QEMU 和 Unicorn 模式 [26]。这些框架旨在提供优化的重写技术以降低运行时开销,但没有一个考虑到接近本地重主机的可能性。AFL++ 的 QEMU 和 Unicorn 在应用插装之前将二进制代码提升到 TCG,它的中间表示,而 Frida 要求模糊测试目标的 ISA 与主机的 ISA 相匹配。
静态二进制重写。最近,提出了不同的静态重写方法用于模糊测试。像 retrowrite [21]、StochFuzz [49] 或 ZAFL [43] 这样的框架将一次性重写成本的较大部分转移到静态离线阶段。因此,运行时的重写保持到最小,或者完全消除。虽然这些方法与通过基于源代码的插装进行模糊测试相比,达到的性能具有竞争力,但在写作时,这些框架都未能实现固件的模糊测试。它们都专注于 x86 或 AArch64 ISA,并且对目标二进制的布局有很强的假设,例如代码和数据段的明确区分或位置无关代码。我们注意到,这些使得静态重写高效的假设在二进制固件中很少适用,这也是我们为 SAFIREFUZZ 采用动态重写方法的原因。
重主机。近年来,重主机 [24,47] 使得各种类型的嵌入式系统的模糊测试成为可能,从基于 Linux 的物联网设备 [36, 50] 到无线芯片组 [33, 40, 45],再到具有单体固件的深度嵌入式设备 [16, 20, 25, 31, 38, 46, 51]。SAFIREFUZZ 从这些框架和原型中汲取了直接的灵感,尤其是来自基于 HAL 的重主机方法,如 HALucinator [20] 和 Para-Rehosting [38]。然而,与 SAFIREFUZZ 相比,大多数之前的重主机方法更专注于为目标固件创建仿真环境,而不是探索高效的模糊测试解决方案。最显著的例外是 FirmAFL 和 Fuzzware。FirmAFL 旨在通过使用 QEMU 的用户模式仿真器对单个应用程序进行模糊测试,同时在需要时选择性地使用全系统仿真来提供额外的运行时上下文,从而提高 Linux 基固件的模糊测试效能。而 Fuzzware 则针对单体固件,将模糊测试器集成到外设建模过程中,同时使用本地动态符号执行来缩小可能的输入空间。虽然这两个解决方案为固件模糊测试提供了补充,但它们都依赖于基于 QEMU 的仿真引擎,并且与 SAFIREFUZZ 不同,并未探索一种低开销的二进制重写方法。
与我们的工作并行,MetaEmu [18] 和 ICICLE [19] 旨在通过扩展可重主机架构的范围,推动技术的发展。与 SAFIREFUZZ 的接近本地重写方法相反,这两个框架使用 Ghidra 的处理器和指令集定义来自动推导虚拟化的执行环境。MetaEmu 还可以同时重主机并分析多个连接的目标。尽管它们确实专注于性能,并实施了多个 IR 优化传递,但它们并未在真实目标上与之前的工作进行基准测试。与 SAFIREFUZZ 不同,它们仅仅展示了在一些微基准测试中略微超越 Unicorn。尽管 ICICLE 专注于模糊测试,但它们的主要贡献是有效的架构无关插装。与 SAFIREFUZZ 相比,它们的框架——基于即时编译的 PCode 和 SoftMMU——并未从根本上重新思考仿真,并且在性能上与 Unicorn 相当。
Rehosting一般译为托管,但可能与机房机器托管的意思混淆,而Re+Host较为直观,即通过将所需的固件模块在新的宿主(host)上运行,已达到成规模的低成本动态分析和安全研究目的。
与Rehosting类似概念有固件移植和固件模拟(Emulation),我认为区别在于,固件移植指将PC软件交叉编译安装至开发板,Rehosting往往相反;而与固件模拟相比,Rehosting一般仅将需要的功能模块提取出,必要时适当修改,以用于Fuzzing和安全性研究。
注解
[注释1]Rehosting一般译为托管,但可能与机房机器托管的意思混淆,而Re+Host较为直观,即通过将所需的固件模块在新的宿主(host)上运行,已达到成规模的低成本动态分析和安全研究目的。
与Rehosting类似概念有固件移植和固件模拟(Emulation),我认为区别在于,固件移植指将PC软件交叉编译安装至开发板,Rehosting往往相反;而与固件模拟相比,Rehosting一般仅将需要的功能模块提取出,必要时适当修改,以用于Fuzzing和安全性研究。
[注释2]基本块(Basic Block)是程序代码中一组具有以下特性的连续指令集合:
- 只有一个入口点:控制流只能从基本块的第一个指令进入,不能从其他指令跳入。
- 只有一个出口点:控制流只能从基本块的最后一条指令退出,而不会在中间被跳转到其他地方。
- 中间的指令都是顺序执行的:从基本块的第一条指令到最后一条指令,控制流不会中断。
- 基本块的特性
- 不可分割性:基本块中的所有指令在一次执行中要么全部执行,要么不执行。
- 连贯性:除入口和出口外,基本块内没有分支指令。
- 基本块的典型结构
一个基本块通常以以下几种指令结束:
- 条件分支(如 if 或 branch 指令)。
- 无条件跳转(如 goto 或 jump 指令)。
- 函数调用或返回指令(如 call 或 ret 指令)。
- 基本块的应用
基本块是程序分析中的基本单位,广泛应用于以下领域:
- 编译器优化
- 编译器可以基于基本块进行局部优化(如指令合并、死代码删除)。
- 控制流图(CFG)构建
- 基本块是构建控制流图的节点,用于分析程序的执行路径。
- 模糊测试和覆盖跟踪
- 基本块作为执行路径的最小单元,可以用来记录程序覆盖信息(如哪些路径被测试过)。
- 动态插桩
- 模糊测试工具在基本块入口或出口处插入代码,用于收集执行信息。
- 一个简单例子
以下是一段伪代码及其基本块划分:
伪代码:
c
复制代码
1. x = 5;
2. if (x > 3) {
3. y = x + 1;
4. } else {
5. y = x - 1;
6. }
7. return y;
基本块划分:
- 基本块 1:x = 5;
- 基本块 2:if (x > 3)
- 基本块 3:y = x + 1;
- 基本块 4:y = x - 1;
- 基本块 5:return y;
在执行时,控制流可能依次经过基本块 1 → 2 → 3 → 5,或者基本块 1 → 2 → 4 → 5。
[注释3]hook(或 hooking)指的是在程序中插入自定义代码的过程,以便在特定的操作发生时能够捕捉并修改它们的行为。它是一个常见的技术,广泛用于调试、监控、篡改、增强功能或进行安全分析。
- 具体来说,hooking 的作用包括:
- 拦截函数调用:通过钩住(hook)程序的函数调用,允许在函数执行之前、之后,或取代其原始功能。这可以帮助你对程序行为进行监控、修改或扩展。例如,如果你希望在某个函数被调用时记录日志,你可以“钩住”这个函数,插入自己的代码来实现日志记录功能。
- 插入自定义代码:在执行过程中,hooking 允许你在程序的特定位置插入自定义代码或“钩子”,这些钩子代码会在程序执行时被触发。通过这种方式,你可以修改程序的行为,监控其状态,或者执行一些特殊操作。
- 修改执行流程:通过 hook 操作,你可以改变程序的执行流程,例如通过修改函数的返回值来绕过一些验证,或者通过更改函数参数来改变某些操作。
- 在这篇论文中,hook 和 hooking 的应用:
- 在固件重寄宿和模糊测试中,hooking 主要用于实现对固件中的特定功能或指令的插桩。
- 例如,在某些嵌入式固件中,hooking 可能用来插入代码来监控硬件抽象层(HAL)的函数调用,从而能够检测或修改硬件访问操作。这种方法通常用于提供额外的调试信息、启用特定的安全检查,或者干预固件的正常执行以测试其脆弱性。
- 在模糊测试中,hooking 可以用来动态地插入测试代码,监控目标程序的行为,并捕捉到异常(如崩溃)。
- 具体的技术细节:
- 函数钩子:如果固件调用了某个库或操作系统函数,而这个函数的行为是你希望监控或修改的,那么通过 hook 你可以插入自定义的代码来代替原始函数或增强其功能。
- 代码插桩:这通常是指在固件的二进制文件中插入代码,以便在执行时进行动态监控或修改。通过 hooking,可以在特定的函数或指令执行时插入代码进行调试、跟踪或者干扰固件的执行。
总之,hooking 是一种动态修改程序行为的技术,它在固件分析和模糊测试中非常有用,能够让你在固件执行过程中插入自定义的监控或修改代码。
[注释4]插桩(Instrumentation)是一种在程序运行之前或运行过程中,向代码中添加额外逻辑(代码片段)的技术。插桩的目的是对程序的行为进行监控、分析或修改。以下是插桩的详细解释:
- 插桩的核心概念
插桩通常是在程序的原始代码中插入附加代码,这些代码不会影响程序的核心功能,而是用于收集信息或修改程序运行的方式。例如:
- 监控程序的执行流
- 记录运行时信息(如函数调用次数、执行时间等)
- 捕获错误或异常
- 进行安全性检查
[注释5]在模糊测试中,覆盖信息是指程序在执行时所经过的代码路径或代码块的信息。具体来说,覆盖信息告诉我们程序在运行某个输入时,哪些代码路径被执行了,哪些没有执行。这种信息对于模糊测试非常重要,因为它能帮助模糊测试工具了解哪些部分的程序已经被测试过,哪些还没有被触及,从而决定下一步的测试策略。
- [注释6]当某个输入能够生成一个新的、独特的覆盖(即执行了之前未触及的路径),这个输入就会被加入到模糊测试的输入集(称为fuzzing corpus)中。这个输入集会被用来生成新的测试用例,以便进行进一步的模糊测试。
[注释7]JIT(Just-In-Time Compilation,即时编译)是一种动态编译技术,用于在程序运行时将代码从一种形式转换为另一种形式,通常是从中间表示(IR)或字节码转换为机器码。它在程序执行的过程中即时完成,而不是在程序执行之前像传统的静态编译那样进行。
- JIT 的工作原理
- 初始输入:
- 输入代码通常是某种抽象形式,例如字节码(如Java的.class文件)或中间表示(IR),这类代码是平台无关的,不能直接在物理硬件上运行。
- 动态翻译:
- 当程序运行时,JIT 编译器会逐步将这些代码翻译成特定硬件架构的机器码。
- 缓存与优化:
- 翻译后的机器码会被缓存,供后续调用时快速复用。
- JIT 编译器可以通过分析运行时行为进行动态优化,例如内联函数调用或去除不必要的代码路径。
- 执行:
- 翻译后的机器码直接运行在硬件上,不再需要解释器的中间参与。
[注释8]MMU(Memory Management Unit,内存管理单元)是一种硬件组件,负责计算机系统中内存的管理和虚拟地址到物理地址的转换。它的主要功能是通过硬件支持来管理内存的访问、映射和保护,确保进程之间的隔离,并优化内存的使用。
MMU(内存管理单元)**通常部署在具有复杂操作系统和多任务处理能力的系统中,这些系统需要虚拟内存管理、进程隔离以及内存保护等功能。
[注释9]lifting 通常指的是将低级别的代码(例如机器码或汇编代码)转换为更高级别的中间表示(Intermediate Representation,IR)的过程。这个过程在动态二进制翻译中很常见,例如在QEMU中。
具体来说:
- 机器码到中间表示的转换:
固件通常是为特定架构(如ARM、MIPS等)编写的机器码,而这个代码无法直接在不同的架构上运行。通过lifting,将这些指令“提升”为一种架构无关的中间表示(IR),使得后续处理(如优化、分析或重新编译)变得更加灵活。 - 方便跨平台执行:
一旦转换为中间表示,工具(如QEMU)可以根据目标主机架构重新生成对应的机器码,实现跨平台的运行。 - 结合动态翻译:
在动态翻译过程中,lifting通常与即时编译(JIT)结合使用:先将目标代码提升到IR,随后将IR转译成主机架构的代码并执行。
[注释10]Function Hooking,可以理解为在固件重寄宿或模糊测试过程中,针对固件中的特定函数进行插桩或“钩住”操作。
- 具体来说,function hooking 是指以下内容:
- 拦截和修改函数行为:通过将自定义的代码插入到目标函数的执行过程中,当固件执行这些函数时,插入的代码会先被执行,从而影响或监控原函数的行为。这通常用于动态分析、调试或修改固件的执行流程。
- 在嵌入式固件中应用:在嵌入式系统中,特别是在低级固件或硬件抽象层(HAL)的代码中,固件常常会调用一些特定的库或硬件驱动函数。这些函数是与硬件交互的关键函数。Function hooking 技术可以通过钩住这些函数,分析或者干预这些调用。
- 模糊测试中的作用:在模糊测试中,通常会使用 hook 技术来插入自定义代码,监控固件的状态或输入输出行为。这允许测试人员观察固件的响应,尤其是在处理意外输入或异常情况下的响应。通过 hook 函数,模糊测试工具能够记录函数的执行、参数变化、内存访问等信息,从而更好地探索固件的漏洞。
- 在论文中的具体应用:
- Function hooking 在本论文中的目的之一是在运行时修改固件的函数调用行为,尤其是那些与硬件交互、内存访问或控制流相关的函数。这是为了在进行模糊测试时能够监控和控制固件执行的关键部分,特别是在固件中需要与硬件外设交互的部分。
- 例如,如果固件调用了一个读取硬件寄存器的函数,通过 hook,这个函数就可以被修改,使得它返回模拟的数据或者让它产生特定的错误。这样,研究人员可以测试固件在面对不同硬件状态时的行为,以找到潜在的漏洞或错误。
- 动态插桩:通过动态 hooking,在固件执行的过程中插入代码,而不是修改静态的二进制文件。这种技术对于处理固件中那些依赖硬件的部分尤为重要,因为这些部分在不同的硬件平台上会有不同的行为。通过这种方法,研究人员能够实时分析固件如何与硬件交互。
- 总结:
Function hooking 在这篇论文中是指通过钩住(hook)特定的函数调用,并插入自定义代码,以便动态地监控、修改或控制固件的行为。这种技术在模糊测试中非常有用,可以帮助研究人员更深入地理解固件如何工作,尤其是与硬件的交互部分,同时也能在测试中更有效地发现潜在的漏洞。
写在最后
本文的预备知识和注释以及翻译都是根据自己现有知识自己完成的,由于能力的不足,可能会存在一些问题,希望大家看到后可以指出,另外本文预备知识和注释仅代表个人观点,大家参考即可。