经典案例

  • 首页Our ProjectsDirtyMoe:Rootkit 驱动程式

DirtyMoe:Rootkit 驱动程式

2024-11-29 15:24:20 12

摘要

在第一篇文章 DirtyMoe:模块化恶意软件的介绍与概述,我们描述了一种复杂且先进的恶意软件,名为 DirtyMoe。该恶意软件主要的作用是进行加密劫持和 DDoS 攻击,这些行为至今仍然流行。毫无疑问,恶意软件的发布目的在于获取利益,所有证据指向中国。在大多数情况下,PurpleFox 活动被用来利用系统漏洞,以便获得最高权限并通过 MSI 安装程序安装恶意软件。简而言之,安装程序滥用了 Windows 系统事件通知服务SENS进行恶意软件的部署。在部署结束时,两个进程工作者执行从隐蔽的 CampC 服务器收到的恶意活动。

正如我们在 第一篇文章 中提到的,任何优秀的恶意软件都必须实现一系列保护、反取证、反追踪和反调试技术。隐藏恶意活动的最常用技术之一是使用根套件。总体而言,根套件的主要目标是在内核层隐藏自身及其宿主恶意软件的其他模块。根套件是强大的工具,但由于其在内核模式下操作,任何关键漏洞都可能导致蓝屏死机BSoD,因此被检测的风险较高。

本文的主要目的是分析 DirtyMoe 使用的根套件技术。本研究的主要部分考察了 DirtyMoe 驱动程序的功能,旨在提供关于该驱动程序静态和动态分析的全面信息。DirtyMoe 根套件的主要技术包括:驱动程序可以在内核和用户模式下隐藏自身和其他恶意活动。此外,驱动程序可以在内核权限下执行从用户模式接收的命令。驱动程序的另一个重要方面是将任意 DLL 文件注入目标进程。最后,驱动程序的功能还包括审查文件系统内容。我们也描述了将驱动程序部署到内核的精细过程以及恶意软件作者使用的反取证方法。

本研究的另一个重要点是对驱动程序元数据的调查,其显示驱动程序是用已被盗并且在过去被撤销的证书进行代码签名的。然而,这些证书在野外广泛存在,并在当前其他恶意软件中被滥用。

最后,文章的最后部分总结了根套件功能,并将关键发现的数字证书相互联系起来,建立 DirtyMoe 和其他恶意软件之间的联系。此外,我们讨论了所使用的根套件技术的实现层次和来源。

1 样本

本研究的对象是一个 SHA256 为:AABA7DB353EB9400E3471EAAA1CF0105F6D1FAB0CE63F1A2665C8BA0E8963A05该样本是 Windows 驱动程序,DirtyMoe 在系统启动时会滴落该驱动程序。

注意:VirusTotal 记录了 71 个 AV 引擎中有 44 个(62) 检测到该样本为恶意。另一方面,DirtyMoe DLL 文件被 86 的注册 AV 检测到。因此,其检测覆盖率是足够的,因为驱动程序是从 DLL 文件中提取的。

基本信息文件类型:可移植执行 64文件信息:Microsoft Visual C 80驱动程序文件大小:11604 KB (118824 字节)数字签名:上海域联软件技术有限公司上海域联软件技术有限公司导入

该驱动程序导入了两个库 FltMgr 和 ntosrnl。表 1 总结了该驱动程序中最可疑的方法。

例程 描述FltSetCallbackDataDirty 一个微过滤驱动程序的前或后操作调用该例程,以指示它已修改回调数据结构的内容。FltGetRequestorProcessId 例程返回请求特定 I/O 操作的进程 ID。FltRegisterFilter FltRegisterFilter 注册一个微过滤驱动程序。ZwDeleteValueKeyZwSetValueKeyZwQueryValueKeyZwOpenKey 在内核模式中删除、设置、查询和打开注册表项的例程。ZwTerminateProcess 节点例程终止进程及其所有线程。ZwQueryInformationProcess 检索有关指定进程的信息。MmGetSystemRoutineAddress 返回一个指向由例程参数指定的函数的指针。ZwAllocateVirtualMemory 在指定的进程中预留一段可应用程序访问的虚拟地址范围。表 1 DirtyMoe 驱动程序导入的内核方法

