好像什么都学了 又好像什么都没学😀
评估嵌入式 IoT 设备安全性的一种方法是对其固件进行动态分析,如模糊测试。为此,现有的方法旨在提供一个仿真环境,以模拟真实硬件/外设的行为。然而,在实践中,这样的方法只能仿真一小部分固件映像。例如,目前最先进的工具 Firmadyne 只能运行我们从前八大制造商收集到的 1,124 个无线路由器/IP 摄像头映像中的 183 个(16.28%)。这种低仿真成功率是由于真实和仿真固件执行环境的差异所致。
在本研究中,我们通过对大规模数据集中的仿真失败案例进行分析,找出低仿真率的原因。我们发现,尽管具有不同的根本原因,普遍的失败案例通常可以通过简单的启发式避免,显著提高仿真成功率。基于这些发现,我们提出了一种技术——仲裁仿真,并将几种启发式系统化为仲裁技术,以解决这些失败。我们的自动化原型 FirmAE 成功运行了 892 个(79.36%)固件映像,包括 Web 服务器在内,比 Firmadyne 运行的映像数量显著多(≈4.8 倍)。最后,通过对仿真映像应用动态测试技术,FirmAE 可以检查 320 个已知漏洞(比 Firmadyne 多 306 个),并在 23 个设备中发现了 12 个新的零日漏洞。
预计到 2025 年,活跃的物联网(IoT)设备数量将达到 342 亿[36]。由于众多 IoT 设备连接到互联网[33],它们面临着网络安全威胁。例如,基于 Linux 的 IoT 设备(如无线路由器和 IP 摄像头)经常成为大规模攻击的目标。在互联网环境中,从这些设备中发现了多个后门[30,38],而像 Mirai 和 Satori 这样的恶意软件感染了数百万这样的设备[5,31,32,37]。
为了解决这些众多 IoT 设备中的安全问题,研究人员一直在专注于对这些设备的固件进行大规模分析。具体来说,一系列研究采取一种方法,即在虚拟硬件的仿真环境中运行设备固件,然后对固件进行动态分析[12,17,23-25,57,60,64]。采用这种方法,不仅可以在没有硬件的情况下动态分析固件,还可以利用云基础设施来扩展安全分析。在众多工具中,Firmadyne[17]是当前最先进的固件仿真框架,旨在通过提供完整的系统仿真环境,为普遍的 IoT 设备实现大规模仿真。
在实践中,这种方法并非万全之策,因为在全系统仿真环境中运行固件通常会因为真实环境和虚拟仿真环境之间的不一致性而失败。仿真环境中任何差异都可能导致固件执行进入意外状态,导致仿真和动态分析失败。解决这种仿真不一致性是具有挑战性的,因为这些不一致性源自于物联网设备硬件和配置的广泛多样性。尤其是,每个物联网设备都配备了来自众多制造商的特定硬件设备。此外,固件通常依赖于配置向量(configuration vectors),例如NVRAM中的数据,而仿真环境可能缺少这些数据,因为这些数据仅在硬件中可用。这种复杂的情况与Firmadyne的仿真环境不符。它的仿真器QEMU [6]仅支持少量通用设备和配置,而不进行大量努力来仿真每个设备和配置,则无法解决这个问题。 为了在实践中了解这个问题的影响,我们从前八个供应商处获取了1,124个无线路由器和IP摄像头固件图像,并使用Firmadyne对其进行了测试。结果令人震惊,因为它只能仿真其中的183个(见表1)。大部分固件图像(83.72%)没有进行分析。这样低的仿真成功率意味着,尽管Firmadyne旨在通过提供固件的全系统仿真环境来实现通用性,但这种方法在实践中不起作用,需要进行大量手动工作以解决仿真环境中的不一致性。
接下来,我们展示如何通过手动处理这些不一致性,作为Motivating examples。首先,我们在Firmadyne上运行D-Link DIR-505L的固件,以测试CVE-2014-3936 [16]漏洞。由于这个漏洞是在运行固件上的Web服务中的基于堆栈的缓冲区溢出,利用需要通过模拟环境的网络接口发送HTTP请求。然而,当我们在Firmadyne上运行固件时,尽管Web服务器正确运行,但我们无法连接到Web服务。通过我们的分析,我们发现固件中的网络配置与模拟环境不匹配,在我们强制配置网络后,我们能够触发漏洞。其次,我们在Firmadyne上运行NETGEAR R6250的固件,以测试CVE-2017-5521。在这种情况下,模拟失败,出现kernel panic。经过我们略微修改引导和与内核相关的配置以匹配虚拟环境后,我们能够运行固件并触发漏洞。
通过这两个例子,我们发现一个配置或设备设置的轻微更改,这很容易实现,可能使固件仿真运行而不会遇到难以处理的仿真不一致问题。因此,我们认为Firmadyne错过了许多仿真和分析IoT固件镜像的机会,不是因为仿真存在根本问题,而是由于设备设置失败,尽管这些可以轻松处理。为了解决这个问题,我们旨在通过分析许多仿真失败案例来系统化这些启发式方法,并最终提高成功固件仿真的机会,超过Firmadyne。
我们通过研究许多仿真失败案例作为第一步来实现这一目标。为了进行调查,我们收集了来自前八家供应商的1124个固件映像[59],包括1079个无线路由器和45个IP摄像头。在仿真中,我们专门关注无线路由器和IP摄像头的Web服务仿真。这是因为Web接口是远程攻击者可以与之交互的部分,并且在这些服务中已经发现了许多关键漏洞[5、7、12、32、51]。通过使用Firmadyne,我们调查了437个仿真失败案例(在AnalysisSet中的527个固件映像中),发现大多数案例属于以下五类问题:1)与引导相关的问题,例如不正确的引导顺序或文件缺失,2)与网络相关的问题,例如网络接口不匹配或配置不当,3)与非易失性RAM(NVRAM)相关的问题,例如缺少库函数或自定义格式,4)与内核相关的问题,例如不支持的硬件或功能,以及5)小问题,例如不支持的命令或时间问题。我们的调查结果表明,尽管这些问题的根本原因不同,但每个类别中的失败案例都可以通过应用简单的启发式方法来解决。例如,227个映像无法设置它们的网络接口,即使它们的Web服务器正在正确运行。尽管失败的根本原因可能不同,例如可用网络端口数量的差异、网络设备名称等,但强制设置在仿真环境中工作的网络配置的启发式方法可以解决问题并启用动态分析。
基于这一观察,我们将这些启发式方法系统化为一种技术,称为仲裁仿真,并开发了几种仲裁技术以绕过故障案例。仲裁仿真不是严格遵循固件的执行行为,而是在遵循原始行为和注入适当干预(即故意操作)之间进行仲裁。因此,它可能会轻微地改变固件的原始行为。然而,我们的目标不是建立与物理设备完全相同的环境,而是创建有利于动态分析的环境。实际上,我们的方法可以仿真许多以前无法仿真的固件映像,并有效地帮助发现真正的漏洞。
在设计了几种仲裁之后,我们自动化和并行化了整个固件仿真过程。在测试了1,124个固件映像的4小时内,我们的原型系统FirmAE成功仿真了892个(总数的79.36%),比Firmadyne多四倍以上(表1)。然后,我们在仿真的映像上运行以前已知的漏洞利用程序,以验证仲裁仿真是否有助于动态分析。结果,320个已知漏洞在FirmAE上成功仿真,比Firmadyne多了306个成功案例。我们还在FirmAE上建立了一个简单的fuzzer,并在95个最新设备中发现了23个独特的漏洞,并负责地向供应商报告了这些漏洞。
总之,我们的研究贡献如下:
为了分析嵌入式设备,可以获取目标固件并进行物理/非物理设备分析。
通常,固件可以从供应商的网站、FTP服务器或第三方存档中获取。这可以手动完成,也可以使用Web爬虫(如Spider [41])完成。固件也可以直接从设备中的闪存中转储[46],但这需要一个物理设备。然后解包固件镜像以供后续分析。一个镜像可以包括多个内容。例如,基于Linux的固件可能有引导加载程序、内核和文件系统。此镜像通常以各种方式压缩,如LZMA、ZIP或Gzip,以节省存储空间。为了解包镜像,通常使用诸如Binwalk [26]、Firmware-Mod-Kit [27]或FRAK [13]之类的工具。在给定的镜像中,这些工具扫描各种文件头的预定义签名。当签名匹配时,它们从镜像中提取文件,并继续扫描到结尾。还存在加密或定制镜像,无法使用签名匹配进行分析;本研究不涉及分析此类固件。
解包后的固件可以使用实际设备进行分析。Zaddach等人[62]和Marius等人[44]通过JTAG接口传递了进程执行和外围设备访问,并部分仿真了目标代码。同样,Kammerstetter等人[28, 29]开发了一个代理环境,使用实际设备并将字符设备访问转发给它们。Cui等人[14, 15]和Kumar等人[33]对连接到公共互联网的嵌入式设备进行了定量研究。
JTAG接口是一种通用的硬件接口标准,它被设计用于在数字集成电路(digital integrated circuits)中进行硬件调试、测试和编程。JTAG接口通常是由4条或更多线组成的,并与芯片的特定引脚相连。通过这个接口,可以读取和写入芯片内部的寄存器,以及对芯片进行在线测试和调试。JTAG接口在嵌入式设备的调试和测试中非常常见,因为它可以在不影响设备外部操作的情况下,对设备内部进行访问和控制。
另一些研究关注于在没有物理设备的情况下分析固件以扩大分析规模。研究人员采用静态方法对固件进行分析[11, 52],但由于缺乏运行时信息,通常会产生大量的假阳性。然而,Costin等人[11]展示了易受攻击的设备统计数据,这些设备具有易于破解的密码或后门字符串。Shoshitaishvili等人[52]使用符号执行找到了绕过认证的漏洞。相反,动态分析可以直接运行目标程序来识别漏洞而不会产生假阳性。然而,执行动态分析并不是一项简单的任务,因为需要仿真设备固件。最近的研究[12, 17, 23-25, 57, 60, 64]聚焦于固件仿真以克服无法获取真实硬件的困难,我们将在以下子节(§2.2)中更详细地描述这些研究。
Firmware emulation已经引起了人们的关注,因为它不需要实际的设备,并为动态分析提供了有用的接口。进行仿真的系统被称为主机系统,而被仿真的系统被称为客户机系统。通常,仿真有两个级别:用户级和系统级。
用户级仿真仅仿真固件内的目标程序,并充分利用主机系统。一个例子是仿真Web接口。Web接口是嵌入式设备中用于设备管理或维护的代表性服务。它提供多个静态内容,例如HTML,或由CGI程序生成的动态内容。尽管静态内容可以使用主机环境提供,但动态内容可能不行。这是因为它们可能与主机系统冲突,或依赖于主机系统中不存在的自定义库和设备驱动程序。
系统级仿真完全仿真了客户系统,包括内核。因为它提供了一个独立的执行环境,所以可以仿真内核和设备驱动程序中的各种特性。然而,固件仿真非常困难,因为需要考虑厂商特定的硬件问题或映射到内存的外设。如果不处理它们,仿真固件中的程序通常会崩溃。因此,最近的研究努力解决这些问题[12,17,23,25,57],通过创建一个尽可能类似于真实设备的仿真环境。流行的仿真器,如QEMU [6],已经支持更多的硬件类型,包括它们的外设。Costin等人[12]提出了一个可扩展的动态分析框架,以及针对各种嵌入式Web界面的几个案例研究。陈等人[17]模拟了非易失性RAM(NVRAM),该RAM存储了仿真固件中程序的各种配置值。Gustafson等人[25]模拟了外设通信中的内存映射I / O(MMIO)操作。Feng等人[23]试图用机器学习来解决同样的问题。最近,Clements等人[10]提出了将硬件与固件分离的方案。
在仿真之后,可以使用之前已知的 PoC 代码 [17] 或模糊测试器 [24, 60, 64] 来检查漏洞。TriforceAFL [24] 是一个针对 QEMU 映像的流行的模糊测试器,利用了美国模糊测试工具(AFL)[63]。它也被 Hu 等人 [60] 采用。在他们的后续研究中,Zheng 等人 [64] 提出了一种针对动态分析的优化仿真方法,该方法在系统级和用户级仿真之间切换上下文。
基于仿真的分析具有优势;然而,仿真来自不同供应商的固件映像时存在许多挑战,这些挑战源于非标准化的开发流程和仿真和物理环境之间的差异。例如,设备中的库、设备驱动程序甚至内核在供应商之间有所不同;除非这些被正确仿真,否则无法执行内部程序。 访问硬件接口的设备(例如 LED 传感器或相机)更具多样性,正如之前的研究[23,25]所指出的那样。主设备与其外设之间的通信通常使用预定义的内存映射IO(MMIO)操作和内存地址。然而,这些地址范围在不同设备之间有显著差异。因此,将此方法扩展到各种设备是困难的。陈等人[17]尝试在大规模上仿真其中一个硬件,即 NVRAM。Muench等人[45]强调了在进行动态分析以识别内存损坏漏洞时的设备特定挑战。 除非函数像物理设备中那样完美实现,否则解决这些挑战可能是不可行的。然而,研究仿真失败案例并解决已识别的问题有助于逐渐提高仿真率,并使动态分析改进物联网生态系统的安全性。因此,我们采用最先进的仿真框架 Firmadyne [17] 并调查失败案例。
Firmadyne[17]是一种先进的固件仿真框架,最初是为大规模分析设计的。许多研究[24、60、64]已经采用它进行动态分析。我们也利用Firmadyne进行了故障调查。 在解包固件映像后,Firmadyne使用一个定制的Linux内核和库进行仿真,这些内核和库预先构建,以支持各种硬件功能,如NVRAM。为了进行仿真,Firmadyne对目标映像进行了两次仿真:第一次仿真记录有用的信息,而第二次仿真则使用记录的信息。因此,定制内核包括一个驱动程序,它hook主要的系统调用来记录有用的信息。例如,它们hook住inet_ioctl()和inet_bind()来获取在仿真固件中使用的网络接口的名称和IP地址。Firmadyne的自定义库还解决了硬件问题。例如,一个名为libnvram的库基于硬编码的默认值存储和返回NVRAM值。 虽然Firmadyne很有前途,但它的网络可达性和Web服务可用性的仿真率都相当低,分别为29.4%和16.3%。因此,我们仔细研究了故障情况,并提出了一种技术来解决这些问题。
我们的目标是成功仿真嵌入式设备的固件映像,特别是运行其Web服务,因为此类设备的Web界面是远程攻击者的关键目标。[5, 12, 17, 32, 60, 64]。我们不旨在解决仿真环境中的所有差异。相反,我们旨在进行简明的仿真以进行动态测试,我们的仿真目标可以用以下属性说明:1)在没有任何内核崩溃的情况下启动,2)从主机实现网络可达性,3)Web服务可用性进行动态分析。我们的目标是保持这些属性,因为这些是运行Web服务所需的最低要求,而不会在固件仿真中遇到问题。因此,我们通过检查目标固件的网络可达性和Web服务可用性来检查仿真成功率。
在各种嵌入式设备中,我们选择无线路由器和IP摄像机作为分析目标,因为它们存在于我们的日常生活中,经常成为攻击目标。实际上,许多僵尸网络 [5,32] 以它们为目标,发起大规模的DDoS攻击。请注意,其他具有类似特征的嵌入式设备也可以使用我们的方法来解决。
为了实现这一目标,我们提出了一种技术,称为仲裁仿真。尽管之前的方法 [12, 17, 23, 25, 57] 一直致力于确保目标固件的操作类似于物理设备,这是一个困难的目标,但仲裁仿真并不完全遵循目标固件的原始执行过程。仲裁仿真的关键思想是,确保高级行为已足以在内部程序上执行动态分析,这相对容易做到,而不是找到并修复仿真失败的确切根本原因。这里提到的高级行为可以根据目标和仿真目标由熟练的分析师进行简单建模。在本研究中,我们使用 §3.1 中定义的模型。
仲裁仿真的一个关键特征是采用干预。干预表示有意添加的操作,可能与物理设备的行为不同。这个操作使得可以绕过未解决的问题,假设它们并不强烈影响仿真固件内部的目标程序的行为。调解执行仿真固件与应用干预之间的过程称为调解。可以根据给定高级行为模型的违反情况分析适当的调解点。然后,在这些调解点注入干预。由于干预专注于高级行为,因此从一小组固件映像获得的这些干预可以广泛应用于其他遭受类似故障的固件映像,即使它们有不同的根本原因。
我们的干预利用基于 Linux 的固件的抽象设计。我们对数据集进行了初步研究,并发现适当的干预可以帮助仿真器绕过许多未解决的问题。例如,当由于未知外围设备访问或不足的 NVRAM 支持而停止网络设置过程时,强制配置固定网络设置的干预可以解决该问题,而不管其根本原因如何。虽然仲裁仿真可能违反全系统仿真的主要概念,但我们假设干预引入的小差异仅对目标程序的行为产生轻微影响。实际上,我们通过成功在 1,124 个固件映像中运行仿真的 Web 服务,并通过进行动态安全分析发现了 12 个 0-day 漏洞来支持这个假设。
我们基于Firmadyne [17]实现了一个仲裁仿真的原型系统FirmAE,其总体架构如图1所示。FirmAE在一个预构建的定制Linux内核和库上,通过仿真来模拟一个与Firmadyne相似的固件映像,如第2.4节所述。它还对目标映像进行了两次仿真,以收集各种系统日志,并利用这些信息进行进一步仿真。我们将前一个仿真步骤称为预仿真,后一个步骤称为最终仿真。在FirmAE中应用的仲裁可以分为五类,这些仲裁是通过我们对AnalysisSet的故障案例调查得出的。我们在第4节中描述了每个仲裁的详细信息,并在第5.1节中将仿真结果与Firmadyne进行了比较。我们还在FirmAE中构建了额外的接口,用于动态分析(第5.3节),并在第5.4节中描述了分析结果。
为了进行大规模的分析,FirmAE需要完全自动化。自然,Firmadyne的许多步骤都已经自动化了,但仍需要一些用户交互。例如,用户必须首先使用特定选项提取目标固件的文件系统。然后,他们评估文件系统是否成功提取并检索架构信息。随后,他们为QEMU制作固件映像并在预模拟中收集信息。最后,他们运行一个脚本进行最终模拟并执行动态分析。我们自动化了所有这些交互,并添加了一个自动化的评估程序来评估网络可达性和Web服务可用性。为此,我们在FirmAE中构建了一个模块,定期运行ping和curl命令。
我们还使用 Docker [40] 的容器化技术并行化了仿真以有效评估大量固件图像。每个固件图像在各自的容器中独立进行仿真,容器中配备了所需的所有软件包和依赖项。这使得目标固件图像能够快速、可靠地进行仿真。FirmAE通过运行多个容器实例来并行仿真固件。
通过容器化,我们可以利用抽象化来优化主机和客户系统之间的网络连接。FirmAE 使用的 QEMU [6] 在主机系统中创建了一个额外的网络接口 TAP,该接口与一个客户网络接口相连。因此,每个仿真的固件都应该在主机系统中具有一个独立的 TAP 接口和一个唯一的 IP 地址,否则将发生网络冲突。容器化隔离了每个容器的网络环境。因此,即使在并行仿真中,主机系统的数据包也可以正确地路由到客户。我们还在每个容器内部放置了检查器和分析引擎。
我们的数据集包括无线家庭路由器市场前八大厂商 [59]。我们从厂商网站收集了1306个固件映像,并使用Binwalk [26]按照第2.1节的描述解包了这些映像,从中提取了文件系统。然后,我们通过验证每个映像的操作系统是否具有ARM小端(ARMel)、MIPS小端(MIPSel)和MIPS大端(MIPSeb)三种架构之一,对它们进行了过滤。这些架构占据了我们初始收集的数据的97%以上。我们以同样的方式准备了IP摄像机固件。我们的最终数据集包括1124个固件映像,其中1079个是无线路由器,45个是IP摄像机。我们将它们分成三个数据集:AnalysisSet、LatestSet和CamSet。它们的简要概述在表1中呈现,包括仿真结果,其详细版本在附录的表4中显示。AnalysisSet由3个厂商的526个过时映像组成,而LatestSet和CamSet只包含截至2018年12月的最新固件映像。LatestSet包括8个厂商的553个最新映像,包括AnalysisSet涵盖的厂商,而CamSet包括3个厂商的45个最新映像。因此,AnalysisSet可能包含每个设备的多个固件版本,而LatestSet和CamSet每个设备只有一个映像。数据集之间没有交集,即它们没有共同的相同映像。我们使用AnalysisSet来分析仿真失败案例。通过分析它们,我们发现了一些仲裁点,可以帮助增加仿真率(§4)。我们将这些仲裁点应用于FirmAE,并使用LatestSet和CamSet进行评估(§5)。
本文中的所有实验都是在一台服务器上进行的,该服务器配备了四个Intel Xeon E7-8867v4 2.40 GHz CPU,896 GB DDR4 RAM和4 TB SSD。我们在服务器上安装了Ubuntu 16.04,并使用了PostgreSQL v9.5.14和Docker v18.09.4。
仲裁仿真的关键是描绘可以帮助仿真器绕过故障的仲裁点。因此,我们首先基于高级行为模型分析了AnalysisSet上的失败案例。为了进行大规模分析,我们应用了FirmAE的自动化和并行化,没有任何仲裁,仿佛仿真部分与Firmadyne相同。值得注意的是,仅对16.9%的固件仿真了Web服务器(§5.1)。为了清晰的解释,我们将故障案例按照仲裁点进行分类:引导(§4.1)、网络(§4.2)、NVRAM(§4.3)、内核(§4.4)和其他(§4.5)。本节中,我们将对其进行详细说明。
注:确定仲裁点并设计适当的干预措施需要经验调查,我们认为我们的研究可以为未来研究做出贡献。
我们在启动过程的早期阶段就遇到了问题,这些问题使得仿真在内核崩溃时失败。
引起错误引导顺序的主要原因是系统初始化程序未正确执行。通常,大多数系统在引导过程中需要初始化。在Linux内核中,初始化通常由一个称为init的程序执行,内核会通过检查预定义的路径(例如/sbin/init、/etc/init和/bin/init)来尝试找到此程序。然而,一些固件镜像具有自定义的初始化程序路径,导致内核无法执行程序并崩溃。
这种故障经常发生在 NETGEAR 固件映像中。在分析了这些映像之后,我们发现它们使用了一个名为 preinit 的名称,该名称通常由一个开源嵌入式设备项目 OpenWrt 使用,并且我们验证了它们确实基于 OpenWrt 实现。我们还发现,一些 TP-Link 映像也使用了 preinit。
为了解决这个问题,Firmadyne 构建了一个脚本,用于搜索和执行频繁访问的硬编码文件列表以初始化程序。但是,这些候选项不足以解决初始化程序路径的多样性。
我们提出了另一种方法,利用目标固件的内核中的信息。具体而言,我们在引导过程的开头创建了一个干预措施,该措施提取了映像内核中的有用信息。具体而言,我们利用内核的命令行字符串,该字符串用于引导过程中内核的默认配置。请注意,这样的字符串在开发阶段是预定义的,因此它自然嵌入在内核映像中。这些信息可能包括初始化程序路径、控制台类型、根目录、根文件系统类型或内存大小。例如,在 NETGEAR 固件中的一个内核映像中,我们可以获得一个字符串console=ttyS0,115200 root=31:08 rootfstype=squashfs init=/etc/preinit。我们可以识别 /etc/preinit 的初始化程序路径,控制台类型为 ttyS0,波特率为 115200,并且根文件系统类型为 squashfs。通过使用从原始内核中获取的信息配置仿真环境,即使初始化程序具有不寻常的路径,也可以正确地初始化guest系统。如果我们无法提取任何信息,则从提取的文件系统中找到初始化程序,例如 preinit 或 preinitMT。
其他失败案例发生是由于缺少文件或目录。当内部程序访问这些路径时,它们会崩溃,仿真停止。Firmadyne尝试通过创建和挂载硬编码的路径,如proc、dev、sys或root在自定义引导脚本的开头,来解决这个问题。一些硬编码的路径确实起作用;例如,使/etc/TZ或/etc/hosts帮助解决了几个此类故障。但是,这种方法无法解决多种情况。此外,由于它在固件初始化之前强制创建文件和目录,因此与内部程序发生冲突,这些程序在相同的路径中创建和挂载其他文件或目录。
我们通过插入一种干预措施来解决这个问题,这种干预措施与之前的案例类似,但是它从文件系统中检索信息而不是内核。在仿真给定的映像之前,我们从其文件系统中提取所有可执行二进制文件中的字符串。然后,我们筛选它们以获取高度可能指示路径的字符串,并根据这些路径准备文件结构。特别是,我们选择以一般的Unix路径开头的字符串,例如/var或/etc。
在引导过程完成后,应配置网络,以便主机系统可以与客户系统进行通信,并最终执行动态分析。对于网络通信,QEMU要求主机创建一个附加的网络接口TAP。此TAP接口连接到客户系统中的网络接口。然后,主机和客户通过它进行通信。
然而,正确配置TAP接口并不是微不足道的,因为它应该设置具有对应于目标网络接口类型的特定选项。这种网络接口类型可以是以太网、无线局域网(WLAN)、网络桥或虚拟局域网(VLAN)。在客户系统中静态区分接口类型并不容易,需要对目标镜像进行一次仿真。
Firmadyne对给定的镜像进行两次仿真(§2.4)。在第一次仿真中,即预仿真,Firmadyne通过钩取系统调用收集内核日志。由于收集到的日志包括仿真期间访问的网络接口的名称和IP地址,因此它们可以在最终仿真中用于网络配置。尽管如此,许多镜像仍然会发生故障。
将多个IP地址分配给单个网络接口称为IP别名[58]。它在路由器中很常见,因为它可以通过IP地址分别管理服务。在IP别名中,网络接口会生成其多个实例,每个实例都分配一个唯一的IP地址。例如,具有IP地址192.168.1.1的桥接接口br0可以具有IP别名169.254.39.3和1.1.1.1,分别分配给其实例br0:0和br0:1。然后,br0链接到以太网接口eth0。在这里,可以使用任何这些IP地址访问br0。与此IP别名相关的故障案例经常出现在D-Link镜像中。在调查后,我们发现这些故障是由于Firmadyne未正确处理IP别名所致。该问题发生在主机系统中Firmadyne网络配置期间。在预仿真步骤中,内核会记录IP别名。然后,Firmadyne解析日志,并尝试将所有记录的IP地址分配给客户机中相应的接口。然后,它添加静态路由规则,将这些IP地址链接到主机中的TAP接口。在这里,多个路由规则被添加到单个TAP接口中,这会导致网络冲突。 有了IP别名的知识,FirmAE通过让主机系统使用其默认路由规则来仲裁这个问题。特别地,即使使用IP别名,一旦将客户端的网络接口链接到主机的TAP接口,所有数据包都会自动路由在主机和客户机之间。因此,这些情况不需要干预,这证明了在正确的情况下放置干预的重要性。
有些固件映像不包含任何可连接网络接口(如eth)的信息,只配置环回接口(lo),而没有设置其他网络接口。由于缺乏可连接网络接口,这些映像无法从主机系统中访问。此外,一些映像尝试将其Web服务器绑定到不存在的网络接口上,并因此崩溃。 经过分析,我们发现一些映像使用动态主机配置协议(DHCP)从DHCP服务器检索其WAN接口的IP地址。DHCP是在终端设备中设置网络接口的流行协议,因为它不需要任何用户交互。通常,无线路由器本身充当DHCP服务器,为其客户端连接的LAN接口分配IP地址。但是,它们也可以从外部DHCP服务器检索IP地址,将其WAN接口连接到Internet,除非用户手动配置了它。实际上,我们分析的映像尝试通过其WAN接口与主机系统的TAP接口之间的连接使用DHCP检索IP地址。但是,由于模拟环境中不存在DHCP服务器,模拟的固件无法获取IP地址和配置网络接口。此外,由于未配置任何网络接口,无法配置将多个网络接口分组的桥接接口。因此,绑定到这些网络接口的内部程序无法正常运行。 我们首先尝试使用QEMU的内部DHCP服务器解决这个问题,以便客户机的网络接口可以从服务器检索IP地址。但是,即使设置了DHCP服务器,一些映像仍然没有网络接口。这可能是由于外围设备的支持不足引起的。如果在网络配置期间的任何程序访问这些外围设备,则会崩溃或表现异常,并最终无法配置网络。 FirmAE通过强制使用默认设置的干预来解决这些问题。具体来说,我们设置一个以太网接口eth0,其IP地址为192.168.0.1。在设置以太网接口后,它与默认桥接接口br0相连接,对于那些内核日志包含桥接接口信息的映像。这种简单的干预显着有助于模拟Web服务(§5.1)。
为了支持多个网络接口,必须选择一个适当的机器来加载目标固件。我们选择了virt,它是QEMU支持的机器之一,遵循了先前研究中使用的方法[17]。这对于几个固件映像表现良好;然而,它无法模拟具有多个网络接口的ARM固件映像。Firmadyne试图通过准备一个固定数量(四个)的虚拟接口来解决这个多接口问题。其基本假设是接口名称的后缀不小于或等于接口数量,该后缀是从内核日志中提取的。例如,如果记录了eth1,则很可能也存在eth0。然而,几乎所有的ARM映像仍然无法被模拟。 我们仔细研究了这些情况,但无法确定确切的原因。然而,我们可以通过一种高级干预来解决这个问题,该干预仅强制设置一个以太网接口。更具体地说,我们的干预强制设置一个以太网接口eth0,并避免设置其他接口。因此,我们设置了一个桥接网络接口,并在需要时将其链接到主机。通过这种干预,大部分ARM固件映像都可以被模拟。
VLAN是路由器的典型功能,它提供了一个隔离的网络环境,将子网络逻辑地分组。VLAN接口与其他网络接口(如以太网或WLAN)具有不同的特征,因此必须使用附加选项进行设置。为了支持VLAN,TAP接口的类型应设置为VLAN,并分配适当的VLAN id。 在具有VLAN接口的固件镜像中,另一个失败情况发生了。尽管以独立的IP地址正确配置了以太网接口,但无法访问客户网络。Firmadyne尝试通过在设置主机TAP接口时运行命令来解决这个问题,但它们的配置不足以处理这个问题。特别是,VLAN应该设置为使用相同的VLAN id将主机和客户网络分组。然而,Firmadyne未设置主机网络。FirmAE通过正确配置VLAN来解决这个问题。
许多路由器设计时都设置了防火墙,以防止未经授权的远程访问。否则,攻击者可能会访问管理接口。我们数据集中的一些固件镜像也通过使用iptables实现了这一策略。因此,客户机内核会丢弃来自主机的所有数据包。我们在TP-Link的大部分案例中发现了这种情况,即使主机和客户机网络已经正确配置,客户机也是不可达的。这不表示仿真失败,因为设置iptables可以模拟实际设备的原始行为。但是,这种过滤阻止了对潜在漏洞和威胁的分析。显然,在分析过程中发现的漏洞可能无法远程利用。尽管如此,许多设备所有者或管理员错误地更改这些规则,使设备变得对公共访问可用。
FirmAE通过检查客户机系统中的过滤规则并删除它们来解决这个问题。这可以通过清除iptables中的所有策略并将默认策略设置为接受所有传入数据包来简单地完成。然后,客户机网络就可以从主机中访问,并进行动态分析。
在固件仿真中,模拟类似于真实环境的外围设备是最具挑战性的部分之一(§2.3)。 NVRAM,本质上是一种闪存,是嵌入式设备中广泛使用的外围设备之一,用于存储配置数据。嵌入式设备中的内部程序通常在其中存储/提取必要的信息。除非支持NVRAM,否则这些程序经常会崩溃。 Firmadyne实现了一个自定义的NVRAM库来模拟与NVRAM相关的功能。通过设置名为LD_PRELOAD的环境变量,提前加载此自定义库以包含其他库。这会截获NVRAM相关的函数,例如nvram_get()和nvram_set(),并模拟没有物理访问的NVRAM。具体来说,当调用nvram_set()时,键值对存储在文件中,稍后在调用nvram_get()时获取。对于这些情况,在调用nvram_set()之前调用nvram_get(),Firmadyne使用给定固件中的默认文件初始化键值对,这些默认文件通常用于设备的出厂重置功能。Firmadyne有一个硬编码的默认文件路径列表,用于提取键值对。然而,我们的数据集中仍有许多固件镜像无法仿真。
我们发现许多情况下,每个设备的默认文件路径不同,甚至它们的键值对也有不同的模式。例如,在一些D-Link镜像中,默认文件位于 /etc/nvram.default 或 /mnt/nvram_rt.default。此外,一些NETGEAR镜像中的默认文件位于 /usr/etc/default。这些文件中的键值对用不同的分隔符分隔,例如回车符或 NULL 字节。有些默认文件甚至有供应商特定的格式,如 OBJ 或 ELM。
为了开发可扩展的方法,FirmAE在预仿真期间准备仲裁。具体来说,FirmAE在预仿真期间记录了所有通过 nvram_get() 和 nvram_set() 函数访问的键值对。然后,它扫描目标固件的文件系统,并搜索包含多个记录的键名称实例,其值未知的文件。FirmAE从文件中提取键值对(如果存在),并在最终仿真中使用它们。
不幸的是,并非所有的固件映像都有默认的NVRAM文件。即使存在默认文件,它可能也不包含所请求的键值对。解决这个问题的一个简单方法是像Firmadyne一样返回空值以表示未初始化的键。然而,我们观察到许多情况下,nvram_get()返回NULL后会崩溃。通过逆向工程崩溃的程序,我们发现,出乎意料的是,许多程序不验证nvram_get()的返回值。它们只是将返回值传递给字符串相关函数,例如strcpy()或strtok(),然后由于空指针解引用而崩溃。FirmAE通过调解nvram_get()函数的行为来处理此问题。与其在访问未初始化的键时返回空值,FirmAE返回一个指向空字符串的指针。这个简单的改变显著降低了崩溃,特别是在NETGEAR映像中。因为我们无法在没有物理设备的情况下获得真正的键值对,所以这将是避免许多内部程序中因错误处理不足而导致的崩溃的最佳方法之一。
嵌入式设备中的许多程序通过内核中的设备驱动程序与外围设备进行协作。通常,它们使用 ioctl 命令与外围设备进行通信。不幸的是,模拟这个过程并不是一件简单的事情,因为每个设备驱动程序都具有独特的特征,这取决于其开发人员和相应的设备。尽管 Firmadyne 实现了一些虚拟内核模块,支持 /dev/nvram 和 /dev/acos_nat_cli,但它无法覆盖实际场景中固件映像的多样化特征。我们的数据集中的许多固件映像也因为这个问题而崩溃。
因为Firmadyne实现的虚拟模块有硬编码的设备名称和ioctl命令,所以在访问具有不同配置的内核模块时,一些程序会出现失败的情况。例如,许多NETGEAR镜像使用称为acos_nat的模块与安装在/dev/acos_nat_cli上的外围设备进行通信。在这些镜像中,Firmadyne的一个模块返回不正确的值并导致httpd的Web服务陷入无限循环。此外,我们发现ioctl命令因固件架构而异,因此也应该考虑这一点。
FirmAE的高级方法利用虚拟化一个特定的内核模块。这里的关键直觉是,许多内核模块是通过共享库访问的,这些共享库有发送相应ioctl命令的函数。因此,FirmAE拦截库函数调用,类似于处理NVRAM问题(§4.3)。当程序调用库函数时,FirmAE返回预定义的值。因此,每个ioctl命令不需要根据设备架构进行虚拟化。在这个例子中,我们只关注了acos_nat,而通过共享库进行其他外围访问也可以用同样的方式处理。
我们发现一些固件镜像面临内核版本问题。Firmadyne在固件仿真中定制了Linux内核v2.6.32。然而,近期的嵌入式设备使用了更新的内核版本。升级内核版本似乎是解决这个问题的一个微不足道的方法。实际上,我们实验性地测试了Linux内核v4.1.17,并成功地仿真了更多的固件镜像。然而,一些固件镜像,特别是旧的固件镜像,无法使用新内核版本进行仿真。这些镜像在libc库中崩溃。我们调查了这些情况,并确定Linux内核v4.1.17的地址空间布局随机化与旧版本的libc不兼容。为了解决这个问题,我们在编译新内核时使用了兼容性选项。具体来说,我们设置了CONFIG_COMPAT_BRK选项,该选项排除在堆内存中随机化brk区域。使用这个新内核,FirmAE能够处理上述情况。其他兼容性问题可能存在,这些问题在我们的实验中没有被检测到。为了解决这些问题,应进一步测试具有各种编译选项的多个内核版本,这是我们未来研究的目标之一。
其他一些小干预可以解决一些失败的情况。
为了进行 Web 服务的动态分析,我们需要实现网络可达性和 Web 服务的可用性。在某些镜像中,即使网络已成功配置,Web 服务器仍未运行。我们无法找到这种现象的确切根本原因。但是,强制执行 Web 服务器的干预可以解决此问题。具体来说,它在目标固件的文件系统中搜索广泛使用的 Web 服务器(如 httpd、lighttpd、boa 或 goahead)及其相应的配置文件,并执行它。
对于长时间不响应的固件镜像,需要强制停止仿真。因此,设置适当的超时时间是必要的。Firmadyne应用了60秒的超时时间;但是,来自NETGEAR等厂商的固件镜像需要很长时间才能完成引导过程,从而最终阻止了它们的仿真。我们调查了这些情况,并经验性地找到了一个适当的超时时间为240秒。尽管这个更改很简单,但是超过60个固件镜像被成功仿真。
嵌入式设备开发者通常会省略不必要的功能以节省存储空间。因此,固件镜像可能没有适当的工具来仿真自身。由于仿真环境没有任何存储限制,我们可以在其中添加几个所需的工具。为了成功仿真,应该在文件系统中准备几个Linux命令,例如mount或ln。我们通过将最新版本的busybox添加到目标固件的文件系统中来解决这个问题。这个简单的添加使得必要的命令得以实现,从而导致成功的仿真。
通过对 AnalysisSet 的调查,我们发现了几个仲裁点(§4)。在本节中,我们使用我们的原型 FirmAE(§3.3)对每个仲裁点进行评估。为此,我们使用 Python 和 shell 脚本实现了总计 3671 行代码。我们还介绍了 FirmAE 动态分析中发现的漏洞。
我们比较了FirmAE和Firmadyne在每个数据集上的仿真率(§3.4)。由于FirmAE支持完全自动化和并行化(§3.3),所有数据集的总仿真时间不到四个小时(14289秒)。
我们比较了 FirmAE 和 Firmadyne 在每个数据集(§3.4)上的仿真率。所有数据集的仿真总时间不到四个小时(14289秒),因为 FirmAE 支持全自动化和并行化(§3.3)。
总体而言,仿真率从 16.28% 显著提高到 79.36%(增长了487%)。由于我们的研究基于 AnalysisSet,因此它显示的最高仿真率为 91.83%。LatestSet 和 CamSet 的仿真率与 Firmadyne 相比也有了很大的提高,并且我们能够在其中识别出漏洞(§5.3)。在 AnalysisSet 中,由于解决了 ARM 网络问题,NETGEAR 镜像的仿真率增长最多,从 10.95% 增长到 93.80%(增长了857%),因为大多数 NETGEAR 镜像都是基于 ARM 的。在 LatestSet 中,TRENDnet、ASUS、Belkin 和 Zyxel 的仿真率都低于60%;这些较低的仿真率归因于这些图像中较多的内核模块以及使用自定义硬件接口。我们在 §5.2 中详细描述了这一点。
CamSet 的仿真率表明,解决无线路由器的故障问题也可以帮助仿真 IP 摄像头。特别是,Firmadyne 无法仿真任何 D-Link镜像,而 FirmAE 可以仿真超过 65% 的镜像。然而,FirmAE 无法仿真所有 TP-Link 镜像。我们调查了这些失败的案例,并发现它们不包含 Web 服务器。CamSet 的结果表明,许多 IP 摄像头与无线路由器具有类似的特征,因此可以将无线路由器的仲裁方法应用于 IP 摄像头。

