像最近大家耳熟能详的PHP这种重大漏洞,公开后的几天里面肯定有成千上万的安全从业者同时搜索同一个关键词,毕竟每个人都急于验证复现然后打肉鸡,这个时间压力会系统性地压制人的审查本能,平时谁都不会运行三无仓库里的 exe,但是热点期不管是哪个行业都容易病急乱投医(
从这次分析恶意poc也能看出来黑产的成本非常低,exe是builder白模板提前造好的,热点来了只需要下载真仓库-塞进去-改一行 README就可以推送出去了,推送渠道我在telemetr上查到了”第一时间推送 github 安全相关项目”的频道是来源,仓库描述就是喂给这类爬虫的关键词饵料,只需要让机器人觉得他值得转发。
当然TG推送频道只能算其中一环,同期一篇传播很广的复现文章结尾就是典型的公众号玩法,对应的exp,关注微信公众号私信*****获取,加上pocindex/trendshift这类trending聚合站,一个命中关键词的新仓库会被多层自动化系统接力广播。已经算是边际成本趋近于零的生意了,从自己经验来看,能下这类木马的都是常年关着杀软来方便测试本地管理员权限,从投入产出比看PoC投毒可能是现存性价比最高的初始访问手段(?)

对比正版POC(不知道为什么不直接下正版),多出来的东西是一个被打包成了exe的rust马,还有就是README里的频道引流,账户名字是ai智能体,名下全是百度/必应SEO排名工具和代理池软件,很像一个做流量灰产的中文运营者,真仓库上线第二天迅猛地花24分钟镜像加料建仓,然后通过TG安全推送聚合频道自动广播出去钓人,哎太坏了
这篇更多的是分享一下自己做静态分析的思路
分析
file exp复现执行.exe
exp复现执行.exe: PE32+ executable (GUI) x86-64, for MS Windows, 6 sections
GUI子系统在双击运行之后不会弹黑色控制台窗口

objdump -x拉了下结构总览
start address 0x0000000140001010
Time/Date Mon Sep 7 18:52:36 2026
.rdata 00005c00 0000000140049000 0000000140049000 00048400 2**4

这组映射是后续所有VA换算的锚点,差值0x140000c00,后面每次把虚拟地址换算成文件偏移都要用它
接下来的字符串榨取非常古法人工,其实直接喂给AI就能跑出来个大概,但是老祖宗的古法分析不能忘.jpg
strings -a -n 4 exp复现执行.exe > str_ascii.txt
strings -a -e l -n 4 exp复现执行.exe > str_utf16.txt
python3 cjk_strings.py exp复现执行.exe
然后按攻击者的工程结构分桶归类,协议层/指令层/模块层/运营层分完类
迫真大鱼
grep -n "execlsgetdisk" str_ascii.txt
execlsgetdiskcateditmvrmrmlistmkdirtouchmodifytimewgetuploaddownloadrunplugininstallscreenshotdownloadtoosstunnel_connecttunnel_data
加上旁边单独的exit,23条远程指令一次性全拉出来了,命令词连续堆放的布局就是指令分发表的字符串池,样本的能力边界基本被它定义的。
C2 信道类型在这里就可以下注
endpoint accessKeyId accessKeySecret bucketName
Authorization: OSS
ThuFriSatSunMonTueWed JanFebMarAprMayJunJulAugSepOctNovDec
{"type":"heartbeat","ping":
{"type":"result","taskId":"
{"type":"tunnel_up","target":"
taskId command taskType
endpoint accessKeyId accessKeySecret bucketName 四字段连排是对象存储认证的标准四件套,Authorization: OSS 是阿里云对象存储的签名格式(AWS S3 是 Authorization: AWS,区分特征)
旁边是星期表和月份表,HTTP Date头的GMT格式化素材所以是手写的HTTP协议栈,协议报文模板能分析出来是心跳/result/tunnel_up隧道上行/taskIdcommandtaskType,后面跟着的转义符表说明JSON是手工拼接的。

还有一个错误码体系,像E010-E021、E030、E040、E041、E050这些,也说明了开头说的这是一个相应迅猛的团队作品…..
E002:unknown task type → 证明任务分发表存在
E021:missing bucket → OSS 配置完整性检查
udp:E040:empty target → 前缀提示隧道有 UDP 通道
E041:load3/entry/invoke → CLR 加载的三步结构
中文字符串四条
- 权限不足
- 已改为当前用户登录启动
- 需要管理员权限才能安装系统服务
- 系统找不到指定的路径
前三条是构成持久化双档降级链,第四条是中文版Windows的本地化错误串,中国制造
区分作者模块与标准库的方法是看路径前缀,带/rustc/的都是编译器自带,裸文件名才是作者工程
grep -oE "[a-z_]+\.rs" str_ascii.txt | sort -u
aead.rs clrhost.rs conpty.rs dns.rs encoder.rs fileops.rs guard.rs
huffman.rs id.rs image_buffer.rs kdf.rs memexec.rs oss.rs peload.rs
screen.rs tunnel.rs udp.rs writer.rs ws.rs

cargo workspace多crate工程,模块划分直接可读。
rustc指纹是panic路径里的commit哈希
grep -oE "[0-9a-f]{40}" str_ascii.txt | head -3
88d9e12ae178fab0fb5cc050a94da85685d449ea
查证对应rustc 1.98.0,工具链构建于2026-08-18,与PE时间戳2026-09-07合读,编译早于漏洞公开约六周,所以蹭热点的只有投放动作,每种编译器都有自己的胎记,Rust是panic路径的commit,Go是版本串和 buildinfo,.NET是元数据版本。
内嵌PowerShell两段
(Get-Item '…').LastWriteTime = [DateTime]::new(1970,1,1,…).AddSeconds(…)
Invoke-WebRequest -Uri '…' … -UseBasicParsing
前者是timestomp反取证,对应modifytime指令,后者是下载通道。
这里还突然刷出来了消息类型词汇表
heartbeat_ack notask pty_close screen_start screen_stop screen_input
tunnel_down pty_open delfile delprocess quality normal pty_input
pty_ 三件套配conpty.rs说明它能开交互式伪终端实时收发按键回显,screen_start/stop/input`说明截屏是可持续推送的屏幕流加远程键鼠。
还有aead.rs加kdf.rs密钥派生是作者自有模块,OSS信箱之上很可能叠了端到端加密,以及三dns.rs加udp.rs加 DoH 模板https://…/dns-query加udp:错误前缀,很明显的DNS/UDP副通道。
导入表精读
objdump -x exp复现执行.exe | grep -A30 "DLL Name"
静态导入名单相当良民,KERNEL32常规函数、WS2_32 全套 socket、bcrypt 随机数、CRT 运行时。ADVAPI32确实出现在导入表里,但是唯一导入函数是:
DLL Name: ADVAPI32.dll
vma: Hint/Ord Member-Name Bound-To
43aa8 503 SystemFunction036

SystemFunction036即RtlGenRandom,取随机数,这是Rust标准库的固有链接,不是作者手写的注册表/服务调用,KERNEL32 里的IsDebuggerPresent和AddVectoredExceptionHandler同理是 Rust 运行时标配。
这一步我自己的通用判断法则是当样本声明的能力和字符串证据证明的实际能力出现有落差,那落差部分就是反分析设计所在。
字符串层已证明它有截屏/服务/注册表/内存执行能力,对应API一个都不在导入表里,因为敏感API全部都要走运行时动态解析,只看导入表是不对的
反汇编定点
objdump -d -M intel exp复现执行.exe > disasm.txt
wc -l disasm.txt
80121 disasm.txt
x86-64引用数据用lea reg,[rip+offset],objdump会在注释里算出目标绝对地址,所以谁引用了这个字符串=grep那个地址,这里踩了两个坑,一个是给锚点地址加了对齐假设,要求末位为 0导致零命中,引用地址就是数据的真实地址没有任何对齐保证,第二个是漏看了objdump注释带0x前缀(
两个锚点命中
grep -n "14004dbb4" disasm.txt # 疑似配置区
grep -n "14004e2a0" disasm.txt # 疑似加密数据
24501: 140009e51: lea rcx,[rip+0x33d62] # 0x14004dbb4
23785: 140009b3c: lea r8,[rip+0x3475d] # 0x14004e2a0
窗口一,0x140009af3是一条动态解析流水线,关键行:
140009af3: push 0xd
140009af5: pop rsi ; 长度 13
...
140009b3c: lea r8,[rip+0x3475d] # 0x14004e2a0 ← 密文源
140009b52: movzx edx,BYTE PTR [r8+rcx*1]
140009b57: xor dl,0x5a ; XOR 0x5A 还原
140009b5a: mov BYTE PTR [rsp+rcx*1+0x30],dl
140009b5e: inc rcx
140009b61: cmp rcx,0xd
140009b65: jne 0x140009b52 ; 恰好循环 13 次
...
140009b8a: mov edx,0x7a120602 ; ror13("getadaptersinfo")
...
140009bc2: xor ecx,ecx ; rcx = NULL
140009bc4: lea rdx,[rsp+0x28] ; rdx = &size
140009bc9: call r11 ; GetAdaptersInfo(NULL, &size)
开头装入长度13,分配缓冲,指针/容量/长度三联写栈,RustVec

解密后字符串交给内部的按模块名取句柄函数,再按哈希0x7a120602解析导出函数,最后以 rcx=NULL、rdx=&size调用GetAdaptersInfo,经典两次调用法的第一步,先问系统网卡信息需要多大缓冲。
DLL名用XOR藏,API名用哈希藏,两套混淆手法叠在同一条解析链上各防一类分析手段 窗口2是0x140009e4b,回答了之前的一个误判,那块高熵数据最初因为熵高被假设为加密配置,但是引用它的代码是一个查找循环:
140009e51: lea rcx,[rip+0x33d62] # 0x14004dbb4 ← 表基址
...
140009e7c: mov edx,DWORD PTR [rcx+rdx*1] ; 逐项取出 4 字节
140009e7f: cmp edx,r10d ; 与 COMPUTERNAME 哈希比
140009e82: je 0x140009ea8
140009e84: cmp edx,r11d ; 与 USERNAME 哈希比
140009e87: je 0x140009ea8
...
140009e91: cmp rcx,0x1c ; 28 项上限
...
140009ea8: inc r13d ; 命中 → 处置计数 +1
全程没有任何解密动作不是密文,其实是一张28项×4字节的预计算哈希黑名单表,命中分支落到inc r13d处置计数加一,配合字符串1exit2delay3degrade闭环,命中一项黑名单计数累加,决定退出/延迟/降级。
XOR 加密字符串
grep -n "xor.*0x5a" disasm.txt
扫出11个命中点,看上下文后呈现三种形态

第一种是内联解密循环,硬编码地址加长度,`iphlpapi.dll和cmd.exe /k属此类
第二种也是内联循环但逐字节String::push追加,0x14004db39长度16解出:Zone.Identifier,第三种0x140045c1a处的循环里长度来自rsi、数据源来自 rdi,全是参数没有常量,是一个参数化的通用解密函数,入口0x140045bc2,解密完紧接调用按名取句柄的解析函数,是解密→解析一体化封装
grep -n "call.*0x140045bc2" disasm.txt
8312: call 0x140045bc2
14980: call 0x140045bc2
15203: call 0x140045bc2
20776: call 0x140045bc2
21341: call 0x140045bc2
21458: call 0x140045bc2
22090: call 0x140045bc2
7 个调用点,按参数约定,rdx=密文指针,r8=长度,从调用点前文提取锚点,归并出5个不同密文地址,数据布局上有个细节是0x14004df76恰好等于0x14004df6b加11,前一条密文的终点就是后一条的起点,加密字符串紧密相邻存放,是builder 批量加密写入的典型痕迹。
用VA→文件偏移换算逐个解出:
python3 xor_decode.py
0x14004e2a0 (13): iphlpapi.dll
0x14004dcf8 (10): cmd.exe /k
0x14004db39 (16): :Zone.Identifier
0x14004df6b (11): user32.dll
0x14004df76 ( 9): ole32.dll
0x14004df80 (10): shlwapi.dll
0x14004df8b (11): mscoree.dll
0x14004df97 ( 9): amsi.dll
加密字符串全集共8条(这个后续我OPSEC自审阶段又补出第9条
cmd.exe /k是杀软特征库命中率最高的命令行模式之一,加密它是了为藏特征。:Zone.Identifier是NTFS备用数据流名,Windows用它给网络下载的文件打Mark-of-the-Web标记,操纵它就是给落地载荷洗掉来自网络的出身,来绕过SmartScreen警告链。
最后三条连成完整的内存执行杀链,amsi.dll先瞎掉反恶意软件扫描接口,ole32.dll初始化 COM,mscoree.dll把.NET CLR运行时请进进程,三步之后内存加载的.NET 插件在杀软失明,不落盘无进程创建的三重掩护下运行,与 clrhost.rs/Iwsp程序集名、E041三步错误码严丝合缝。
所以他们思路是不搞全量加密,只对少数最扎眼的字符串动刀,这样点状混淆成本极低
ror13 批量反查
哈希反查的方法论是识别算法→确认规约→字典爆破,用Python现算:
python3 -c "
def ror13(s):
h=0
for c in s.lower().encode():
h=((h>>13)|(h<<19))&0xFFFFFFFF; h=(h+c)&0xFFFFFFFF
return h
print(hex(ror13('kernel32.dll')), hex(ror13('getadaptersinfo')))"
0x8fecd63f 0x7a120602
与样本中出现的两个立即数逐位相符,算法与规约双双确认,然后从8万行反汇编提取全部 mov ecx/edx, imm32立即数,用词表建反向索引批量反查:
| python3 ror13_batch.py # 正则 r”^\s([0-9a-f]+):.\bmov\s+(?:ecx | edx),0x([0-9a-f]{6,8})” 扫 disasm.txt |
共提取立即数 116 个,词表 68 条,命中 51 个
三层构造:
锚点层,九个 DLL 名,验证算法用;证据驱动层,字符串层已证明的每项能力反推对应 API 家族,截屏推 GDI 链、中文双档提示推服务与注册表族、memexec.rs 推 Virtual 四连;变体层,A/W 后缀与Ex变形,词表质量取决于与样本证据的咬合度。
命中名单按首次出现地址排序后功能族的边界在地址空间上自己现形:
0x1400099xx-0x140009bxx GetAdaptersInfo / GetProcessHeap …… 主机指纹模块
0x140013cxx CreateServiceW OpenSCManagerW …… 服务体系(档一)
0x1400149xx RegCreateKeyExW RegSetValueExW …… 注册表族(档二)
0x14001c3xx-0x14001c5xx GetDesktopWindow→GetDC→CreateCompatibleDC
→CreateCompatibleBitmap→BitBlt→GetDIBits→ReleaseDC
0x1400220xx/0x1400221xx 服务/注册表第二簇,与上面互相缠绕,双档持久化的代码形态
0x140045fxx-0x1400462xx CreatePipe CreateProcessW GetStdHandle SetStdHandle

XOR 解密点与哈希解析点在地址上是邻居
ole32.dll的解密调用在0x140025184,CoCreateInstance的哈希解析在0x14002518c,相距8字节;shlwapi.dll`的解密与解析同样首尾相接。解密DLL名→加载→按哈希取函数的流水线在两处代码里完整现身,静态导入表干净的原因彻底闭环。
另外StartServiceCtrlDispatcherW的解析发生在0x140001018,程序入口附近样本启动时先尝试以服务模式运行,GUI静默外表与服务驻留里子从进程第一分钟就是双轨,投放通道与权限提升通道共用一个载荷。
词表之外还有第四条反查路径,持久化精读时遇到四个词表外哈希,用调用点的槽位与入参上下文约束候选集再爆破:
0x45a4afed → OpenServiceW (存进 [rbp+0x1700] 的槽位,后来在"服务已存在"分支被以全权调用)
0xddb5bc6c → CloseServiceHandle (入参为 SCM 句柄的槽位)
0x966b458c → ChangeServiceConfig2W
0x35e27fee → RegCloseKey
错误码分流的指令级闭环
ror13名单显示服务体系与注册表簇地址互相缠绕,取CreateServiceW解析点0x140013ce9与 RegCreateKeyExW解析点0x1400149e4各±40行窗口。
第一个窗口是典型的一次备齐全部弹药,六个服务管理API一口气解析完毕,每个失败都跳同一撤离点,然后才开第一枪OpenSCManagerW(NULL, NULL, 3),权限位3=连接服务管理器加创建服务,第二个窗口揭示了双档切换的真实机制,比预想更讲究一点
call CreateServiceW
...
call GetLastError
cmp eax,0x431 ; 1073 = ERROR_SERVICE_EXISTS
je 0x14001… ; → OpenServiceW(SERVICE_ALL_ACCESS=0xf01ff) 全权接管
...
cmp r13d,0x5 ; 5 = ERROR_ACCESS_DENIED
je 0x14001… ; → 落入注册表 Run 键路径
代码里没有任何我是不是admin的权限预检,先试装服务再取 GetLastError按错误码分流,服务已存在就用OpenServiceW以全权接管,安装器因此幂等,重复投放不露馅还顺手完成载荷更新;权限不足才落入注册表路径,写当前用户Run键。
这样不做权限预检就少调一串Token查询API,行为特征更小,先试高档,失败按错误码降级对受害者环境零假设,普通用户双击和管理员双击都能落地,ChangeServiceConfig2W暗示写服务描述伪装或配置失败自动重启,是驻留韧性加分项;幂等安装对”热点期批量投递、总会有人点两次”的场景是刚需。
OPSEC自审
破解黑名单,看一下攻击者怕什么环境,把28项查找表的原始哈希从文件偏移0x4cfb4抠出(struct.unpack_from("<28I", data, 0x4cfb4)),拿沙箱与工具名词表分别用ror13、djb2、crc32试撞:
python3 crack_blacklist.py
ror13 命中 4/28:
sandbox
malware
virus
cuckoo
djb2 命中 0/28
crc32 命中 0/28
环境检查的哈希函数还是ror13,作者一个算法全工程复用,四个命中词全是沙箱默认用户名/主机名(Cuckoo沙箱默认用户就叫 cuckoo),结合窗口二的证据,被哈希的是 COMPUTERNAME/`USERNAME,这个木马的环境检查,只看机器叫什么名字不查虚拟机驱动,也不查调试器进程。
所以他们防的是云沙箱批量检测
还有心跳常数,两处mov ecx,0x1388命中后精读上下文
mov ecx,0x1388 ; 5000 ms
mov edx,0x5dc ; 1500
...
div rsi ; ÷1000
imul rax,rax,0xf4240 ; ×1,000,000 → Duration::new(秒, 纳秒) 构造现场
0x1388与0x5dc并排传入同一调用,随后的div与imul是 Rust Duration的构造现场,心跳是5 秒基准加 1.5 秒抖动
AEAD 算法识别,看能不能解它的流量
python3 -c "
data=open('exp复现执行.exe','rb').read()
print('AES S-box:', data.find(bytes([0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5])))
print('ChaCha :', data.find(b'expand 32-byte k'))"
AES S-box: -1
ChaCha : 297424 (= 文件偏移 0x489d0)
AES 的 S-box 固定字节序不存在,ChaCha20的expand 32-byte k命中于文件偏移 0x489d0,aead.rs实现ChaCha20-Poly1305,这个无AES指令集也快,是近年新写木马的主流选择,配合kdf.rs密钥派生
ZZZZZZZ还原 builder工作流,字符串层记录的ZZZZZZZ(VA 0x14004e098)有三个引用点:
grep -n "14004e098" disasm.txt
21458: 14002516a: lea rax,[rip+0x28f27] # 0x14004e098
22090: 1400253d2: lea rax,[rip+0x28cbf] # 0x14004e098
23117: 1400255f9: lea rax,[rip+0x28a98] # 0x14004e098
三处全落在 CLR 宿主模块区,三个函数形态完全一致,包装两个常量、双比较、命中则lock inc原子自增引用计数,是同一个检查字段是否为默认值的泛型函数对不同配置字段的三份单态化实例。
Z的ASCII码是0x5A,XOR密钥恰好也是0x5A,0x5A异或 0x5A 等于零,ZZZZZZZ是一段全零数据加密后的样子,配置槽出厂即空,builder投放前把真实配置XOR灌装进去,builder白模假设由此拿到新的证据,我们分析的是出厂
投放按目标客户逐份灌装,配置块按 16 字节定长字段组织,包装函数0x1400253b7内是16轮就地XOR,空槽与加密数据交错存放,其中一段高熵尾巴解密后是oleaut32.dl,加密DLL名单扩到第9条。
三个函数各自配对的第二常量,各16字节高熵数据做了最后一轮尝试:
python3 xor_brute256.py
0x14004e0a8: 256 个密钥全部无可打印解
0x14004df00: 256 个密钥全部无可打印解
0x14004e0f0: 256 个密钥全部无可打印解
要么是多字节加密要么是随机令牌