乍一看,该驱动程序通过 MmGetSystemRoutineAddress() 找到内核例程,作为一种混淆形式,因为字符串表包含与 VirtualMemory 操作的例程名称;例如,ZwProtectVirtualMemory()、ZwReadVirtualMemory()、ZwWriteVirtualMemory()。内核例程 ZwQueryInformationProcess() 和字符串如 servicesexe、winlogonexe 表明该根套件可能与关键 Windows 进程的内核结构相关。

2 DirtyMoe 驱动程序分析

DirtyMoe 驱动程序并不会执行任何具体的恶意软件活动。然而,它提供了广泛的根套件和后门技术。该驱动程序被设计为 DirtyMoe 服务在用户模式中的支持系统。

该驱动程序可以执行需要高权限的操作,例如将文件写入系统文件夹、写入系统注册表、终止任意进程等。用户模式中的恶意软件只需发送定义的控制代码和数据给驱动程序,驱动程序将提供更高权限的操作。

此外,恶意软件还可以使用该驱动程序隐藏某些记录,以帮助掩盖恶意活动。该驱动程序影响系统注册表,并可以隐藏任意键。此外,系统进程 servicesexe 在其内存中被打补丁,驱动程序可以将任意服务从正在运行的服务列表中排除。此外,驱动程序修改了记录已加载驱动程序的内核结构,因此恶意软件可以选择哪些驱动程序可见,哪些不可见。因此,恶意软件仍然处于活动状态,但系统和用户无法通过标准 API 调用列出恶意软件记录,注册表、服务或已加载的驱动程序。恶意软件还可以隐藏存储在文件系统中的必要文件,因为驱动程序实现了微过滤机制。因此,如果用户请求文件系统中的记录,驱动程序会捕获这一请求,并可以影响传递给用户的查询结果。

该驱动程序包含 10 个主要功能,如 表 2 所示。

功能 描述驱动程序入口 核心在加载驱动时调用的例程。启动例程 作为内核线程运行,从系统注册表恢复驱动程序配置。设备控制 处理用户模式控制驱动程序的系统定义 I/O 控制代码 (IOCTL)。微过滤驱动程序 例程完成一个或多个类型的 I/O 操作的处理;在这种情况下为 QueryDirectory。换句话说,该例程过滤文件夹枚举。线程通知 例程注册驱动程序提供的回调,并在新线程创建时通知。NTFS驱动程序的回调 包装 NTFS 驱动的原始回调以进行 IRPMJCREATE 主要功能。注册表隐藏 是钩子方法,提供注册表键隐藏。服务隐藏 是隐藏定义服务的例程。驱动程序隐藏 是隐藏定义驱动程序的例程。驱动程序卸载 内核在卸载驱动程序时调用的例程。表 2 驱动程序的主要功能

大多数实现的功能作为公共样本在互联网论坛上可用。每个驱动程序功能的编程水平各不相同。看起来驱动程序的作者在大多数情况下合并了公共样本。因此,该驱动程序包含了一些错误和未使用的代码。该驱动程序仍在开发中,我们可能会在野外找到其他版本。

21 驱动程序入口

驱动程序入口 是内核加载驱动后首个被调用的例程。驱动程序初始化了大量全局变量,以确保其正常工作。首先,驱动程序检测操作系统版本并设置后续恶意使用所需的偏移。其次,指向 驱动程序映像 的变量被初始化。驱动程序映像 用于隐藏驱动程序。驱动程序还关联以下主要功能:

IRPMJCREATE IRPMJCLOSE 无关的操作,IRPMJDEVICECONTROL 用于驱动程序配置和控制,IRPMJSHUTDOWN 将恶意定义的数据写入磁盘和注册表。

驱动程序入口 创建一个指向驱动程序的符号链接,并尝试将其与其他恶意监控或过滤回调关联。第一个是通过 FltRegisterFilter() 方法激活的微过滤器,注册 FltPostOperation();它过滤对系统驱动器的访问并允许隐藏文件和目录。

接下来,初始化方法将主要功能 IRPMJCREATE 交换,以便于 FileSystemNtfs 驱动程序。因此,每次 API 调用 CreateFile() 或内核模式函数 IoCreateFile() 都可以被恶意 MalNtfsCreatCallback() 回调监视和影响。

另一个 驱动程序入口 方法使用 PsSetCreateThreadNotifyRoutine() 设置返回方法。NotifyRoutine() 监控内核进程创建,恶意软件可以将恶意代码注入到新创建的进程/线程中。