我们还通过从应用所有仲裁的FirmAE的最终版本中省略特定仲裁来研究每个仲裁的有效性。这是因为众多仲裁点应该合作解决故障,并且直接减少特定仲裁会直接影响仿真率。图2展示了这些结果,附录中的表5提供了详细版本。
NVRAM仲裁似乎是最有效的,平均降低了所有数据集的仿真率35%。这与Firmadyne专注于仿真NVRAM的方法相一致。删除启动和网络仲裁也会将仿真率显着降低约30%。没有内核仲裁,所有数据集中只有4.88%的固件映像受到影响。其他仲裁影响了22.35%的固件映像。这些结果表明,所提出的仲裁确实对于成功的固件仿真是有效和可扩展的。

在进行大规模仿真之后,我们调查了无法通过简单的仲裁轻松解决,但需要更复杂的虚拟化来解决的未处理故障问题。
如前面的研究所述[12, 17, 23, 25, 57],模拟内核模块是具有挑战性的,因为1)不同的内核版本经常会产生兼容性问题,2)有些固件映像可能没有内核,因此无法获取有用的信息。在一些情况下,Web 服务器和其他程序访问/proc目录下的内核模块。由于模拟环境中不存在这样的文件,这些程序通常会崩溃。例如,TP-Link固件映像中的Web服务器在/proc/simple_config/system_code处访问内核模块进行配置,并随后崩溃,因为该模块不存在。
固件的一些内部程序使用自己专用的接口进行外设通信,这使得仿真外设接口更加困难。例如,我们hook住了一些流行的库调用来仿真NVRAM。然而,D-Link固件的某些程序调用/bin/flash直接访问/dev/nvram。同样,一些TP-Link固件图像中的httpd服务器访问闪存/dev/ar7100_flash_chrdev以检索设备配置信息。同时,Linksys固件中的webs Web服务器直接操作/dev/mtd接口。它们甚至验证设备的完整性,并验证给定固件的签名和版本。
虽然Web服务器是可访问的,但其中一些很少响应服务器错误,即500 Internal Server Error。这种错误有几种原因,例如CGI程序中的语法/代码错误,无效的Web界面配置和PHP错误。但是,大多数错误情况都是由于后端CGI程序崩溃引起的。我们通过反向工程分析了CGI程序,并发现它们具有硬件接口相同的问题。因此,它们尝试访问/proc或/dev下的条目以获取配置值,并在失败时异常停止。上述情况展示了在没有物理设备的情况下模拟外围通信的困难。解决这些问题需要更复杂的仿真环境,这将在未来的研究中进行探讨。
在成功仿真固件图像之后,我们对它们的Web服务进行动态分析,即模糊测试。通过这个评估,我们1)验证了经过仲裁的仿真确实适用于应用嵌入式设备的动态安全分析,以及2)评估了互联网嵌入式设备安全的当前状态。我们的目标是针对最新固件的LatestSet和CamSet。
针对大规模分析,我们的重点在可扩展性上。因此,我们的动态分析工具需要适用于不同的仿真固件图像,且需要尽可能少的用户交互。在这些标准的基础上,我们首先搜索现有的工具[9、19、21、34、39、43、53、55、56、64],并检查它们是否适用于 FirmAE。然而,现有的工具不符合我们的标准,因为它们 1) 不是公开可用的,2) 对于大规模分析缺乏可扩展性,3) 不能发现新的漏洞。例如,Firmadyne [17] 利用 Metasploit [43],检查已知的漏洞。其他网络扫描器,如 Burp Suite [55]、Arachni [34] 或 Commix [53] 只检查预定义的 HTTP 模式的组合。因此,它们对于实际场景中的各种固件 Web 服务来说是不够充分的。此外,它们没有设计用于发现内存破坏漏洞,例如缓冲区溢出或use-after-free。与此同时,最先进的模糊测试工具 Firm-AFL [64] 是一个检测内存破坏漏洞的有前途的工具。然而,它不适用于大规模分析,因为它需要为目标程序进行单独的环境设置。由于这些限制,我们建立了自己的分析引擎。开发分析引擎本身是一个正交的研究领域,在这里我们只提出了一个概念性设计。我们的概念也可以应用于上述工具。
我们的分析引擎由两部分组成:如果需要,它会自动初始化并登录到网页,并识别包括内存破坏漏洞在内的漏洞。为了发现 1-day 漏洞,我们利用了 RouterSploit [56],该工具具有先前已知漏洞的概念证明(PoC)代码。我们还添加了一些自定义的 PoC 代码。为了分析 0-day 漏洞,我们开发了一个简单的 Web 模糊测试工具,用 Python 编写了 880 行代码。
初始化Web服务是动态分析的首要步骤,除非它们不接收任何其他请求。我们的数据集中有很大一部分Web服务需要在管理页面中进行网络和安全配置(例如管理员或AP密码)。然而,每个固件的初始化过程也不同。大多数DLink、TP-Link、Belkin、Linksys和ZyXEL固件镜像中的Web服务器在成功仿真后会自动初始化,而ASUS和TRENDnet的固件镜像则需要亲自初始化。幸运的是,其中许多有一个跳过按钮来配置默认选项。有些Web服务没有显式的跳过按钮,但有内部JavaScript函数可以行为相同。与此同时,有些需要手动输入管理员密码。
为了自动处理初始化,我们分析了Web服务的初始化过程,并从中提取了代表性的模式,包括按钮和菜单。然后,我们利用这些模式自动化这个过程。在这里,我们利用了Selenium [50],这是一个开源工具,可以提供类似真实浏览器的接口。
在成功运行固件映像及其 Web 服务后,引擎首先利用 RouterSploit [56] 和我们的定制 PoC 代码检查 1-day漏洞。由于 RouterSploit 包含多个已知漏洞的利用程序,在此评估中,我们可以 1) 检查目标设备是否已打补丁,以及 2) 找到一个新的已知漏洞的易受攻击设备。为了发现 0-day 漏洞,我们的引擎首先搜索目标固件的文件系统,并通过检查文件扩展名(例如 .html、.aspx 或 .xml)生成一个 Web 页面候选列表。然后,它从候选页面中提取可能的参数,并生成请求以检测漏洞。例如,对于 .htm 和 .html 候选页面,我们的引擎解析 HTML 标签,如 script、form 和 input,以提取目标 URL、方法和参数信息。此方法特别有助于构建针对使用家庭网络管理协议(HNAP)的设备的请求;HNAP 请求基于 XML 格式,并在 .html 页面的 javascript 代码中设置默认值。通过利用提取的信息,我们可以构建用于模糊测试的有效请求模板。由于我们从文件系统中搜索候选项,因此我们还可以检查通过爬行无法访问的 Web 服务。
在各种类型的漏洞中,我们关注命令注入和缓冲区溢出,因为它们经常出现在嵌入式设备中。为了检测命令注入漏洞,我们的引擎发送有效负载,它们实质上是候选字符的组合,例如“'”、“"”或“&”,后跟执行我们的可执行二进制文件的 shell 命令。我们将此二进制文件放置在日志中,记录有用的信息,例如时间和环境变量,从而检查是否触发了漏洞。我们还钩取了 execve 系统调用,以轻松检测我们的输入是否被注入到命令中。对于缓冲区溢出检测,FirmAE 在发生崩溃时提供反馈。请注意,由于需要处理请求的时间,我们必须在发送请求后等待;我们经验性地确定 10 到 15 秒足够了。我们还利用边界值,例如用于模糊输入的大型缓冲区,因为它们更有可能触发漏洞。我们的分析引擎报告的任何漏洞都必须得到验证。为此,我们将调试程序,例如 strace、gdb 和 gdbserver,添加到目标固件的文件系统中。请注意,我们可以利用 ptrace 系统调用进行调试,因为我们升级了内核版本(§4.4)。我们还添加了 netcat 和 telnetd 来访问 guest shell。使用这些工具,我们手动验证了识别出的漏洞。

