背景
该分析是 FortiGuard 事件响应团队领导的事件调查的一部分。
我们发现恶意软件已在一台受感染的机器上运行了数周。威胁行为者执行了一批脚本和 PowerShell,以便在 Windows 进程中运行该恶意软件。虽然获取原始恶意软件可执行文件比较困难,但我们成功获取了正在运行的恶意软件进程的内存转储以及受感染机器的完整内存转储(“fullout”文件,大小为 33GB)。
图 1:受感染机器的完整内存转储文件。
图 1 提供了有关转储内存文件“fullout”的详细文件信息,我们对该文件进行了扫描,以构建用于分析恶意软件的本地测试环境。
转储的恶意软件文件
该恶意软件在进程号为 PID 8200 的 dllhost.exe 进程中运行。转储文件名为 pid.8200.vad.0x1c3eefb0000-0x1c3ef029fff.dmp。该文件名表明,该恶意软件已加载并部署在内存地址范围 0x1c3eefb0000 至 0x1c3ef029fff 中。
转储文件是一个已部署的 64 位 PE(可移植可执行文件)文件。在执行过程中,Windows 加载器会读取并解析其 DOS 和 PE 标头,以加载和部署 PE 文件。部署完成后,这些标头将不再需要。为了避免将恶意软件转储到文件中供研究人员分析,某些恶意软件通常会通过用零(例如本例)或随机数据覆盖这些标头区域来破坏它们。如图 2 所示,DOS 和 PE 标头均已损坏,这使得从内存中重建整个可执行文件变得非常困难。
在本地部署转储的恶意软件
为了动态分析恶意软件,我们需要在本地复制受感染系统的环境。这需要在调试器中启动 dllhost.exe 进程,将其作为部署转储恶意软件的目标进程。这样我们就可以在本地分析环境中分析恶意软件。
准备恶意软件以在此受控设置中正确执行涉及几个复杂的步骤。
-
定位入口点
第一步是找到入口点函数(启动函数),这是恶意软件被 Windows Loader 加载到内存中时执行的初始代码。
虽然入口点函数的偏移量通常存储在 PE 头中,但实际情况并非如此。因此,我们必须手动定位入口点(起始函数)。根据我们的经验,入口函数的第一条指令通常编译为“sub rsp, 28h”,但其他函数也可能包含此指令。不过,通过将恶意软件转储到 IDA Pro 中,我们能够在 IDA Pro 数据库中搜索到此指令的所有匹配项。
幸运的是,该指令在恶意软件中仅出现了8个实例(图3)。经过分析,我们确认第四个函数(位于0x1C3EEFEE0A8)是入口点。
图 3:定位入口点函数。
-
分配主内存
在新启动的 dllhost.exe 进程中,我们手动执行了一些指令来分配内存以部署转储的恶意软件,如图 4 所示。它调用相关的 VirtualAlloc() API,其基地址与受感染系统中的基地址相同——0x1C3EEB70000。
一旦分配,转储的恶意软件就会被复制到新创建的内存中。
-
解析导入表
PE 文件的导入表列出了它所依赖的 Windows API。这些 API 的加载地址在不同的 Windows 系统上有所不同。为了在本地系统中运行和分析转储的恶意软件,需要将这些地址迁移到本地系统中已加载的地址。
图 5:恶意软件导入表的部分视图。
图5展示了导入表中的部分Windows API地址。根据我们的分析,可以根据这些信息计算出最终的API地址。
例如,0x1C3EF0240D0 处的 API 地址为 0x1C3EEEE1CE0,如图 5 所示。通过在地址 0x1C3EEEE1CE0h 处执行以下 ASM 代码,计算出 API 地址为 0x7FFD74224630:
001C3EEEE1CE0 mov r10, 0E528F49552F112B4h001C3EEEE1CEA mov r11, 0E5288B6826D35484h001C3EEEE1CF4 xor r11, r10001C3EEEE1CF7 jmp r11 ; 0x7FFD74224630
使用 Volatility 工具,我们从“fullout”文件中列出了 dllhost.exe 进程(PID 8200)中加载的模块。如图 6 所示,位于 0x7FFD74224630 的 API 是从模块 GDI32.dll 导出的。
图6:dllhost.exe 中的加载模块列表。
通过从“fullout”文件中转储 GDI32.dll 并进行分析,我们确定地址 0x7FFD74224630 处的 API 对应于受感染系统中的 GetObjectW()。
在我们的本地测试环境中,这个 API 位于地址 0x07FFFF77CB870。对于这个 API,其原始地址已被替换为本地地址。
该恶意软件有 257 个 Windows API,需要在 16 个模块中重新定位,其中包括:
-
kernel32.dll
-
ws2_32.dll
-
ntdll.dll
-
gdi32.dll
-
shlwapi.dll
-
sspicli.dll
-
user32.dll
-
shell32.dll
-
msvcrt.dll
-
advapi32.dll
-
comctl32.dll
-
crypt32.dll
-
gdiplus.dll
-
ole32.dll
-
rpcrt4.dll
-
userenv.dll
对于导入表中的每个 API,必须使用与 API GetObjectW() 相同的方法将其原始地址替换为其对应的本地地址。
此外,我们还需要加载所有 dllhost.exe 未自动加载的必需模块。为此,必须调用带有模块名称的 API LoadLibraryA() 或 LoadLibraryW() 来将它们加载到恶意软件的内存中。
图 7 展示了恶意软件在调试器中的运行情况。RIP 寄存器指向入口点的地址 0x1C3EEFEE0A8。调试器还在底部显示了一些重定位的 Windows API 函数。
图 7:使用修复的 API 表在入口点函数处中断。
-
分配更多内存
根据我们的分析,该恶意软件还需要位于地址 0x1C3EEB7000 处的一些全局变量数据,大小为 0x5A000 字节。
我们使用 Volatility 工具和 dd 命令从“fullout”文件中提取了所需的全局数据。我们再次调用 VirtualAlloc() API,在 dllhost.exe 进程中按所需地址和大小分配新的内存区域。分配成功后,提取的数据将被复制到新分配的内存空间中,如图 8 所示。
图 8:从地址 0x1C3EEB7000 开始复制的全局变量数据。
-
将参数固定到入口点和堆栈
对恶意软件入口点函数的静态分析表明,该函数需要三个参数。
-
第一个参数(RCX)是加载的恶意软件的基地址,在本例中为 0x1C3EEFB0000。
-
第二个参数(RDX)的值为0x1。
-
第三个参数(R8)是指向 0x30 字节缓冲区的指针,也可以从“fullout”文件中提取。
我们相应地准备了所有三个参数,并分配了额外的内存来存储第三个参数的 30H 数据,如图 9 所示。
图 9:准备好所有三个参数和调整后的 RSP 寄存器。
最后一个关键点是 RSP 寄存器的正确对齐。在入口点处中断时,RSP 值的最低四位必须为 0x8,如图 9 所示。如果 RSP 未能正确对齐,恶意软件启动时可能会触发 EXCEPTION_ACCESS_VIOLATION 异常(代码 0xC0000005)。
这是由于对齐错误造成的,尤其是在执行“movdqa”之类的指令时,该指令需要 16 字节对齐。在 64 位代码模式下,在调用入口点函数之前,RSP 的值是 16 字节对齐的(最低四位为 0x0)。它将返回地址压入堆栈,此时 RSP 的值是 -8。为了纠正这个问题,我们将值从 0x09CAD11D850 调整为 0x09CAD11D848。
分析恶意软件
经过多次尝试、错误和反复修复,我们终于成功在本地环境中运行该恶意软件。
恶意软件执行后,会调用一个函数来解密其存储在内存中的 C2 服务器域名信息。如图 10 所示,解密函数与新解密的域名详细信息一起显示,包括域名(“rushpapers.com”)和端口号(“443”)。
图10:刚刚解密的C2服务器信息。
然后,恶意软件通过创建线程与其 C2 服务器建立通信。如图 11 所示,它准备使用位于 0x1C3EEFDE300 的线程函数调用 CreateThread() API。
图 11:即将为 C2 通信创建的线程。
新创建的线程负责处理与其 C2 服务器的通信。该线程的部分代码如图 11 右侧所示。启动线程后,主线程进入睡眠状态,直到通信线程完成执行。
与 C2 服务器通信
根据解密后的域名端口“443”可知,该恶意软件通过 TLS 协议与 C2 服务器通信。它使用 getaddrinfo() API 通过 DNS 查询获取域名“rushpapers.com”的 IP 地址。
图 12 是恶意软件与其 C2 服务器通信时生成的网络流量的 Wireshark 捕获。
图 12:恶意软件和 C2 服务器之间交换的网络数据包。
由于 TLS 数据包是加密的,我们需要在加密前或解密后检查数据才能查看明文内容。这可以通过在调试器中针对该恶意软件使用的加密和解密例程设置断点来实现。
该恶意软件利用两个 API 函数 SealMessage() 和 DecryptMessage() 来加密和解密 TLS 流量的数据。如图 13 所示,该恶意软件正准备使用 SealMessage() API 加密 HTTP GET 请求。
图 13:准备使用 SealMessage() 加密 GET 请求。
下面是两个明文数据包的示例(一个已发送,一个已接收):
-
请求数据包(TLS加密前):
GET/ws/ HTTP/1.1Host: rushpapers[.]comConnection: UpgradeUpgrade: websocketSec-WebSocket-Version: 13Sec-WebSocket-Key: OCnq155rYct3ykkkdLrjvQ==
-
响应数据包(TLS解密后):
HTTP/1.1 101 Switching ProtocolsServer: nginx/1.18.0Date: Fri, 28 Mar 2025 06:13:24 GMTConnection: upgradeUpgrade: websocketSec-WebSocket-Accept: Bzr0K1o6RJ4bYvvm4AM5AAG172Y=
这两个明文数据包用于完成类似握手的过程。之后,它会切换到自定义加密算法对数据包数据进行加密,然后再应用 TLS 加密。
以下是使用自定义算法加密的数据示例。
下表对数据进行了更清晰的细分:
按照可变扩展长度规则计算数据大小为0xA6-0x80=0x26。
加密密钥(例如 0x755C9816)是一个随机生成的数字。自定义加密的算法对密钥的每个字节和加密数据字节执行重复的异或运算。
解密后的数据如下:
解密后的数据清晰地揭示了我们本地测试环境的系统信息:“OS: Windows 10 / 64-bit (10.0.19045)rn”。当C2服务器发出请求时,这些信息会被收集并发送到C2服务器。
下面是一个代码片段,演示了用于加密和解密数据的自定义算法。
特征分析
通过对其API调用和执行流程的全面分析,我们确认该恶意软件为RAT(远程访问木马)。本节详细介绍了该恶意软件控制受感染系统的功能。
-
屏幕截图
该恶意软件具有一项功能,可以将受害者的屏幕捕获为 JPEG 图像,并将其传输到其 C2 服务器。它还会收集当前活动(最顶层)程序的标题,以提供有关用户在捕获时正在执行的操作的上下文信息。
为此,它调用一系列 API,包括 CreateStreamOnHGlobal()、GdiplusStartup()、GetSystemMetrics()、CreateCompatibleDC()、CreateCompatibleBitmap()、BitBlt()、GdipCreateBitmapFromHBITMAP()、GdipSaveImageToStream() 和 GdipDisposeImage()。
图 14 展示了恶意软件如何通过调用这些 API 来捕获屏幕截图。
图 14:捕获屏幕截图函数的伪代码。
-
充当服务器
该恶意软件包含一个线程函数,旨在充当服务器,监听 C2 服务器指定的 TCP 端口。一旦激活,该函数将允许恶意软件等待来自攻击者的传入连接。
它实现了多线程套接字架构:每当有新的客户端(攻击者)连接时,恶意软件都会创建一个新线程来处理通信。这种设计支持并发会话,并支持更复杂的交互。
通过这种模式的运行,恶意软件有效地将受感染的系统变成远程访问平台,允许攻击者发起进一步的攻击或代表受害者执行各种操作。
-
控制系统服务
该恶意软件可以枚举并操纵受感染计算机上的系统服务。它利用多个 Windows 服务控制管理器 (SCM) API(包括 OpenSCManagerW()、EnumServicesStatusExW()、ControlService() 等)来实现此目的。
结论
该分析成功展示了在受控本地环境中具有损坏的 DOS 和 PE 标头的恶意软件的部署和动态分析。
从准备执行恶意软件(包括内存分配和 API 解析)到纠正执行参数的详细过程确保准确模拟恶意软件的行为。
我们对有效载荷的调查揭示了它与 C2 服务器的复杂通信,包括使用 SealMessage() 和 DecryptMessage() API 的安全加密和解密机制。
最后,我们确认了该恶意软件在受感染系统上的重要功能,例如屏幕捕获、远程服务器功能以及通过服务控制管理器 API 操纵系统服务。
原文始发于微信公众号(Ots安全):深入研究没有 PE 头的转储恶意软件
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论