最后,驱动程序尝试从系统注册表恢复其配置。

22 启动例程

启动例程 是在驱动程序入口例程中创建的作为内核系统线程运行。启动例程 将驱动程序版本写入系统注册表,如下所示:

键: HKLMSYSTEMCurrentControlSetControlWinApiWinDeviceVer值: 20161122

如果存在以下 SOFTWARE 注册表项,驱动程序将加载注入过程所需的伪件:

HKLMSOFTWAREMicrosoftWindowsCurrentVersionInstallerSecure

启动例程 的最后一部分从注册表加载剩余必要条目。系统注册表的完整列表在附录 A 中记录。

23 设备控制

设备控制是控制加载驱动程序的机制。当用户模式线程调用 Win32 API DeviceIoControl() 时,驱动程序接收 IRPMJDEVICECONTROL I/O 控制代码 (IOCTL);有关更多信息请参见 [1]。用户模式应用程序直接将 IRPMJDEVICECONTROL 发送到特定设备驱动程序。驱动程序随后执行相应的操作。因此,恶意用户模式应用程序可以通过此机制控制驱动程序。

该驱动程序支持约 60 个控制代码。我们将控制代码分为 3 个基本组:控制、插入和设置。

海马加速器官方控制

共有 9 个主要控制代码从用户模式调用驱动程序功能。以下 表 3 总结了恶意软件可以使用 Win32 API 发送的控制 IOCTL。

IOCTL 描述0x222C80 驱动程序仅在启用后接受其他 IOCTL。用户模式的恶意软件可以通过发送此 IOCTL 和授权代码 0xB6C7C230 激活驱动程序。0x2224C0 恶意软件发送数据,驱动程序将数据写入系统注册表。键、值和数据类型由设置控制代码设置。使用的变量:regKey,regValue,regData,regType0x222960 此 IOCTL 清除驱动程序存储的所有数据。使用的变量:请参见 设置 和 插入 变量0x2227EC 如果恶意软件需要隐藏特定驱动程序,驱动程序将在 listBaseDllName 中添加特定驱动程序名称,并使用驱动程序隐藏。0x2227E8 驱动程序将注册表键名称添加到 WinDeviceAddress 列表,并使用注册表隐藏来隐藏此键。使用的变量:WinDeviceAddress0x2227F0 驱动程序根据 DLL 映像的名称来隐藏给定服务。服务的名称被插入到 listServices 变量中,并且服务隐藏技术在系统中隐藏该服务。0x2227DC 如果恶意软件想要停用注册表隐藏,驱动程序会恢复原始内核 GetCellRoutine()。0x222004 恶意软件发送希望终止的进程 ID。驱动程序调用内核函数 ZwTerminateProcess(),终止该进程及其所有线程,而不管恶意软件权限。0x2224C8 恶意软件发送数据,驱动程序将数据写入由 filePath 变量定义的文件;请参见 设置 控制代码使用的变量:filePath,fileData表 3 控制 IOCTLs

插入

共有 11 个控制代码将项目插入白名单/黑名单。以下 表 4 总结了变量及其目的。

白/黑名单 变量 目的注册表 HIVE WinDeviceAddress 定义恶意软件希望在系统中隐藏的注册表项列表。进程映像文件名 WinDeviceMaker 表示由 process image file name 定义的进程白名单。它在 NTFS 驱动程序回调中使用,并允许访问 NTFS 文件系统。此外,它在微过滤驱动程序中操作,防止隐藏在 WinDeviceNumber 变量中定义的文件。最后的使用是在注册表隐藏中;恶意软件不会隐藏白名单进程的注册表键。 WinDeviceMakerB 定义由 process image file name 定义的进程白名单。它在 NTFS 驱动程序回调中使用,并允许访问 NTFS 文件系统。 WinDeviceMakerOnly 定义由 process image file name 定义的进程黑名单。它在 NTFS 驱动程序回调中使用,并拒绝访问 NTFS 文件系统。文件名完整路径 WinDeviceNameWinDeviceNameB 确定通过 NTFS 驱动程序回调应获得访问权限的文件白名单。它与 WinDeviceMaker 和 WinDeviceMakerB 结合使用。因此,如果文件在白名单内,且请求的进程也在白名单内,驱动程序将访问文件。 WinDeviceNameOnly 定义应被 NTFS 驱动程序回调拒绝访问的文件黑名单。它与 WinDeviceMakerOnly 结合使用。因此,如果文件在黑名单内,且请求进程也在黑名单内,驱动程序将拒绝访问该文件。文件名包含数字 WinDeviceNumber 定义通过微过滤驱动程序应在系统中隐藏的文件列表。恶意软件使用命名规范如下:[AZ][az][09][az]{3}。因此,文件名包含数字。进程 ID ListProcessId1 定义要求访问 NTFS 文件系统的进程列表。恶意软件不会限制这些进程的访问;请参见 NTFS 驱动程序的回调。 ListProcessId2 与 ListProcessId1 的相同目的。此外,它作为注册表隐藏的白名单使用,因此驱动程序不限制对该列表中进程的访问。微过滤驱动程序不会限制此列表中的进程。驱动程序名称 listBaseDllName 定义应该在系统中隐藏的驱动程序列表;请参见驱动程序隐藏。服务名称 listServices 指定应该在系统中隐藏的服务列表;请参见服务隐藏。表 4 白黑名单