为了评估仲裁仿真的有效性,我们对每个仿真的固件映像执行了动态分析,其中的Web服务已经由我们的引擎初始化。特别地,在目标固件映像的Web服务由FirmAE和Firmadyne各自初始化后,我们运行了先前已知的PoC漏洞利用。我们首先在AnalysisSet中使用RouterSploit [56]测试已知的漏洞,并列出了FirmAE和Firmadyne的结果,如表2所示。如果不使用任何仲裁(即Firmadyne),我们只能检查14个映像的漏洞,其中9个是唯一的设备。通过应用所有提出的仲裁(即FirmAE),我们可以检查320个映像的漏洞,其中128个是唯一的。由于FirmAE旨在仿真Web服务(§3.1),因此所有已识别的漏洞都位于Web服务中,例如SOAP CGI,UPnP和HNAP。这个结果表明,FirmAE的成功仿真有助于在动态分析固件映像方面胜过Firmadyne。 此外,我们在LatestSet和CamSet中的最新映像上进行了包括模糊测试在内的动态分析。结果,我们在95个唯一设备中发现了23个唯一漏洞。这包括表3中列出的11个1-day漏洞和12个0-day漏洞。 对于模糊测试,当同时运行50个映像时,每个模糊请求平均需要10-15秒,每次找到漏洞的平均时间为70分钟,最大时间为150分钟。模糊测试吞吐量可以根据系统规格和并行仿真实例的数量而变化。 有趣的一点是,一些供应商共享相同的漏洞。例如,D-Link和TRENDnet的某些设备具有信息泄漏的相同漏洞,以及UPnP和SOAP CGI程序的命令注入漏洞。相反,一些NETGEAR设备与Xiongmai的设备共享路径遍历漏洞。另一个要点是,对目标Web服务的分析可能会揭示与其相关的其他程序的漏洞。具体而言,当我们发送一个长的负载以检测缓冲区溢出时,一个目标CGI程序会将负载存储在文件中。然后,另一个读取写入的文件的程序由于溢出的负载而崩溃。这种漏洞只能在全系统仿真环境中找到,因为用户模式仿真不考虑文件系统关系。
总之,结果表明FirmAE在漏洞分析方面是可行的。我们相信仍然存在未发现的漏洞,这应该在未来的研究中进行调查。
我们发现的12个0-day漏洞分布在四个供应商之间。我们在2019年12月向所有供应商报告了这些漏洞,最长等待他们回复的时间为九个月
FirmAE的目标不是消除真实环境与仿真环境之间的差异,而是旨在运行固件的Web服务器并正确地提供Web接口。这可能导致与在硬件上运行固件不同的行为。然而,为了应用动态安全分析,我们需要检查的是1)易受攻击的程序是否运行,2)是否接受恶意输入,以及3)在程序中触发漏洞。虽然仿真可能不正确,但如果我们可以运行固件的Web服务,通过网络发送攻击数据包并验证是否成功执行攻击,则这三个项目是可检查的。由于我们的仲裁仿真可以支持这些,FirmAE发现的漏洞是合法的,并且在真实设备中也有效。
尽管我们的仲裁仿真启发式算法针对当前固件映像的失败情况表现更好,但因为我们的算法是通过经验来处理的,所以我们的系统化仲裁仿真只能处理观察到的情况,并不适用于新设备和新配置。在这方面,我们认为进行经验调查以寻找这样的干预措施是必不可少的,以处理物联网设备及其配置的复杂性。为了鼓励未来的研究,我们发布我们的代码,相信我们的经验性发现可以作为参考。
在本研究中,我们开发了一个简单的分析引擎,自动初始化、登录和分析 Web 服务进行动态分析。然而,可以通过应用其他有前途的技术进一步改进每个步骤。例如,可以使用符号执行来分析并绕过登录过程[52]。此外,采用其他模糊测试策略[8,48]、混合分析方法[54,61]或相似性技术[18,20],可能会发现更多的漏洞。我们将这样有前途的动态分析引擎的改进作为未来的工作。
仲裁仿真还可以用于构建用于分析针对物联网设备的众多攻击的蜜罐。事实上,已经有几项利用仿真的蜜罐研究[35, 47, 49, 57]。特别地,Vetterl等人[57]提出了一个名为Honware的基于固件仿真的蜜罐,类似于FirmAE的方法。由于蜜罐应该与网络外的攻击者进行交互,因此作者专注于通过研究仿真故障案例来增加网络可达性率。因此,FirmAE的网络干预,即配置默认的网络设置,与Honware的方法非常相似。然而,FirmAE包括额外的干预措施,以运行Web服务以积极分析其中的漏洞,这些干预措施甚至进一步增加了仿真率(表5)。因此,我们相信仲裁仿真也可以用于构建物联网蜜罐。
嵌入式设备安全性分析受到了相当的关注。在本研究中,我们调查了一个大规模的固件数据集,并发现固件仿真可以从简单的干预措施中获得实质性的好处。我们提出了仲裁仿真和可以解决高级别失败问题的干预措施。通过一个原型系统 FirmAE,我们展示了该方法可以将最先进的框架的仿真率提高了 487%。我们还对仿真固件进行了动态分析,并发现了23个独特的漏洞,其中包括12个 0-day 漏洞
bash$ git clone --recursive https://github.com/pr0v3rbs/FirmAE
bash$ ./download.sh
bash$ ./install.sh
bash$ ./init.sh
bash$ wget https://github.com/pr0v3rbs/FirmAE/releases/download/v1.0/DIR-868L_fw_revB_2-05b02_eu_multi_20161117.zip
bash$ sudo ./run.sh -c <brand> <firmware>
bashdu4t@ubuntu:/media/psf/Home/Desktop/博客文章/Paper/FirmAE/FirmAE$ sudo ./run.sh -c D-Link ./DIR868L_B1_FW205WWb02.bin
[*] ./DIR868L_B1_FW205WWb02.bin emulation start!!!
[*] extract done!!!
[*] get architecture done!!!
mke2fs 1.44.1 (24-Mar-2018)
e2fsck 1.44.1 (24-Mar-2018)
[*] infer network start!!!
[IID] 1
[MODE] check
[+] Network reachable on 192.168.0.1!
[+] Web service on 192.168.0.1
[*] cleanup
======================================
bash$ sudo ./run.sh -a <brand> <firmware>
bash$ sudo ./run.sh -a <brand> <firmware>

