嵌入式设备通常具有资源受限、环境复杂等特点,导致诸多脆弱性分析工具难以在嵌入式系统中有效应用
[1-2]。为了解决这些困难,研究人员先后提出全系统仿真
[3]、硬件在环
[4](Hardware-in-loop)、固件托管
[5](Rehosting)等虚拟仿真技术。其中,固件托管以开发周期短、方便扩展等优点而备受关注,其核心挑战在于如何判断外设的硬件生成值(Hardware-generated Value),即固件在执行过程中通过与外设交互所获取的值。这些值将直接影响到固件的功能和执行路径。
然而,硬件外设种类繁多,其向固件传递的硬件生成值各不相同。研究人员需要在没有硬件的情况下判断这些值,往往需要花费很大精力,并且容易出错。现有的多数固件托管技术在固件需要硬件生成值时再动态判断,例如P
2IM
[6]的做法是根据外设寄存器在固件中的访问方式对寄存器进行分类,具体来说,就是人工为目标MCU构建抽象模型,捕获固件在访问处理器外围接口时遵循的通用模式和约定。这种做法的缺点是仅针对具体外设的某个行为进行判别,外设寄存器类别判断的准确率不高。
针对上述问题,提出一种利用支持向量机
[7-8](Support Vector Machine, SVM)对寄存器进行预分类的方法。首先,利用固件操作寄存器的行为特征,提取出与寄存器相关的二进制代码样本并整理成数据集;其次,将提取好的特征样本分为训练集和测试集,利用训练集对SVM模型进行训练;最后,使用测试集对模型进行验证和评估,调整模型参数以提高分类的准确性和泛化能力。相较于P
2IM仅针对某个寄存器具体行为进行的判别方法,利用大量相同指令集产生的训练样本,可有效提高分类正确率。在此基础上,对正确类别的寄存器进行建模处理,确定寄存器提供硬件生成值的方式,从而保证固件的顺利执行。
在寄存器分类的基础上,构建了一套固件托管系统,对各类寄存器的处理行为进行建模,在固件仿真执行时提供硬件生成值,实现固件托管,并在托管的过程中结合模糊测试工具
[9],实现更高的代码覆盖率,寻找到多个崩溃点,为脆弱性分析工作提供基础支持。
1 外设寄存器预分类
根据功能的不同,外设寄存器可以分为4类:控制寄存器(CR)、状态寄存器(SR)、数据寄存器(DR)和控制状态寄存器(C&SR)。其中:控制寄存器通常用来设置外设的技术参数等,例如串口中的波特率寄存器;状态寄存器用来在执行过程中动态显示外设的状态,例如当串口buffer内数据存满时,对应状态寄存器中的某个比特位就会被置1;数据寄存器用来存放数据,它通常是外设的I/O来源;控制状态寄存器是指寄存器的某些比特位具备控制功能,某些比特位具备显示状态的功能。
本章节从样本采集、特征提取以及分类算法3个方面,介绍寄存器预分类的基本原理。
1.1 样本采集和特征提取
通过研究发现,固件访问寄存器的行为主要依靠汇编码来描述,不同的指令集具有不同的汇编指令特征,因此在操作寄存器时表现出不同的行为特征。以ARM Cortex-M3所使用的Thumb-2指令集为例,其操作控制寄存器的行为可以描述为:用LDR指令读取一个基地址,再使用一个LDR指令间接寻找到所要加载的寄存器地址,随后利用ORR等位操作指令对寄存器重新赋值,再将赋值好的值用STR指令写回寄存器地址中,具体汇编码特征如
图1所示。
再如,固件需要读取状态寄存器来实时获取外设的状态,其行为特征可描述为:首先使用LDR指令加载寄存器地址,再通过CMP指令比较状态值,并根据状态值来进行相应的分支跳转BEQ/B,具体汇编码特征如图2所示。
基于上述原理,提出以汇编码作为数据集,使用SVM技术进行寄存器分类的方法。通过逆向分析技术获取固件访问寄存器的地址,查询技术手册确定寄存器类别,提取该寄存器访问行为周边的汇编码特征作为输入,以寄存器类别作为输出,生产机器学习分类算法的训练样本。
1.2 寄存器分类算法
适用于小样本场景下的分类算法模型SVM是一种基于统计学习理论的监督学习算法,对于线性可分和非线性可分的数据均有良好的处理能力
[10]。
对寄存器的分类由两个步骤完成:首先是进行二分类,即根据数据集来判断寄存器是不是某一类寄存器,从而判定数据样本的准确性;其次是进行四分类,最终将寄存器分为4类,并将分类结果写入配置文件。按照SVM对结构化输入数据的要求,需对原始数据进行格式化处理,分为以下3个步骤。
第1步,编写Ghidra
[11]脚本从固件中定位MMIO访问寄存器的位置,以二元组(寄存器地址:访问位置)的形式进行记录。
第2步,根据访问位置,提取访问寄存器周边行为的汇编码。提取的规则是:从赋基址的指令开始提取,如遇到以下条件之一,则停止提取:
1)遇到POP等函数结束指令;
2)遇到BE、BNE等指令指示的跳转地址;
3)提取的字节数超过14字节。
第3步,根据二元组信息中的寄存器地址查询技术手册,确定寄存器类别,以“样本数×特征”的矩阵形式存储数据样本。矩阵中每行代表一个样本,由提取的“特征+类别”标签构成,该数据样本用于构建和划分数据集。
对于汇编码中的非模式部分,如具体地址、立即数等,采取泛化指令集的方法,将指令集中操作数部分提取出来,再将寄存器和立即数部分进行泛化,从而得到多个数据特征,增加样本数据量。例如,指令“LDR R3, =0x40021000”的主要特征是LDR,可以相应地更改R3为其他寄存器。根据Thumb-2指令集手册中有关LDR指令的描述(如
图3所示),指令中的第8~10比特用来指示寄存器,因此可以遍历3比特的值,这一行可以得到8个特征汇编码,再与后面的指令一起组成数据集。
2 固件托管系统设计
构建的固件托管系统主要分为两个模块:寄存器分类模块和仿真执行模块,其基本架构如
图4所示。其中:寄存器分类模块以上述预分类方法为基础构建分类模型,对寄存器进行预分类,将结果以配置文件的形式保存下来;仿真执行模块使用QEMU
[12]作为仿真工具,根据不同类别的寄存器设计响应模型,保证固件的顺利执行。为验证方法的有效性,在构建的系统上对固件进行模糊测试,同时统计代码覆盖率。寄存器分类模块已在第1章详细描述,本章重点介绍仿真执行模块。
仿真执行模块将分类的结果应用于固件托管中,针对不同类别寄存器的特性,设计出不同的分类响应模型。经过分析,控制寄存器的操作通常是在初始化的过程当中,因为它代表了对外设基本属性的设置;固件根据状态寄存器指示的状态进入不同的执行分支;控制状态寄存器的个别比特具备控制功能、个别比特具备状态功能,因此可先按照控制寄存器的方式进行处理,而后进行修正;数据寄存器通常用来记录输入输出,可在此处输入测试用例进行模糊测试。
除控制寄存器涉及初始化操作外,其余类别寄存器的写入动作并不会影响固件的执行路径,因此可以忽略。对外设寄存器的建模,主要是对读取动作的响应行为进行建模。
图5展示了仿真执行模块的设计原理。当模拟执行固件遇到读写寄存器时,首先从分类结果中读取寄存器类别,其次根据类别按照如下模型进行处理。
1)如果是写入控制寄存器,就将写入值记录到存储中;如果是读取控制寄存器,则从存储中读取。
2)如果是读取状态寄存器,通常会有根据该值进行的分支操作,此时需从仿真执行状态切换到符号执行状态,预测出分支所需的输入值,再返回仿真执行,2.1节将详细描述。
3)如果是读取数据寄存器,将模糊测试的测试样例输入到数据寄存器中,以达到持续执行固件的目的。
4)如果是读取控制状态寄存器,分两种情况:如果在读取该值之前有写入过值,就将写入的值赋给寄存器;如果没有写入值,或者读取该值后有CMP指令,则采用SR写入的方法,即符号执行进行修正。
5)重复执行上述1~4步,如果没有未建模的PC位置,并且没有未分类的寄存器,则完成固件的托管,通过QEMU模拟器自带的trace功能统计执行路径。最后,再对固件进行一次模糊测试,寻找崩溃点。
2.1 对状态寄存器的符号执行
符号执行
[13]是一种有效的程序分析技术,可以在固件托管时探索执行路径
[14-15],以
图6所示的代码为例,它来自于NXP SDK的串口读取函数。
在
图6第11行的while循环中读取base结构体中的寄存器值,只有当此寄存器的某些位为1时,才会跳出while循环,继续执行下面的语句。因此,为了让固件继续执行,需将base结构体中的未知寄存器标记为一个符号,随后计算出满足继续执行的值。
基于上述思路,结合动态符号执行,对状态寄存器的读入值进行预测。
图7展示了动态符号执行的基本原理。它的实现方式是在仿真执行和符号执行之间进行切换,当仿真执行访问到一个未实现的状态寄存器时,记录寄存器状态和PC值等上下文信息,随后切换到符号执行,将状态寄存器进行符号化处理,利用约束求解计算出对应的寄存器值,反馈给仿真执行。
当满足如下条件之一时,从符号执行切换回仿真执行。
条件1:遇到软中断(SVC调用)或者异常返回指令;
条件2:遇到长循环结构;
条件3:执行达到的分支数量超过10个;
条件4:符号执行时间超过30 s。
对于条件1,由于遇到异常或中断时程序通常会崩溃,在此情况下进行符号执行比较困难。对于条件2~条件4,是为了防止状态爆炸、执行时间过长。
符号执行的难点是如何有效地探索固体执行路径,为此提出一种动态路径选择策略,具体遵循以下策略进行最佳路径的选择。
策略1:符号执行工具在返回到仿真执行之前,必须积累2个以上的分支数量。该规则可以避免固件执行陷入死循环。
策略2:对于不影响固件执行路径的系统级ARM指令,直接替换为NOP指令。由于符号执行工具无法在特权模式下执行指令,需对系统级指令进行预处理。对于不影响执行路径的指令,可以将其用NOP指令替换。对于影响执行路径的指令,则直接退出符号执行状态。
策略3:优先执行新路径。在QEMU执行阶段利用trace功能记录已经执行的基本块,优先考虑没有执行过的基本块。该策略可确保优先探索未执行的代码路径,提升代码覆盖率。
2.2 对数据寄存器的模糊测试
固件通过读取数据寄存器来获取外设传入的数据,例如串口数据、网络数据等。以数据寄存器作为输入点,输入随机的测试用例,对固件进行模糊测试。
模糊测试发生在两个阶段。第1阶段是在探索路径进行阶段,如果在探索路径阶段遇到固件读取数据寄存器的情况,则从测试用例中读取数据输入。测试用例是通过对初始种子进行变异而得来的,依据语义有效性优先原则,根据数据寄存器所在外设的不同,有针对性地选择符合目标输入的基础语法规则来进行初始种子的设置。例如,对外设为网络设备的数据寄存器,遵循OSI/RM 7层模型或TCP/IP 4层模型,输入结构呈现嵌套式分层封装,如
表1所示。因此,在初始种子的设置中,对语义有效性进行了分层,设置了3种类型的种子。
1)合法种子:符合协议格式的规范输入(如完整的HTTP/1.1请求);
2)半合法种子:保留协议框架但包含异常字段值(如HTTP头中的超长Content-Length);
3)非法种子:完全破坏协议结构(如随机字节流)。
模糊测试的第2阶段是在探索路径执行完成后进行的,目的是执行尽可能多的路径,记录崩溃点。具体方法是使用模糊测试工具加载仿真执行模块,捕获固件对数据寄存器的访问,在访问点输入测试用例并继续模拟执行,同时实时监测程序运行时的异常行为(如崩溃、内存泄漏、逻辑错误等)。采用覆盖率引导反馈策略来影响测试用例的变异,在执行模糊测试之前,编写Ghidra脚本记录外设寄存器的访问位置,利用二进制插桩技术在访问位置中插入一些用于信息采集的代码段,如执行路径记录、函数调用次数等,插入的代码不会改变原程序的语义。插桩代码可以记录程序执行过程中哪些代码块被执行到,从而为模糊测试提供覆盖率信息。具体的执行过程中,当检测到一个新输入数据使得程序执行路径发生变化,即达到了新的代码覆盖率时,将其加入到后续测试的种子队列中,用于进一步衍生出更多新的输入数据,以此不断拓宽对程序功能的探索范围,提高发现固件执行崩溃点的效率。
3 实验及结果分析
3.1 实验设置
3.1.1 实验环境
本次实验包含寄存器分类实验与仿真执行实验,两类实验的环境参数分别如下:寄存器分类实验环境中,处理器为Intel (R) Xeon (R) W-2245 CPU @ 3.90 GHz,内存为64 GB,操作系统为Windows 10,scikit-learn版本为1.3.2;仿真执行实验环境中,处理器为Intel (R) Core (TM) i7-11390H @ 3.40 GHz,内存为16 GB,操作系统为Ubuntu 16.04。
3.1.2 数据集
利用从设备供应商网站中获取嵌入式系统固件,从中选取了19款不同MCU芯片的固件进行样本分析,涉及STM32F103RB、MK64FN1M0VLL12、SAM3X8E等多个MCU芯片类型,如
表2所示。通过对每个固件进行分析,从中提取出1 572个寄存器训练样本。
3.2 寄存器分类实验
为验证分类效果,对P2IM的寄存器分类方法进行了复现,并在相同的数据集上进行对比测试。实验选取4种常见的核函数:线性核函数(Linear Kernel)、径向基函数(Radial Basis Function,RBF)、多项式核函数(Polynomial Kernel)、Sigmoid核函数(Sigmoid Kernel),分别展开二分类与四分类实验。
二分类实验旨在判断寄存器是否属于某一特定类别,以验证单类别判别准确率。不同函数下的实验结果如
图8所示。结果表明,除使用线性核函数外,其余核函数的正确率相比P
2IM均有不同程度的提高。例如,状态寄存器判别的准确率高达94%,比P
2IM提高了44%。控制寄存器、数据寄存器、控制状态寄存器整体的分类正确率分别达到了79%、89%和84%,均体现出显著优势。
采用径向基函数(RBF)对不同架构、不同外设的固件进行四分类测试实验,实验结果如
图9所示,相比P
2IM方法,本文方法的整体正确率提高36.29%。
从实验结果可以看出,由于使用了大量的数据样本进行训练,相同类别的寄存器存在较为稳定的行为特征,使用SVM技术可以提高其准确性。而P2IM使用的是动态建模的方法,没有和同类寄存器进行联合判断,因此存在误判的风险。例如从技术手册中可以得知0x40000120、0x40000104、0x40000114这3个寄存器为控制寄存器,但由于其在固件中是以间接寻址的形式出现,如图10所示,寄存器R6存放基址。由图11所示0x000806CA处开始的3行汇编码可以看出,这里仅是给寄存器0x40000120、0x40000104、0x40000114的赋值,因此被P2IM判断为数据寄存器。而通过本文方法对该类样本的标记为控制寄存器,再进行分类时,可以准确判断出类别。
3.3 仿真执行实验
选取与P
2IM相同的10个具有代表性的固件进行测试实验,结果如
表3所示。
表3中NF为函数数量,NC为覆盖函数数量,CC为代码覆盖率。
从实验结果看,10个固件的代码覆盖率分别有了3.31%~36.84%的提高,这说明正确的寄存器类别起到了较好的固件托管的效果。
为了验证本文方法对固件进行脆弱性分析的有效性,在仿真执行模块时加入了模糊测试,
图12是各个固件在执行24小时后的崩溃点数量。从结果来看,不同固件均可产生不同程度的崩溃点,符合脆弱性分析预期。
4 结束语
针对现有嵌入式固件托管方法中外设寄存器分类正确率较低且需动态判别的问题,提出一种面向固件托管的外设寄存器预分类与建模方法。该方法基于SVM技术,以固件汇编码特征作为输入,以寄存器类别作为输出,有效提升了寄存器分类的正确率。在预分类基础上提出的外设寄存器建模方法,能够有效提高固件托管的代码覆盖率,避免了固件动态执行过程中反复对寄存器类别进行判别。在固件托管系统中进行的模糊测试,能找到多个崩溃点,为固件的虚拟仿真和漏洞挖掘提供重要支撑。