设置

设置控制代码将标量值存储为全局变量。以下 表 5 总结并分组这些变量及其目的。

功能 变量 描述文件写入关机时 filename1forShutDowndata1forShutDown 定义在驱动程序关机时写入的第一个文件名和数据。 filename2forShutDowndata2forShutDown 定义在驱动程序关机时写入的第二个文件名和数据。注册表写入关机时 regKey1shutdownregValue1shutdownregData1shutdownregType1 指定在驱动程序关机时写入的第一个注册表键路径、值名称、数据和类型REGBINARY、REGDWORD、REGSZ 等。 regKey2shutdownregValue2shutdownregData2shutdownregType2 指定在驱动程序关机时写入的第二个注册表键路径、值名称、数据和类型REGBINARY、REGDWORD、REGSZ 等。文件数据写入 filePath 确定将用于写入数据的文件名;请参见 控制 IOCTL 0x2224C8。注册表写入 regKeyregValueregType 指定注册表键路径、值名称和类型REGBINARY、REGDWORD、REGSZ 等;请参见 控制 IOCTL 0x2224C0。未知未使用的 dwWinDevicePathAdwWinDeviceDataA 保存文件 A 的路径和数据。 dwWinDevicePathBdwWinDeviceDataB 保存文件 B 的路径和数据。表 5 驱动程序的全局变量

以下 表 6 总结用于进程注入的变量;请参见线程通知。

功能 变量 描述注入进程 dwWinDriverMaker2dwWinDriverMaker22 定义两个命令行参数。如果创建的进程具有其中一个参数,则驱动程序应注入该进程。 dwWinDriverMaker1dwWinDriverMaker12 定义两个应在创建时注入的进程名称。DLL 注入 dwWinDriverPath1dwWinDriverDataA 指定定义进程注入的文件名和数据,由 dwWinDriverMaker2 或 dwWinDriverMaker1 定义。 dwWinDriverPath12dwWinDriverDataA2 定义由 dwWinDriverMaker22 或 dwWinDriverMaker12 定义的进程注入的文件名和数据。 dwWinDriverPath2dwWinDriverDataB 保存由 dwWinDriverMaker2 或 dwWinDriverMaker1 定义的进程注入的文件名和数据。 dwWinDriverPath22dwWinDriverDataB2 指定由 dwWinDriverMaker22 或 dwWinDriverMaker12 定义的进程注入的文件名和数据。表 6 注入变量

24 微过滤驱动

微过滤驱动程序在驱动入口例程中通过 FltRegisterFilter() 方法注册。方法的一个参数定义配置FLTREGISTRATION和回调方法FLTOPERATIONREGISTRATION。如果调用 QueryDirectory 系统请求,恶意驱动程序会捕获此请求并通过其 FltPostOperation() 进行处理。

FltPostOperation() 方法可以修改 QueryDirectory 操作IRP的结果。实际上,恶意驱动程序可以影响隐藏、插入、修改目录枚举。因此,某些用户模式下的应用程序可能无法看到所请求目录的实际图像。

