作者:lylemi
原文链接:https://blog.lyle.ac.cn/2021/07/09/uemu/
对物联网设备中的应用程序进行模糊测试时,直接使用实体设备进行测试是一种比较直接的方法,但是实体测试会带来较高的测试成本,也无法自动化地对待测目标进行测试。考虑到实体设备测试面临的限制,使用虚拟化技术对设备进行测试是一种方案,但是直接使用QEMU仿真并不能保证成功测试目标程序,本文尝试对其中的原因进行分析,并提出一种相对通用的解决方案模型。
物联网设备分为多种类型,本文的测试目标主要考虑使用通用操作系统的设备,即拥有轻量级用户空间环境如busybox、uClibc等的Linux类设备环境。在这类设备的环境中,与定制硬件进行的交互大部分是通过特定的设备驱动进行的。
本文分为几个部分,可以按需阅读,第一部分介绍了当前工作,说明在有firmadyne、qiling等强大的工具下为什么还需要有新的仿真工具;第二部分说明了本文解决问题的思路;第三部分对具体实现的一些细节进行说明;最后一部分基于实现完成了一些小规模的实验,说明了本文的仿真能力。如果只是想对本文思路有简单的了解,可以阅读 TL;DR
部分。
本文提出的方法在用户层对物联网设备中的网络服务进行仿真用于测试,通过对系统调用分类并在驱动层建立设备模型的思路,实现了成功率较高、相对泛用且易扩展的一种仿真方案。
仿真的方法可以分为四种类型:全系统仿真、用户态仿真、应用级仿真、代码片段仿真。全系统仿真对整个设备的操作系统进行仿真,运行操作系统的所有组件。用户态仿真,也可以称作进程级仿真,最常见的仿真方式就是使用chroot改变根目录到固件文件系统的目录下,并使用仿真工具的用户模式执行待测程序。
应用级仿真是指并不执行程序,而是仅仅加载网络应用对应页面的方式。最简单的应用级仿真是直接用对应架构的操作系统系统,将网页文件复制出来,使用常用的网络服务启动。但是这种方式会丢失过多的细节,另外很多固件使用了自定义的脚本语言函数来获取硬件配置的信息,因而通过这种方式只能对HTTP服务进行仿真,且只能检测相当有限的漏洞类型,例如命令注入、跨站脚本攻击等。
代码片段仿真则只执行二进制文件中的一部分代码,使用Patch、预设数据等方式使得代码可以正常执行。
在目前,需要仿真这类设备时,通常使用的工具为QEMU、Unicorn等,在这些工具之上有Firmadyne、Qiling等相对完善的框架。QEMU提供用户模式和系统模式两种运行方式,用户模式主要用于执行不同处理器的Linux程序,系统模式用于模拟整个系统运行环境,包括CPU及其他设备,使得系统级的测试更为方便。
在QEMU的基础上,Firmadyne倾向于对整个系统进行仿真,主要考虑解决QEMU在仿真物联网设备时遇到的难点。Firmadyne以系统级仿真为基础,使用定制动态链接库完成库函数劫持支撑NVRAM设备调用,基于定制内核完成操作系统启动、探测网络结构、虚拟硬件,通过系统配置完成网络构建,最后调用QEMU仿真异架构操作系统完成仿真。
但是Firmadyne的方法存在一定的局限性,在Firmadyne获取的23035个固件中,其中8617个固件可以成功解包,在解包成功的固件中有8591个固件能够成功启动系统,成功启动系统的固件中只有2797个可以成功配置网络,最后只有1971个固件可以访问网络。在所有能解包的固件中,仅有22.9%的固件可以通过网络访问,即可以对其进行测试。根据Firmadyne论文中的解释,不能成功仿真主要是因为提取固件失败、NVRAM仿真失败、网络设备仿真失败等原因。
Qiling提供了非常强大的跨平台与跨架构的二进制仿真能力,可以对二进制文件或者代码片段进行仿真,可以进行自动化patch。但是并不是物联网设备专用的工具,在仿真设备时仍需要一定的手动分析。
虽然当前工具已经解决了很多问题,但是但是在测试目标上仍然有局限性。如果要对型号多样的物联网设备进行测试,需要找到一种较为合适的仿真方式。在已有工具的基础上,本文尝试提出一种仿真成功率高、更容易调试与扩展的方案,可以更简单的对物联网设备中的二进制文件进行仿真,从而进行测试。
比较泛泛的讲,物联网设备难以仿真的主要原因是运行环境复杂,设备由很多不同厂商生产,仅路由设备常见的厂家就超过四十余家。而各个厂商会提供不同系列和型号的产品,不同产品又使用各种不同的硬件、指令集架构、操作系统、网络协议。其中部分产品依赖自研的外部设备,部分产品会对操作系统进行深度自定制,网络协议可能会有自研的通信格式,最后衍生出复杂的软硬件依赖问题。尤其是物联网设备的自研部分往往是闭源的,各个厂商对协议、外部设备有不同标准和实现,这些非标准的协议和设备实现多不公开,继续加大了仿真的困难程度。
更具体来说,可以把二进制程序的执行分为两部分,用户态和内核态。用户态的指令可以由通用的仿真工具来翻译执行,而最终进入内核态的系统调用需要进一步构建执行环境解决。也就是说,仿真要解决的问题实际是如何执行系统调用的问题。
而在具体的仿真方案选择上,全系统仿真引入了不必要的复杂性,应用级仿真缺少细节,代码片段仿真更适合测试一部分功能或调试漏洞,因此本文的目标是通过构建合适的系统调用模型来在用户态执行物联网设备中的二进制程序。
系统调用有很多种分类方式,其中一种分类方式是将系统调用分为进程控制、文件管理、设备管理、信息维护、通信、保护六大类,本文在这个分类的基础上继续对问题进行解决。
进程控制类系统调用主要用于完成创建进程、终止进程、载入与执行进程、获取进程属性、等待时间事件与信号、申请与释放内存等功能。文件管理类系统调用主要用于完成创建删除文件、打开关闭文件、读写文件、读取或设置文件属性等功能。设备管理类系统调用主要用于完成获取与释放设备实例、读取或设置设备属性、挂载或卸载设备等功能。信息维护类系统调用主要用于传递信息,例如当前的事件、日期、用户数、操作系统版本、内存或磁盘信息等。通信类系统调用负责进程间通信,实现常用的消息传递模型和共享内存模型等功能。保护类系统调用负责设置资源权限,用于允许和拒绝用户访问特定资源。
其中信息维护、通信、保护间通信三类系统调用宿主机可以较为容易的支撑,而其他几类系统调用在从模拟器环境向宿主机环境转发时则存在如下图所示的几个需要解决的问题。
进程在执行时,会需要进程本身的可执行文件、可执行文件对应的动态链接库文件、用于配置程序的配置文件,以及用于写入临时文件、日志文件、进程当前信息的目录等文件与目录环境。在仿真时通常会通过加载原本固件的文件系统的方式来构建文件系统的运行环境,但是在由于设备固件并不遵循统一的标准,提取出的文件系统可能并不完整,会存在部分文件无法找到的情况。另外,有的文件由设备在运行时动态创建,简单的文件系统提取并不能获取对应的文件。
物联网设备通常需要大量的外部硬件设备参与运行,主要是运算与控制设备、网络设备、存储设备与输入输出设备。模拟器仅对常见的硬件设备进行了支持,其中包含了运算与控制设备、部分内存与磁盘设备、部分输入输出设备。但是物联网设备中存在着大量的定制外部设备,如定制的NVRAM、Flash存储设备、网络设备等,在执行到和这些设备相关的系统调用时,可能会面临缺少输入输出设备与网络设备,设备的硬件调用宿主机不支持等问题。
待测的可执行程序和宿主机大多是不同架构的,需要通过模拟器执行。而在通过用户态模拟执行新的程序时,由于系统调用被转发到了宿主机,宿主机将以正常的进程加载方式加载程序,但是不同架构的程序在默认场景下并不受宿主机支持,此时程序无法被执行,父进程报错终止。
系统调用劫持主要是基于ptrace控制系统调用,在一些检查环境的系统调用处实现控制,获取相关信息的同时屏蔽一些不重要的报错,使得程序可以正常运行。
基于ptrace的方案的缺点在于性能消耗较高,每次系统调用都需要有对应的逻辑判断。为了减少这种消耗,在长期测试时可以根据收集到的信息生成对应的内核模块代码,编译为内核模块,在后续需要屏蔽、修改部分系统调用或用户态调用的情况下,使用基于Linux内核模块控制系统调用的方案完成持久化。
进程运行所需要的文件主要是进程本身的可执行文件与对应的链接库文件,系统的设备与配置文件,进程的配置文件与进程在运行时产生的进程信息、日志文件、临时文件几种类型的文件。其中从固件提取出的文件系统包含有可执行文件、动态链接库文件、操作系统的配置文件,正常情况下,操作系统在启动后会创建系统的设备文件、用于写入日志文件的目录与各个程序的配置文件。
满足文件依赖从下至上分为四个层次来对缺失的文件进行补全,第一层挂载固件文件系统,这一层是执行的基础;第二层根据运行时信息动态创建缺失的文件,这一层在可执行文件的基础上创建部分所需的文件;第三层覆盖特定的系统配置文件,这一层用于对配置进行归一化方便进行后续的测试;第四层是根据对指纹、配置文件的解析创建应用对应的配置文件,这一层是在之前的基础上进行细节的修正,保证待测程序可以正常运行。
用于挂载的文件系统来自于之前通过解包固件获取的文件系统,主要包括可执行的二进制文件与对应的动态链接库。
动态创建的文件主要是本应在Linux系统启动时创建的文件,主要是 var 目录下的多个子目录与文件,proc、sys、dev等目录,这些文件与目录在待测程序启动前进行通过宿主机进行创建或挂载。
之后覆盖系统基本的配置文件,这类文件可能存在于固件中或不存在,但是格式都是已知的。系统基本的配置文件包括passwd、shadow等用户相关的配置、DNS服务器等网络相关的配置,还有TZ、localtime等时间相关配置。因为固件生态的多样性,这些文件可能存在自定义的部分,为了测试环境的统一化,本文使用预置的系统配置文件对这些文件进行覆盖。
最后一部分是动态运行所需要的内容,主要是不同类型的服务需要不同的配置文件,同一类型不同实现的服务也需要不同的配置文件,这些配置文件往往是根据设备状态动态创建出来的,并不存在于固件中。不同类型的服务器例如DNS服务启动所需要的dnsmasq.conf,PPTP服务启动所需要的pptdp.conf,SMB服务启动所需要的smb.conf。同一类型的服务也存在不同实现,以HTTP协议为例,存在lighttpd、mini_httpd、mathopd等多个大类的实现,不同应用所需要的配置类型和位置是不同的。
对于同一类型不同实现的服务,本文根据程序的类型和当前环境动态创建配置文件,执行文件并进行测试。如果程序执行成功则保留该配置文件,如果执行失败则尝试其他参数与配置文件。如果预置的配置文件不能成功,则分析固件中的系统脚本,主要是初始化文件,从中找出配置文件的生成方式与程序的执行参数,并进行相应的执行来创建配置文件。
用户进程和硬件的交互过程如下图所示,用户态程序加载动态链接库,动态链接库根据标准用户库中的标准输入输出相关的函数构造对应的系统调用转发到内核层,内核根据系统调用对应执行设备驱动中的代码。
对于缺少外部设备的问题,Firmadyne的解决方案是通过自定义的用户态动态链接库在软件层劫持相关的调用来实现。Firmadyne自定义了用户态的动态链接库,通过预加载的方式通过该链接库控制对硬件的调用,当用户态应用调用对应函数的时候,会优先调用自定义的用户态标准库,从而实现用户态的NVRAM功能。
但是这种方式存在几个问题,首先,基于劫持的方式需要了解上层应用调用的函数名称,Firmadyne仅仅通过枚举来解决,一旦遇到没有在枚举列表中的函数,运行就会出现错误。其次,这种方式仅支持NVRAM一种设备,可扩展性差,无法适应其他的设备。另外,对于每一种架构,这种方式都要编译一个对应的动态链接库文件,需要维护多套编译环境。
考虑到Firmadyne的缺陷,本文主要使用定制内核模块的方式来实现虚拟的设备。在Linux操作系统中,硬件设备也被看做文件来处理,有对应的文件标准操作。除此之外,在Linux的设计中,驱动定义的标准仅有数次比较小的修改,可以较为容易的枚举出所有的驱动操作。具体来说,POSIX的内核驱动标准中仅定义了read、write、ioctl等数种意义较为明确的硬件操作,也在一定程度上减轻了实现的难度。本文最后根据驱动定义标准设计内核模块,对于每一种设备,以内核驱动的方式,模拟实现文件的标准操作,通过定制内核模块完成外部设备的软件形式实现。
根据Firmadyne的分析,52.6%的固件都通过用户态链接库访问了NVRAM,大部分固件的核心设备也以NVRAM为主,因此本文同样主要关注NVRAM的实现。NVRAM可以看作一个硬件实现的哈希表,用户可以通过键值对的形式向NVRAM写入需要存储的变量,也可以通过输入特定的KEY值来读取之前存储的数据。基于NVRAM的输入输出特点,本文在软件层进行了一个哈希表的实现,并完成了对应的驱动,加载驱动后,设备可以按照正常的NVRAM调用方式进行运行。和Firmadyne相比,本文的实现方式更加通用,且能够更好的处理系统调用的情况。
在本文的实现方式下,每种设备仅需要对应实现几个驱动的函数即可,不需要适配同一设备的不同用户态调用。另外因为本文的硬件实现最后挂载在宿主机中,系统调用的翻译已经在用户态模拟完成,所以这种方式并不需要对不同的架构进行适配。
除了I/O设备之外,物联网设备可能会依赖一些特定的网络外设,这部分本文在基于系统调用劫持获取信息的基础上,创建一张对应名称和IP的网卡,并使用桥接的方式和本地网卡连接,以用于后续的测试中。
除了IO设备与网络设备,还有类似LED等少数附加设备,这些设备通常有专门的可执行文件控制,网络服务程序并不直接和这些设备进行交互,对于这部分设备,本文使用前文中提到的系统调用劫持直接屏蔽对应的系统调用。
如前文中提到的,部分进程在执行时会进行execve、fork等系统调用操作,而因为本文使用了转发系统调用到宿主机的方式,在使用这些系统调用时,进程会脱离模拟器环境,由操作系统来执行程序。
操作系统加载可执行文件时,会默认按照宿主机架构加载程序代码,进行解释执行。显而易见的,宿主机在默认情况下无法处理异架构的程序,对于这个问题,本文提出了一种基于内核配置的跨架构进程透明启动技术。
无论用户层使用什么方式创建一个新进程,最后都会通过execve等系统调用传递信息至操作系统,由操作系统内核寻找对应格式的处理器来执行对应的进程。本文注册内核的执行函数,在涉及execve、fork脱离模拟器环境时调用execve时进行判断,如果当前载入的程序并非宿主机架构的程序,则载入对应的模拟器环境加载该程序用于执行,防止程序脱离当前定制的用户态仿真环境执行。
关于其中具体工具的使用和配置,可以参考这篇文章 。
为了验证仿真工具的能力,设计了一些简单的实验进行测试。本文主要使用网页爬取与FTP同步的方式,基于网页的爬虫自动解析厂商的固件下载页面并下载固件;基于FTP同步的方式主要同步厂商FTP中与固件相关的文件,例如后缀是zip、bin、pkg等结尾的固件。另外考虑到实验的多样性,也手工下载了一些品牌的固件用作实验。
经过爬取,本文一共得到来自46个厂商的14483个固件作为测试集,用于验证测试模式的产生效率以及实际的测试实验。固件数据集中比较多数的是路由设备的固件,也包含一些摄像头、NAS的固件。固件中包含i386、ARM、MIPS、PowerPC,并有对应的32位、64位、大小端等多种不同架构。由于不同厂商对固件开放的程度不同、产品数量不同,在数据库中部分厂商如D-Link、TP-Link等厂商的固件占了较大的比例。
总计爬取了14483个固件,其中因为文件格式没有成功识别、解压缩或解密错误、固件中不包含正常可执行文件等原因,有6495个固件不能正确解包,本文对正确解包的7989个固件进行实验。
以应用程序为维度衡量仿真能力,本文判定应用程序是否仿真成功的标准为:使用编写好的测试程序发送对应协议的请求报文,对应端口返回了协议对应的正确响应时,认为仿真成功,否则认为仿真失败。
基于这个标准,本文对仿真成功的程序架构与类型分别进行了统计,在仿真成功的程序中,各个架构程序的数量如下表所示。由于有大量的固件使用了同样的可执行文件,本文在统计中分别统计了执行成功的程序数量与根据哈希去重后的程序数量。
架构 | 位长 | 大小端 | 程序数量 | 去重后数量 |
---|---|---|---|---|
arm | 32 | big | 857 | 13 |
arm | 32 | little | 730 | 89 |
i386 | 32 | little | 767 | 8 |
mips | 32 | big | 19472 | 629 |
mips | 32 | little | 9445 | 469 |
mips64 | 64 | big | 40 | 11 |
powerpc | 32 | big | 60 | 4 |
在仿真成功的程序中,程序数量如下表所示,因为每个固件中存在的程序数量与类型不同,所以在表中,不同程序的数量和比例有所不同。其中部分程序对应的比例较小,这是由于不是所有固件都带有对应的功能,例如只有小部分固件存在UPnP相关的服务程序,而大部分的固件中都存在DNS相关的服务程序。
程序名称 | 程序数量 | 去重后数量 |
---|---|---|
dnsmasq | 7610 | 147 |
hnap | 12 | 3 |
httpd | 6899 | 181 |
lighttpd | 90 | 42 |
miniupnpd | 84 | 26 |
smbd | 866 | 120 |
telnetd | 7728 | 399 |
tftpd | 144 | 43 |
udhcpd | 7894 | 245 |
utelnetd | 44 | 17 |
没有仿真成功的测试程序有几种原因,一种原因是部分程序在启动时会对系统环境做详尽的检查,如检查运行进程、检查系统各种参数,当有一些条件没有满足时程序会退出,由于有一部分检查在可执行程序内部完成,不涉及到外部的调用或函数,本系统的技术不能控制,导致本系统不能成功的仿真。一种原因是部分程序依赖的设备较为特殊,是本文尚未实现的设备,在这种条件下本文也不能很好的进行仿真。
本文通过对物联网设备模糊测试技术的研究,实现了对物联网设备中的网络应用程序进行仿真的目的。但是,本文的实现总体来说比较粗糙,有很多没有自动化或者不完善的部分,主要作为一种仿真的思路提出以供后来的研究者参考。
在完成仿真后,要继续的工作是对仿真成功的二进制文件进行模糊测试,在仿真的基础上,还需要解决三个问题。即如何对网络程序进行测试、如何获取覆盖率反馈信息、如何对格式敏感的程序进行测试,对应的文章会在后续放出。
由于当前代码结构比较混乱、缺少文档,目前没有开源的计划,如果感兴趣的朋友比较多,等整理好代码后可能会通过这个 repo 开源。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1634/