下载相关的内核和应用程序等
bash#!/bin/sh
set -e
download(){
wget -N --continue -P./binaries/ $*
}
echo "Downloading binaries..."
echo "Downloading kernel 2.6 (MIPS)..."
download https://github.com/pr0v3rbs/FirmAE_kernel-v2.6/releases/download/v1.0/vmlinux.mipsel.2
download https://github.com/pr0v3rbs/FirmAE_kernel-v2.6/releases/download/v1.0/vmlinux.mipseb.2
...
继续安装一些程序以及对PostgreSQL数据库进行一些配置
bash#!/bin/bash
sudo apt-get update
sudo apt-get install -y curl wget tar git ruby python python3 python3-pip bc
sudo python3 -m pip install --upgrade pip
sudo python3 -m pip install coloredlogs
# for docker
sudo apt-get install -y docker.io
# postgresql
sudo apt-get install -y postgresql
sudo /etc/init.d/postgresql restart
sudo -u postgres bash -c "psql -c \"CREATE USER firmadyne WITH PASSWORD 'firmadyne';\""
sudo -u postgres createdb -O firmadyne firmware
sudo -u postgres psql -d firmware < ./database/schema
echo "listen_addresses = '172.17.0.1,127.0.0.1,localhost'" | sudo -u postgres tee --append /etc/postgresql/*/main/postgresql.conf
echo "host all all 172.17.0.1/24 trust" | sudo -u postgres tee --append /etc/postgresql/*/main/pg_hba.conf
...
重启PostgreSQL数据库
bash#!/bin/bash
set -e # 如果任何命令的执行结果不是0 则立即退出脚本
set -x # 在脚本执行期间打印所有执行的命令及其参数
# Start database
sudo service postgresql restart
echo "Waiting for DB to start..."
sleep 5
很长 按照执行流程分解来分析
bashif [ $# -ne 3 ]; then # 检查命令行参数是否为三个
print_usage ${0} # 如果不是 则调用print_usage() 并将当前脚本的路径作为参数传入函数
exit 1
fi
set -e # 如果任何命令的执行结果不是0 则立即退出脚本
set -u # 如果使用了未声明的变量,立即退出脚本
# 寻找配置文件firmae.config 如果找到就使用source将其引入
if [ -e ./firmae.config ]; then
source ./firmae.config
elif [ -e ../firmae.config ]; then
source ../firmae.config
else # 如果找不到就报错退出
echo "Error: Could not find 'firmae.config'!"
exit 1
fi
OPTION=`get_option ${1}` # 将命令行第一个参数传入函数get_option()
if [ ${OPTION} == "none" ]; then # 如果get_option()返回值为none 则调用print_usage()
print_usage ${0}
exit 1
fi
if (! id | egrep -sqi "root"); then # 检测是否为root权限 如果不为 则报错退出
echo -e "[\033[31m-\033[0m] This script must run with 'root' privilege"
exit 1
fi
BRAND=${2}
WORK_DIR=""
IID=-1
FIRMWARE=${3}
if [ ${OPTION} = "debug" ] && [ -d ${FIRMWARE} ]; then # 如果OPTION=debug且第三个参数指向一个文件夹 则报错退出
echo -e "[\033[31m-\033[0m] select firmware file on debug mode!"
exit 1
fi
if [ ! -d ${FIRMWARE} ]; then # 如果第三个参数不是文件夹则启动仿真
run_emulation ${FIRMWARE}
else
FIRMWARES=`find ${3} -type f` # 如果第三个参数为文件夹则对其中内容逐一进行仿真
for FIRMWARE in ${FIRMWARES}; do
if [ ! -d ${FIRMWARE} ]; then
run_emulation ${FIRMWARE}
fi
done
fi
没啥讲的 输出使用指南
bashfunction print_usage()
{
echo "Usage: ${0} [mode]... [brand] [firmware|firmware_directory]"
echo "mode: use one option at once"
echo " -r, --run : run mode - run emulation (no quit)"
echo " -c, --check : check mode - check network reachable and web access (quit)"
echo " -a, --analyze : analyze mode - analyze vulnerability (quit)"
echo " -d, --debug : debug mode - debugging emulation (no quit)"
echo " -b, --boot : boot debug mode - kernel boot debugging using QEMU (no quit)"
}
简单来说 就是根据参数调用具体的脚本和方法对固件进行模拟和分析
./scripts/util.py 检查数据库连通性./sources/extractor/extractor.py 提取固件内核和文件系统./scripts/getArch.py 检查固件架构信息./scripts/inferKernel.py 检查内核版本及初始化相关信息./scripts/tar2db.py 将固件相关信息导入数据库./scripts/makeImage.sh 制作qemu镜像./scripts/makeNetwork.py 配置网络接口./analyses/analyses_all.sh 对固件进行漏洞分析bashfunction run_emulation()
{
echo "[*] ${1} emulation start!!!"
INFILE=${1}
BRAND=`get_brand ${INFILE} ${BRAND}` # 将第一个参数 和第二个参数传入到函数get_brand()
FILENAME=`basename ${INFILE%.*}` # 去除第一个参数(固件)的后缀
PING_RESULT=false
WEB_RESULT=false
IP=''
if [ ${BRAND} = "auto" ]; then # 如果BRAND是auto 则报错退出
echo -e "[\033[31m-\033[0m] Invalid brand ${INFILE}"
return
fi
if [ -n "${FIRMAE_DOCKER-}" ]; then # 检查FIRMAE_DOCKER变量是否为空
if ( ! ./scripts/util.py check_connection _ $PSQL_IP ); then # 如果不为空则调用脚本检查连接
echo -e "[\033[31m-\033[0m] docker container failed to connect to the hosts' postgresql!"
return
fi
fi
# ================================
# extract filesystem from firmware
# ================================
t_start="$(date -u +%s.%N)" # 获取当前时间戳
timeout --preserve-status --signal SIGINT 300 \ # 限制脚本执行时间为300s
./sources/extractor/extractor.py -b $BRAND -sql $PSQL_IP -np -nk $INFILE images \
2>&1 >/dev/null # 调用extractor.py 提取固件文件系统
IID=`./scripts/util.py get_iid $INFILE $PSQL_IP`
if [ ! "${IID}" ]; then
echo -e "[\033[31m-\033[0m] extractor.py failed!"
return
fi
# ================================
# extract kernel from firmware
# ================================
timeout --preserve-status --signal SIGINT 300 \
./sources/extractor/extractor.py -b $BRAND -sql $PSQL_IP -np -nf $INFILE images \
2>&1 >/dev/null # # 调用extractor.py 提取固件内核文件
WORK_DIR=`get_scratch ${IID}` # 以IID为标志生成一个临时工作目录
mkdir -p ${WORK_DIR}
chmod a+rwx "${WORK_DIR}" # 对所有用户授予rwx权限
chown -R "${USER}" "${WORK_DIR}"
chgrp -R "${USER}" "${WORK_DIR}"
echo $FILENAME > ${WORK_DIR}/name
echo $BRAND > ${WORK_DIR}/brand
sync # 强制将缓冲区的内容写入硬盘
if [ ${OPTION} = "check" ] && [ -e ${WORK_DIR}/result ]; then # 如果OPTION为check 且在工作目录下有结果
if (egrep -sqi "true" ${WORK_DIR}/result); then # 检测结果文件中是否包含true 包含则输出 否则删除
RESULT=`cat ${WORK_DIR}/result`
return
fi
rm ${WORK_DIR}/result
fi
if [ ! -e ./images/$IID.tar.gz ]; then # 如果不存在文件系统压缩包 则报错退出
echo -e "[\033[31m-\033[0m] Extracting root filesystem failed!"
echo "extraction fail" > ${WORK_DIR}/result
return
fi
echo "[*] extract done!!!"
t_end="$(date -u +%s.%N)" # 再次获得时间戳 输出过程时间
time_extract="$(bc <<<"$t_end-$t_start")"
echo $time_extract > ${WORK_DIR}/time_extract
# ================================
# check architecture
# ================================
t_start="$(date -u +%s.%N)" # 检测架构
ARCH=`./scripts/getArch.py ./images/$IID.tar.gz $PSQL_IP`
echo "${ARCH}" > "${WORK_DIR}/architecture"
if [ -e ./images/${IID}.kernel ]; then
./scripts/inferKernel.py ${IID} # 解析内核版本和 init内容
fi
if [ ! "${ARCH}" ]; then
echo -e "[\033[31m-\033[0m] Get architecture failed!"
echo "get architecture fail" > ${WORK_DIR}/result
return
fi
if ( check_arch ${ARCH} == 0 ); then
echo -e "[\033[31m-\033[0m] Unknown architecture! - ${ARCH}"
echo "not valid architecture : ${ARCH}" > ${WORK_DIR}/result
return
fi
echo "[*] get architecture done!!!"
t_end="$(date -u +%s.%N)"
time_arch="$(bc <<<"$t_end-$t_start")"
echo $time_arch > ${WORK_DIR}/time_arch
if (! egrep -sqi "true" ${WORK_DIR}/web); then # 检测web文件中是否包含true
# ================================
# make qemu image
# ================================
t_start="$(date -u +%s.%N)"
./scripts/tar2db.py -i $IID -f ./images/$IID.tar.gz -h $PSQL_IP \
2>&1 > ${WORK_DIR}/tar2db.log # 将文件系统导入数据库
t_end="$(date -u +%s.%N)"
time_tar="$(bc <<<"$t_end-$t_start")"
echo $time_tar > ${WORK_DIR}/time_tar
t_start="$(date -u +%s.%N)"
./scripts/makeImage.sh $IID $ARCH $FILENAME \
2>&1 > ${WORK_DIR}/makeImage.log # 将指定的IID制作为镜像
t_end="$(date -u +%s.%N)"
time_image="$(bc <<<"$t_end-$t_start")"
echo $time_image > ${WORK_DIR}/time_image
# ================================
# infer network interface
# ================================
t_start="$(date -u +%s.%N)"
echo "[*] infer network start!!!"
# TIMEOUT is set in "firmae.config". This TIMEOUT is used for initial
# log collection.
TIMEOUT=$TIMEOUT FIRMAE_NET=${FIRMAE_NET} \
./scripts/makeNetwork.py -i $IID -q -o -a ${ARCH} \
&> ${WORK_DIR}/makeNetwork.log
ln -s ./run.sh ${WORK_DIR}/run_debug.sh | true
ln -s ./run.sh ${WORK_DIR}/run_analyze.sh | true
ln -s ./run.sh ${WORK_DIR}/run_boot.sh | true
t_end="$(date -u +%s.%N)"
time_network="$(bc <<<"$t_end-$t_start")"
echo $time_network > ${WORK_DIR}/time_network
else
echo "[*] ${INFILE} already succeed emulation!!!"
fi
# 网络连通性监测
if (egrep -sqi "true" ${WORK_DIR}/ping); then
PING_RESULT=true
IP=`cat ${WORK_DIR}/ip`
fi
if (egrep -sqi "true" ${WORK_DIR}/web); then
WEB_RESULT=true
fi
echo -e "\n[IID] ${IID}\n[\033[33mMODE\033[0m] ${OPTION}"
if ($PING_RESULT); then
echo -e "[\033[32m+\033[0m] Network reachable on ${IP}!"
fi
if ($WEB_RESULT); then
echo -e "[\033[32m+\033[0m] Web service on ${IP}"
echo true > ${WORK_DIR}/result
else
echo false > ${WORK_DIR}/result
fi
if [ ${OPTION} = "analyze" ]; then
# ================================
# analyze firmware (check vulnerability)
# ================================
t_start="$(date -u +%s.%N)"
if ($WEB_RESULT); then
echo "[*] Waiting web service..."
${WORK_DIR}/run_analyze.sh & # 调用自动分析
IP=`cat ${WORK_DIR}/ip`
check_network ${IP} false
echo -e "[\033[32m+\033[0m] start pentest!"
cd analyses
./analyses_all.sh $IID $BRAND $IP $PSQL_IP
cd -
sync
kill $(ps aux | grep `get_qemu ${ARCH}` | awk '{print $2}') 2> /dev/null
sleep 2
else
echo -e "[\033[31m-\033[0m] Web unreachable"
fi
t_end="$(date -u +%s.%N)"
time_analyze="$(bc <<<"$t_end-$t_start")"
echo $time_analyze > ${WORK_DIR}/time_analyze
elif [ ${OPTION} = "debug" ]; then
# ================================
# run debug mode.
# ================================
if ($PING_RESULT); then
echo -e "[\033[32m+\033[0m] Run debug!"
IP=`cat ${WORK_DIR}/ip`
./scratch/$IID/run_debug.sh &
check_network ${IP} true
sleep 10
./debug.py ${IID}
sync
kill $(ps aux | grep `get_qemu ${ARCH}` | awk '{print $2}') 2> /dev/null | true
sleep 2
else
echo -e "[\033[31m-\033[0m] Network unreachable"
fi
elif [ ${OPTION} = "run" ]; then
# ================================
# just run mode
# ================================
check_network ${IP} false &
${WORK_DIR}/run.sh
elif [ ${OPTION} = "boot" ]; then
# ================================
# boot debug mode
# ================================
BOOT_KERNEL_PATH=`get_boot_kernel ${ARCH} true`
BOOT_KERNEL=./binaries/`basename ${BOOT_KERNEL_PATH}`
echo -e "[\033[32m+\033[0m] Connect with gdb-multiarch -q ${BOOT_KERNEL} -ex='target remote:1234'"
${WORK_DIR}/run_boot.sh
fi
echo "[*] cleanup"
echo "======================================"
}
bashfunction get_brand()
{
INFILE=${1}
BRAND=${2}
if [ ${BRAND} = "auto" ]; then
echo `./scripts/util.py get_brand ${INFILE} ${PSQL_IP}`
else
echo ${2}
fi
}
根据命令函参数解析调用函数
pythonif __name__ == '__main__':
[infile, psql_ip] = sys.argv[2:4]
if sys.argv[1] == 'get_iid':
print(get_iid(infile, psql_ip))
if sys.argv[1] == 'get_brand':
print(get_brand(infile, psql_ip))
if sys.argv[1] == 'check_connection':
exit(check_connection(psql_ip))
测试数据库连接 如果连接正常则返回0 异常则返回1
pythondef check_connection(psql_ip):
try:
dbh = psycopg2.connect(database="firmware",
user="firmadyne",
password="firmadyne",
host=psql_ip)
dbh.close()
return 0
except:
return 1
计算文件的md5值 并将其值返回
pythondef io_md5(target):
blocksize = 65536
hasher = hashlib.md5()
with open(target, 'rb') as ifp:
buf = ifp.read(blocksize)
while buf:
hasher.update(buf)
buf = ifp.read(blocksize)
return hasher.hexdigest()
根据固件的hash在数据库内查询固件的ID 返回固件ID
pythondef get_iid(infile, psql_ip):
md5 = io_md5(infile)
q = "SELECT id FROM image WHERE hash = '%s'" % md5
image_id = query_(q, psql_ip)
if image_id:
return image_id[0]
else:
return ""
根据镜像的md5值查询出镜像的品牌
pythondef get_brand(infile, psql_ip):
md5 = io_md5(infile)
q = "SELECT brand_id FROM image WHERE hash = '%s'" % md5
brand_id = query_(q, psql_ip)
if brand_id:
q = "SELECT name FROM brand WHERE id = '%s'" % brand_id
brand = query_(q, psql_ip)
if brand:
return brand[0]
else:
return ""
else:
return ""
主要就是调用binwalk来对固件进行提取文件系统和内核
解析参数
pythondef main():
parser = argparse.ArgumentParser(description="Extracts filesystem and \
kernel from Linux-based firmware images")
parser.add_argument("input", action="store", help="Input file or directory")
parser.add_argument("output", action="store", nargs="?", default="images",
help="Output directory for extracted firmware")
parser.add_argument("-sql ", dest="sql", action="store", default=None,
help="Hostname of SQL server")
parser.add_argument("-nf", dest="rootfs", action="store_false",
default=True, help="Disable extraction of root \
filesystem (may decrease extraction time)")
parser.add_argument("-nk", dest="kernel", action="store_false",
default=True, help="Disable extraction of kernel \
(may decrease extraction time)")
parser.add_argument("-np", dest="parallel", action="store_false",
default=True, help="Disable parallel operation \
(may increase extraction time)")
parser.add_argument("-b", dest="brand", action="store", default=None,
help="Brand of the firmware image")
parser.add_argument("-d", dest="debug", action="store_true", default=False,
help="Print debug information")
result = parser.parse_args()
if psql_check(result.sql): # psql_check()检查数据库连接
extract = Extractor(result.input, result.output, result.rootfs,
result.kernel, result.parallel, result.sql,
result.brand, result.debug)
extract.extract()
if __name__ == "__main__":
main()
python# 核心提取函数
def extract(self):
"""
Perform the actual extraction of firmware updates, recursively. Returns
True if extraction complete, otherwise False.
"""
# self.item=image_path
self.printf("\n" + self.item.encode("utf-8",
"replace").decode("utf-8")) # 将image进行utf8编码
# check if item is complete
if self.get_status():
self.printf(">> Skipping: completed!")
return True
# check if exceeding recursion depth
if self.depth > ExtractionItem.RECURSION_DEPTH:
self.printf(">> Skipping: recursion depth %d" % self.depth)
return self.get_status()
# check if checksum is in visited set
self.printf(">> MD5: %s" % self.checksum)
with Extractor.visited_lock: # 多线程锁
# Skip the same checksum only in the same status
# asus_latest(FW_RT_N12VP_30043804057.zip) firmware
if (self.checksum in self.extractor.visited and # 如果checksum已经在visited中且状态相同 则判断为同一固件 跳过处理
self.extractor.visited[self.checksum] == self.status):
self.printf(">> Skipping: %s..." % self.checksum)
return self.get_status()
else:
self.extractor.visited[self.checksum] = self.status
# check if filetype is blacklisted
if self._check_blacklist():
return self.get_status()
# create working directory
self.temp = tempfile.mkdtemp()
# Move to temporary directory so binwalk does not write to input
os.chdir(self.temp)
try:
self.printf(">> Tag: %s" % self.tag)
self.printf(">> Temp: %s" % self.temp)
self.printf(">> Status: Kernel: %s, Rootfs: %s, Do_Kernel: %s, Do_Rootfs: %s" % (
self.get_kernel_status(),
self.get_rootfs_status(),
self.extractor.do_kernel,
self.extractor.do_rootfs))
for module in binwalk.scan(
self.item,
"--run-as=root",
"--preserve-symlinks",
"-e",
"-r",
"-C",
self.temp,
signature=True,
quiet=True,
): # 使用binwalk对固件进行扫描
# self.item:要扫描的文件路径;
# --run-as=root:在提取文件系统时,使用 root 用户权限;
# --preserve-symlinks:在提取文件系统时,保留符号链接;
# -e:将嵌入式文件系统和固件映像提取到一个单独的文件中;
# -r:递归扫描提取出的文件,以找到更深层次的文件系统和固件映像;
# -C:提取的文件输出到指定目录;
# signature=True:使用文件头签名来识别文件中的嵌入式文件系统和固件映像;
# quiet=True:在控制台中只输出错误和警告信息,不输出详细信息。
prev_entry = None
for entry in module.results:
desc = entry.description
dir_name = module.extractor.directory
if prev_entry and prev_entry.description == desc and \
'Zlib comparessed data' in desc:
continue
prev_entry = entry
self.printf('========== Depth: %d ===============' % self.depth)
self.printf("Name: %s" % self.item)
self.printf("Desc: %s" % desc)
self.printf("Directory: %s" % dir_name)
self._check_firmware(module, entry)
if not self.get_rootfs_status():
self._check_rootfs(module, entry)
if not self.get_kernel_status():
self._check_kernel(module, entry)
if self.update_status():
self.printf(">> Skipping: completed!")
return True
else:
self._check_recursive(module, entry)
except Exception:
print("ERROR: ", self.item)
traceback.print_exc()
return False
就是调用file获取文件类型 然后对预设好的列表进行比对 返回结果值 然后将结果更新到数据库中
python#!/usr/bin/env python3
import sys
import tarfile
import subprocess
import psycopg2
archMap = {"MIPS64":"mips64", "MIPS":"mips", "ARM64":"arm64", "ARM":"arm", "Intel 80386":"intel", "x86-64":"intel64", "PowerPC":"ppc", "unknown":"unknown"}
endMap = {"LSB":"el", "MSB":"eb"}
# 对于传入的filetype对archMap进行比对 返回对应的arch
def getArch(filetype):
for arch in archMap:
if filetype.find(arch) != -1:
return archMap[arch]
return None
# 同理获取大小端序
def getEndian(filetype):
for endian in endMap:
if filetype.find(endian) != -1:
return endMap[endian]
return None
infile = sys.argv[1]
psql_ip = sys.argv[2]
base = infile[infile.rfind("/") + 1:]
iid = base[:base.find(".")]
tar = tarfile.open(infile, 'r')
infos = []
fileList = []
for info in tar.getmembers():
# 如果tar压缩包内包含'/busybox' '/alphapd' '/boa' '/http' '/hydra' '/helia' '/webs' '/sbin/' '/bin/' 则将其新增到infos中
if any([info.name.find(binary) != -1 for binary in ["/busybox", "/alphapd", "/boa", "/http", "/hydra", "/helia", "/webs"]]):
infos.append(info)
elif any([info.name.find(path) != -1 for path in ["/sbin/", "/bin/"]]):
infos.append(info)
fileList.append(info.name)
with open("scratch/" + iid + "/fileList", "w") as f:
for filename in fileList:
try:
f.write(filename + "\n")
except:
continue
for info in infos:
tar.extract(info, path="/tmp/" + iid) # 将infos内的文件解压到/tmp/iid/目录下
filepath = "/tmp/" + iid + "/" + info.name
filetype = subprocess.check_output(["file", filepath]).decode() # 使用file命令检测其类型
arch = getArch(filetype)
endian = getEndian(filetype)
if arch and endian:
print(arch + endian)
subprocess.call(["rm", "-rf", "/tmp/" + iid])
dbh = psycopg2.connect(database="firmware",
user="firmadyne",
password="firmadyne",
host=psql_ip)
cur = dbh.cursor()
query = """UPDATE image SET arch = '%s' WHERE id = %s;"""
cur.execute(query % (arch+endian, iid))
dbh.commit()
with open("scratch/" + iid + "/fileType", "w") as f:
f.write(filetype)
break
subprocess.call(["rm", "-rf", "/tmp/" + iid])
就是使用string获取到kernel中的版本信息和启动参数 然后启动参数进行处理 提取出init 以便进行论文中提到的Boot arbitrations
python#!/usr/bin/env python3
import sys
import os
import subprocess
IID = -1
def ParseInit(cmd, out):
for item in cmd.split(' '):
if item.find("init=/") != -1:
out.write(item + "\n")
def ParseCmd():
if not os.path.exists("scratch/" + IID + "/kernelCmd"):
return
with open("scratch/" + IID + "/kernelCmd") as f:
out = open("scratch/{}/kernelInit".format(IID), "w")
cmds = f.read()
for cmd in cmds.split('\n')[:-1]:
ParseInit(cmd, out)
out.close()
if __name__ == "__main__":
# execute only if run as a script
IID = sys.argv[1]
kernelPath = './images/' + IID + '.kernel'
os.system("strings {} | grep \"Linux version\" > {}".format(kernelPath,
"scratch/" + IID + "/kernelVersion"))
os.system("strings {} | grep \"init=/\" | sed -e 's/^\"//' -e 's/\"$//' > {}".format(kernelPath,
"scratch/" + IID + "/kernelCmd"))
ParseCmd()
主要实现论文中的Network arbitrations 主要缓解了仿真网络可达性的问题 思想比较简单 先尝试启动一下镜像 然后根据日志输出检索出相关的网络配置 然后根据脚本依次尝试重新构建仿真系统的网络配置 逐一测试 直到网络联通或者全部不通 同时在此处实现了NVRAM arbitrations
pythondef inferNetwork(iid, arch, endianness, init):
global SCRIPTDIR
global SCRATCHDIR
TIMEOUT = int(os.environ['TIMEOUT'])
targetDir = SCRATCHDIR + '/' + str(iid)
loopFile = mountImage(targetDir) # 将image.raw挂载到./image下
fileType = subprocess.check_output(["file", "-b", "%s/image/%s" % (targetDir, init)]).decode().strip()
print("[*] Infer test: %s (%s)" % (init, fileType))
with open(targetDir + '/image/firmadyne/network_type', 'w') as out:
out.write("None")
qemuInitValue = 'rdinit=/firmadyne/preInit.sh'
if os.path.exists(targetDir + '/service'):
webService = open(targetDir + '/service').read().strip()
else:
webService = None
print("[*] web service: %s" % webService)
targetFile = ''
targetData = ''
out = None
if not init.endswith('preInit.sh'): # rcS, preinit 区分不同情况打开不同的init文件
if fileType.find('ELF') == -1 and fileType.find("symbolic link") == -1: # maybe script
targetFile = targetDir + '/image/' + init
targetData = readWithException(targetFile)
out = open(targetFile, 'a')
# netgear R6200
elif fileType.find('ELF') != -1 or fileType.find("symbolic link") != -1:
qemuInitValue = qemuInitValue[2:] # remove 'rd'
targetFile = targetDir + '/image/firmadyne/preInit.sh'
targetData = readWithException(targetFile)
out = open(targetFile, 'a')
out.write(init + ' &\n')
else: # preInit.sh
out = open(targetDir + '/image/firmadyne/preInit.sh', 'a')
if out:
out.write('\n/firmadyne/network.sh &\n')
if webService:
out.write('/firmadyne/run_service.sh &\n')
out.write('/firmadyne/debug.sh\n')
# trendnet TEW-828DRU_1.0.7.2, etc...
out.write('/firmadyne/busybox sleep 36000\n')
out.close()
umountImage(targetDir, loopFile)
print("Running firmware %d: terminating after %d secs..." % (iid, TIMEOUT))
cmd = "timeout --preserve-status --signal SIGINT {0} ".format(TIMEOUT)
cmd += "{0}/run.{1}.sh \"{2}\" \"{3}\" ".format(SCRIPTDIR,
arch + endianness,
iid,
qemuInitValue)# ./script/run.armel.sh 1 True 启动qemu对image进行模拟
cmd += " 2>&1 > /dev/null"
os.system(cmd)
loopFile = mountImage(targetDir) # 此处为NVRAM arbitrations
if not os.path.exists(targetDir + '/image/firmadyne/nvram_files'): # firmadyne相关的目录哪里来的? makeImage.sh "Creating FIRMADYNE Directories"
print("Infer NVRAM default file!\n")
os.system("{}/inferDefault.py {}".format(SCRIPTDIR, iid)) # python inferDefault.py 1
umountImage(targetDir, loopFile)
data = open("%s/qemu.initial.serial.log" % targetDir, 'rb').read()
ports = findPorts(data, endianness) # 寻找在日志中出现过的端口
#find interfaces with non loopback ip addresses
ifacesWithIps = findNonLoInterfaces(data, endianness) # 寻找日志中除127.0.0.1和0.0.0.0之外的ip
#find changes of mac addresses for devices
macChanges = findMacChanges(data, endianness) # 寻找日志中出现过的Mac地址
print('[*] Interfaces: %r' % ifacesWithIps)
networkList = getNetworkList(data, ifacesWithIps, macChanges) # ifacesWithIps=[] macChange=[]
return qemuInitValue, networkList, targetFile, targetData, ports # networkList=[] ports=[]

在原文中提到Boot arbitrations主要解决的是错误的引导顺序和缺少文件系统结构的问题
在run.sh中分别调用了extractor.py和inferKernel.py两个脚本来分别解决文件系统和启动顺序的问题
extractor.py 主要实现了针对固件使用binwalk来提取出内核文件以及文件系统
inferKernel.py 针对内核文件 使用strings来以及sed匹配来提取内部init相关的信息 以及内核版本问题
最后统一在Network arbitrations阶段使用
在makeNetwork.py中 会逐一尝试启动镜像以配置网络环境 在这时会针对inferKernel.py提取出来的init信息进行逐一尝试
在原文中提到Network arbitrations主要解决无效的IP别名和没有网络信息以及多个网络接口的问题
在makeNetwork.py中首先是针对inferKernel.py中获取到的Init进行逐一尝试启动镜像 同时记录其启动日志
然后在日志中针对关键的端口 IP 接口等网络信息进行记录 最后在NVRAM arbitrations阶段处理完成之后 进行遍历尝试 直到找到可以正常启动镜像 且网络联通的配置
NVRAM arbitrations依然是借助的Network Arbitrations阶段中的启动日志
针对启动日志中跟NVRAM相关的信息进行记录 然后全部存为配置文件在启动时加载
本文作者:Du4t
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!