驱动程序仅在请求进程不在白名单中时才会影响 QueryDirectory 结果。存在两个白名单。第一组白名单进程 ID 和 文件名意味着如果请求进行 I/O 操作 (QueryDirectory) 的进程 ID 或进程映像文件名称在白名单中,则不修改 QueryDirectory 结果。这代表应允许访问文件系统的恶意进程。第二个白名单称为 WinDeviceNumber,定义一组后缀。FltPostOperation() 迭代每个 QueryDirectory 的项。如果枚举项名称有在白名单中定义的后缀,驱动程序将从 QueryDirectory 结果中移除该项。这确保白名单文件对非恶意进程不可见。因此,驱动程序可以轻松隐藏任意目录/文件,以便用户模式下的应用程序包括 explorerexe。WinDeviceNumber 白名单的命名可能源自恶意文件名称,例如,Ke145057xsl,因为后缀是数字;见附录 B。

25 NTFS 驱动程序的回调

当驱动程序加载时,驱动程序入口例程会修改 NTFS 文件系统的系统驱动程序。原始的 IRPMJCREATE 主要功能的回调方法被恶意回调 MalNtfsCreatCallback() 替换,如 图 1 所示。恶意回调决定什么该获取访问权限,什么不该访问。

图 1 重写常规 NTFS 驱动的 IRPMJCREATE 回调

恶意回调仅在微过滤驱动注册完成并且原始回调被替换后生效。存在白名单和黑名单。白名单存储有关允许的 process image names、process ID 和允许的 files 的信息。如果请求访问磁盘的进程在白名单中,则请求的文件也必须在白名单中。这是双重保护。黑名单则集中在 process image names 上。因此,黑名单中的进程被拒绝访问文件系统。图 2 展示了允许的进程白名单。如果进程在白名单中,驱动程序调用原始回调;否则,请求将以拒绝访问结束。

图 2 授权访问白名单中的进程

一般而言,如果恶意回调确定请求进程被授权访问文件系统,驱动程序将调用原始 IRPMJCREATE 主要功能。如果没有,驱动程序将请求以 STATUSACCESSDENIED 结束。

26 注册表隐藏

驱动程序可以隐藏给定的注册表键。对注册表键的每次操作都被内核方法 GetCellRoutine() 捕获。配置管理器为每个打开的注册表键分配一个控制块。控制块CMKEYCONTROLBLOCK结构在哈希表中保持所有控制块,以便快速搜索已存在的控制块。GetCellRoutine() 钩子方法计算所请求键的内存地址。因此,如果恶意驱动程序控制了 GetCellRoutine(),驱动程序就可以过滤哪些注册表键将可见;更确切地说,哪些键将在哈希表中被搜索。

恶意驱动程序找到原始 GetCellRoutine() 的地址,并用其恶意钩子方法 MalGetCellRoutine() 替换掉它。驱动程序使用良好文档化的实现 [3 4]。驱动程序只是通过 ZwOpenKey() 内核调用遍历内核结构。然后,驱动程序查找 CMKEYCONTROLBLOCK,其关联的 HHIVE 结构与所请求的键对应。HHIVE 结构包含一个指向 GetCellRoutine() 方法的指针,驱动程序将其替换;见 图 3。

图 3 重写 GetCellRoutine

该方法的缺点在于,查找的结构、变量或方法的偏移量在每个 Windows 版本或构建中都是特定的。因此,驱动程序必须确定其运行的 Windows 版本。

MalGetCellRoutine() 钩子方法执行三项基本操作,如下所示:

驱动程序调用原始内核 GetCellRoutine() 方法。检查所请求注册表键的白名单。根据白名单检查捕获或释放请求的注册表键。注册表键隐藏

此隐藏技术使用一个简单原理。驱动程序迭代所请求键的整个 HIVE。如果驱动程序找到要隐藏的注册表键,它将返回迭代的 HIVE 的最后一个注册表键。当与 HIVE 的交互到达末尾时,驱动程序不会返回最后一个键,因为它之前已返回,而是返回 NULL,从而结束 HIVE 的查询。

这一原则的结果是,如果驱动程序希望隐藏多个键,驱动程序则多次返回搜索 HIVE 的最后一个键。因此,注册表查询的最终结果可能包含重复的键。

白名单

servicesexe 和 System 服务默认在白名单中,无需任何限制。白名单中的 process image names 也没有注册表访问限制。

对于 Windows 10,决策机制复杂得多。如果 Windows 10 构建为 143932016 年 7 月或 150632017 年 3 月,驱动程序仅对 regeditexe 应用程序隐藏请求键。

27 线程通知

线程通知 的主要目的在于将恶意代码注入新创建的线程中。驱动程序在设备入口初始化期间通过 PsSetCreateThreadNotifyRoutine() 注册线程通知例程。该例程注册了一个回调,随后在新线程创建或删除时进行通知。可疑的回调PCREATETHREADNOTIFYROUTINENotifyRoutine() 接受三个参数:ProcessID、ThreadID 和 Create 标志。驱动程序仅影响 Create 标志为 TRUE 的线程,因此仅影响新创建的线程。

白名单集中在两个方面。第一个是 image name,第二个是新线程的 commandline arguments。image name 存储在 WinDriverMaker1 中,参数存储为 WinDriverMaker2 变量中的校验和。驱动程序旨在仅为由 process name 定义的两个进程和两个由命令行参数定义的进程注入代码。没有白名单,仅有 4 个全局变量。

271 内核方法查找

成功的恶意代码注入需要几个内核方法。由于检测技术,驱动程序不会直接调用这些方法,而是尝试混淆所需方法。驱动程序需要以下内核方法:ZwReadVirtualMemory、ZwWriteVirtualMemory、ZwQueryVirtualMemory、ZwProtectVirtualMemory、NtTestAlert、LdrLoadDll

这些内核方法对于成功的线程注入至关重要,因为驱动程序需要读取/写入被注入线程的进程数据,包括程序指令。

虚拟内存方法查找

驱动程序从 ZwAllocateVirtualMemory() 方法的地址开始检查代码段。如 图 4 所示,所有查找方法依次位于 ntdlldll 内部 ZwAllocateVirtualMemory() 方法之后。

图 4 代码段的 ntdlldll 及虚拟内存方法

驱动程序从 ZwAllocateVirtualMemory() 的地址开始检查代码段,并查找代表常量转移到 eax 的指令例如,mov eax h。这样可以识别 VirtualMemory 方法;见 表 7 中的常量。

常量 方法0x18 ZwAllocateVirtualMemory0x23 ZwQueryVirtualMemory0x3A NtWriteVirtualMemory0x50 ZwProtectVirtualMemory表 7 Windows 1064 位虚拟内存方法的常量

找到适当常量后,查找方法的最终地址如下计算:

methodaddress = constantaddress alignmentconstant其中 alignmentconstant 有助于指向查找方法的开始。

查找方法的步骤可以总结为:

获取 ZwAllocateVirtualMemory() 的地址,其在检测方面不是怀疑的。每个所需方法包含特定常量在特定地址constantaddress。如果 constantaddress 被发现,则减去另一个特定偏移量alignmentconstant;alignmentconstant 是每个 Windows 版本特定的。

虚拟内存方法查找方法的确切实现展示在 图 5 中。

图 5 查找例程的实现,搜索内核虚拟内存方法

这种混淆的成功依赖于 Windows 版本的识别。我们发现一个 Windows 7 的版本返回与恶意软件所需的不同方法;即,ZwCompressKey()、ZwCommitEnlistment()、ZwCreateNamedPipeFile()、ZwAlpcDeleteSectionView()。很显然,alignmentconstant 是根据驱动程序在执行时所运行的当前 Windows 版本,取自驱动程序的入口例程。

NtTestAlert 和 LdrLoadDll 查找

获取 NtTestAlert() 和 LdrLoadDll() 例程的方法采用不同的方法。驱动程序附加到 winlogonexe 进程,并获取指向内核结构 PEBLDRDATA 的指针,该结构包含所有系统进程的 PE 头和导入。如果导入表中包含所需方法,则驱动程序提取基地址,基地址是查找所需例程的入口点。

DirtyMoe:Rootkit 驱动程式 272 进程注入

进程注入的目的是通过内核函数 LdrLoadDll() 将定义的 DLL 库加载到新线程中。x86 和 x64 操作系统版本的进程注入略有不同。

x64 操作系统版本滥用了原始的 NtTestAlert() 例程,该例程检查线程的 APC 队列。APC异步过程调用是一种排队作业以在特定线程上下文中执行的技术。它会定期调用。驱动程序重写了 NtTestAlert() 的指令,该指令跳转到恶意代码的入口点。

修改 NtTestAlert 代码

进程注入的第一步是查找代码洞中的可用内存。驱动程序在 NtTestAlert() 例程地址附近找到可用内存。代码洞包括 shellcode,正如 图 6 演示的那样。

图 6 恶意负载覆盖原始 NtTestAlert() 例程

该 shellcode 为恶意代码准备一个参数 (codecave 地址),然后跳转到它。原始 NtTestAlert() 地址被存储到 rax 中,因为恶意代码以 return 指令结尾,因此调用原始 NtTestAlert()。最后,rdx 包含跳转地址,即驱动程序在其中注入了恶意代码。代码洞的下一个项是 DLL 文件的路径,该文件将在注入的进程中载入。代码洞的其他项包括原始地址和 NtTestAlert() 的原始代码指令。

驱动程序将恶意代码写入在 dllloadingshellcode 中定义的地址。对 NtTestAlert() 的原始指令进行了重写,仅留下跳转至 shellcode 的指令。这样,在调用 NtTestAlert() 时,将激活 shellcode 并跳转到恶意代码。

恶意代码 x64

x64 的恶意代码很简单。首先,它恢复重写的 NtTestAlert() 代码的原始指令。其次,代码调用找到的 LdrLoadDll() 方法,并在注入进程的地址空间中加载适当的 DLL。最后,代码执行 return 指令,跳回到原始 NtTestAlert() 函数。

x86 操作系统版本则直接利用注入进程的入口点。该过程与 x64 注入非常相似,x86 恶意代码也与 x64 版本相同。然而,x86 恶意代码需要找到 LdrLoadDll() 方法的 32 位变种。它使用与上述描述的方法 NtTestAlert 和 LdrLoadDll 查找。

28 服务隐藏

Windows 使用服务控制管理器SCM来管理系统服务。SCM 的可执行文件为 servicesexe。该程序在系统启动时运行,并执行多个功能,例如启动、结束和与系统服务交互。SCM 进程还存储所有正在运行的服务在一个未文档化的服务记录SERVICERECORD结构中。

要隐藏所需服务,驱动程序必须修补服务记录。首先,驱动程序必须查找进程 servicesexe 并通过 KeStackAttachProcess() 附加到该进程。恶意作者定义了驱动程序在进程内存中查找服务记录时所寻找的字节序列。servicesexe 将所有正在运行的服务存储为 SERVICERECORD 的链表 [5]。恶意驱动程序迭代此链表,并从 listServices 白名单中分离所需服务;见 表 4。

用于 Windows 2000、XP、Vista 和 Windows 7 的字节序列如下:{45 3B E5 74 40 48 8D 0D}。还有另一个字节序列 {48 83 3D 48 8D 0D} 由于与驱动程序尚未确定的 Windows 版本有关,因此从未使用。

隐藏的服务无法通过 PowerShell 命令 GetService、Windows 任务管理器、进程资源管理器等检测。然而,启动的服务通过 Windows 事件日志记录。因此,我们可以列出所有常规服务以及所有记录的服务。然后,我们可以创建差异以查找隐藏服务。

29 驱动程序隐藏

驱动程序能够根据来自用户模式的 IOCTL 隐藏自身或其他恶意驱动程序。驱动程序入口由一个表示已加载驱动程序图像的参数初始化DRIVEROBJECT。驱动程序对象包含一个官方未记录的项,称为驱动程序节。LDRDATATABLEENTRY 内核结构存储有关已加载驱动程序的信息,例如基地址/入口点地址、图像名称、图像大小等。驱动程序节指向LDRDATATABLEENTRY 作为表示系统中所有已加载驱动程序的双向链表。

当用户模式应用程序列出所有已加载驱动程序时,内核会遍历LDRDATATABLEENTRY 结构的双向链表。恶意驱动程序遍历整个列表,并对应该隐藏的项驱动程序解链。这样,内核将失去对被隐藏驱动程序的指针,无法枚举所有加载的驱动程序 [6]。

210 驱动程序卸载

驱动程序卸载 功能包含可疑代码,但在此版本中似乎从未使用。其余的卸载功能执行从系统中卸载驱动程序的标准过程。

3 启动期间加载驱动程序

DirtyMoe 服务加载恶意驱动程序。驱动图像并未永久存储在磁盘上,因为服务在启动时始终提取、加载和删除驱动图像。次级服务的目的是消除有关驱动加载的证据,并最终使取证分析更为复杂。该服务力求伪装注册表和磁盘活动。DirtyMoe 服务注册如下:

服务名称:MsltvolumeidgtApp;例如,MsE3947328App注册表项:HKLMSYSTEMCurrentControlSetservicesImagePath:SystemRootsystem32svchostexe k netsvcsServiceDll:CWindowsSystem32dll,ServiceMainServiceType:SERVICEWIN32SHAREPROCESSServiceStart:SERVICEAUTOSTART

31 注册表操作

启动时,服务创建一个注册表记录,描述要加载的恶意驱动程序;以下示例:

注册表键:HKLMSYSTEMCurrentControlSetservicesdumpE3947328 ImagePath CWindowsSystem32driversdumpLSIFCsys DisplayName dumpE3947328

乍一看,显然 ImagePath 中的值并未反映 DisplayName,这与 Windows 常规命名方式相左。此外,ImagePath 以 dump 字符串作为前缀,用于虚拟驱动程序仅在内存中加载,用于管理 Windows 崩溃期间的内存转储。恶意软件试图使用虚拟驱动程序名称约定,以鸣不响的方式掩护自己。虚拟驱动程序的转储内存原理在 [7 8] 中有描述。

ImagePath 的值在每次 Windows 重启时都是不同的,但它总是滥用了系统本机驱动程序的名称;以下是收集到的一些实例:dumpACPIsys、dumpRASPPPOEsys、dumpLSIFCsys、dumpUSBPRINTsys、dumpVOLMGRsys、dumpINTELPPMsys、dumpPARTMGRsys。

32 驱动程序加载

当注册表项准备好之后,DirtyMoe 服务将驱动程序转储到由 ImagePath 定义的文件中。然后,服务通过 ZwLoadDriver() 加载该驱动程序。

33 证据清理

当驱动程序加载成功或失败时,DirtyMoe 服务开始掩盖更多恶意组件,以保护整个恶意软件层次结构。

DirtyMoe 服务删除表示已加载驱动程序的注册表项;请参见注册表操作。此外,加载的驱动程序隐藏了恶意软件服务,正如服务隐藏部分所描述的那样。与驱动程序相关的注册表项通过 API 调用被删除。因此,取证轨迹可以在位于 SystemRootsystem32configSYSTEM 的 SYSTEM 注册表 HIVE 中找到。这些 API 调用仅删除相关 HIVE 指针,但未引用的数据仍保留在 HIVE 中,即使它仍存储在磁盘上。因此,我们可以通过 RegistryExplorer 读取已删除的注册表项。

加载的驱动程序也删除了转储的dump 前缀驱动程序文件。我们无法通过使用户能够恢复已删除文件的工具恢复该文件,但它是直接从服务 DLL 文件中提取的。

捕获驱动程序映像和注册表键

恶意服务负责驱动程序加载及清理加载证据。我们在 nt!IopLoadDriver() 内核方法中设置了断点,该方法在某个进程需要加载驱动程序到系统中时会被调用。我们等待要加载的驱动程序,然后列出所有系统进程。相应服务svchostexe调用了驱动程序加载的内核调用,但该服务因 EIP 注册表修改而被终止。该进程服务被终止,整个 Windows 最终崩溃为 BSoD。Windows 进行了崩溃转储,因此文件系统缓存已被刷新,恶意服务未能及时完成清理。因此,我们能够挂载卷并读取所有所需数据。

34 取证痕迹

尽管 DirtyMoe 服务努力掩盖恶意活动,但仍有一些方面有助于识别该恶意软件。

DirtyMoe 服务及加载的驱动程序本身是隐藏的;然而,Windows 事件日志系统记录有关已启动服务的信息。因此,我们可以获取关于所有服务的信息,例如其 ProcessID 和 ThreadID,包括隐藏的服务。

连接到 Windows 内核的 WinDbg 可以使用 lm 命令显示所有加载的模块。模块列表可以揭示以 dump 前缀命名的非虚拟驱动程序,并识别恶意驱动程序。

离线连接卷可以提供服务及其他支持文件的 DLL 库,而不幸的是,这些文件经过加密和 VMProtect 混淆。最后,离线 SYSTEM 注册表存储了关于 DirtyMoe 服务的记录。

4 证书

Windows Vista 及以后的版本要求加载的驱动程序必须经过代码签名。数字代码签名应验证驱动程序供应商的身份和完整性 [9]。然而,Windows 不检查所有对 Windows 驱动程序进行签名的证书的当前状态。因此,如果路径中的任何证书过期或被撤销,驱动程序仍然会成功加载到系统中。我们在这里不讨论为什么 Windows 会

订阅我们的时事通讯

获取更多更新