Hacker News 中文摘要

RSS订阅

为什么 mathmain 需要一个加密加载器 -- Why does mathmain need an encrypted loader?

2026年9月17日「安全依赖(SafeDep)」分析「数学主(mathmain)@1.0.0」时,我们发现这个复制流行「数学库(mathjs)」的 npm 包藏有远程访问植入程序。恶意代码加密发送,休眠至程序用库解出特定方程,该方程即密钥。匹配后包解密载荷运行,载荷从攻击者获取命令在主机执行,用公共聊天与区块链作命令通道。包为改名混淆的「数学库(mathjs)」副本,求解器额外调用引出加载器。

正文

我们在 mathmain@1.0.0 这个复制了流行 mathjs 库的 npm 包里,发现了一个隐藏的远程访问植入程序。恶意代码以加密形式发布,在程序用该库解出特定方程前一直休眠,那个方程就是密钥。密钥匹配时,包会解密载荷并运行,载荷从攻击者处接收命令并在主机上执行,使用公共聊天服务和区块链网络作为命令通道。以下展示我们如何发现加载器、如何解密、载荷的作用以及可用于查找它的指标。

求解器调用类型检查

2026年9月17日我们从 SafeDep 对 mathmain 的分析开始。包像改名的 mathjs 且代码混淆,solver 里一个额外调用引我们找到了加载器。

在 lusolve() 末尾的 CommonJS 构建里,我们发现了一个额外调用。求解器算完结果后,把下三角矩阵数据传给了 removeSolveValidation():l && (q = removeSolveValidation(l._data)); return x;,其中 l 是下三角矩阵,x 是结果。求解器返回不变的 x,把额外调用返回值赋给 q 却不再使用。

跟进到了 lib/cjs/utils/is.js 里的 isGraph(x)。该文件含 isMatrix、isNumber 等检查,加的 isGraph() 解密并加载代码:它把输入转 JSON 字符串作密码,先解密文件名,再把路径和密码传给 event(),用 require() 加载返回路径。

矩阵数据变成密码

在 lib/cjs/utils/event.js 里找到解密函数,用 scrypt 把密码转成 256 位密钥,再用 AES-GCM 解密。加密数据布局固定:16字节盐、12字节初始化向量、16字节认证标签,之后是密文,整个序列存为 base64。

通过求解器的调用,密码是 JSON.stringify(L._data)。调用者可透过 lusolve() 对象形式提供 L,因此必须传能产生正确密码的矩阵数据,可见加载器里没存密码。加密文件名有8字节密文,怀疑命名 graph.js,但因没拿到密码未能确认。

加载器写出并运行文件

event() 辅助函数解密文件、写盘并返回路径:若文件名无 enc_ 前缀,就用解密代码覆盖加密文件,isGraph() 里的 require() 随后加载,代码以 Node.js 进程同等权限运行。

三个加的文件含 base64 数据而非正常 JS:graph.js 20918 字节、fraction.js 9084 字节、bignumber/type.js 1179416 字节。后两个在可见加载器里无引用,解密的 graph.js 会作为后续阶段加载它们。

manifest 里无安装钩子,通过审查路径导入包也不激活加载器。求解器须先通过验证和计算才能到额外调用;密码错则 validEvent() 在写文件前认证失败。

同样的加载器出现在另外两个包中

搜 npm registry 找到 mathsbase 和 math-universe。五个版本里加载器文件、触发代码、求解器改动、两个大加密文件都一致。mathsbase@1.0.1 的加密 graph.js 与其他不同。9月17日 npm 把 mathsbase@1.0.2 作默认发布,该版无加载器,只查默认版会漏掉 1.0.1 代码。

9月19日查三包下载量(9月12-18日):mathmain 605,157,mathsbase 1,923,059,math-universe 569,730。npm 还报 9月17日整 registry 下载为零,使前述 mathmain 零不可靠。这些包无公开依赖者,jsDelivr 上也无流量,计数不代表真实安装,无法确定何来此量。

加载器不在 GitHub 源码里

mathsbase 和 math-universe 各链公共 GitHub 仓库,我们阅毕,均无加载器。公共 math-universe 源码求解器结尾是 if (q) { x.data = csIpvec(q, x.data); } return x;,无 removeSolveValidation() 调用,npm 构建却有,说明发布时加的。

我们也找用触发器调用求解器的程序,搜 GitHub 代码、lockfiles、依赖服务,未果。私有项目及搜索引擎漏掉代码不可及。

最初尝试未找到密码

我们测 16,922 个可能密码对抗加密文件名,又测 533 个对抗五个不同加密 blob(含旧 graph.js),试了常用密码和求解器测试里的数字数组,都没过认证。密码非常规值或测试矩阵,是后来找到的特定矩阵。

破解加密

密码是矩阵下因子 L 的 JSON 形式。JFrog 首先报告产生它的输入,我们对照 mathmain 文件重现。

恢复的触发器是 3x3 帕斯卡矩阵 A = [[1, 1, 1], [1, 2, 3], [1, 3, 6]]。调用者把 A 传给 lusolve(),求解器做 LU 分解,下三角因子 L 变 [[1,0,0],[1,1,0],[1,0.5,1]],加载器把 L 转 JSON 串作密码。

我们确认密码解密出文件名 graph.js,也解密了 mathmain 和 math-universe 三个载荷文件,两包载荷逐字节一致。mathsbase@1.0.1 用同密码不同加密数据,同一密码解锁整个家族。

载荷分析

解密文件组成小型远程访问植入程序。解密 graph.js 是第一阶段,加载器解密后 require() 运行:用 os 和 fs 读主机数据,用 Node crypto 生成 X25519 密钥对,用 child_process 跑 shell 命令,用捆绑 ethers 读 Base Sepolia 测试网智能合约,向 Slack 和 Telegram 报告,再加载 bignumber/type.js 和 fraction.js。

解密 bignumber/type.js 是 ethers 库副本(标记 ethers/5.7.2)。

解密 fraction.js 是命令代理:运行时解密 Slack bot token 和 channel id,需环境变量 CHAT_PASSWORD,无则退。每10秒轮询 Slack conversations.history,验毕来自操作员则用 execSync 或 spawn 执行消息内容作 shell 命令。包不存命令,代理运行时从 Slack 读。这些文件让操作员在触发加载器主机上跑 shell 命令,命令经公共聊天服务和区块链测试网传输。

触发器如何触及受害者

加载器休眠直到调用者用能产生因子 L 的输入运行求解器。普通导入到不了附加代码,普通 lusolve() 调用因认证失败不写文件。密码是 L 的 JSON 形式,帕斯卡矩阵非唯一触发器;调用者可把 L 作系数矩阵传,或透过对象形式提供 L,都得同密码。

攻击分两部:一包含加密载荷,另一包或受损调用者用触发矩阵运行求解器。数学库看似流行可信依赖,调用者提供密钥。公开代码里未找到该调用者。

调查时间线

| 日期和时间 | 事件 | | --- | --- | | 2026年8月26日 08:14 | mathsbase@1.0.0 发布,无匹配加载器 | | 2026年8月27日 07:44 | mathsbase@1.0.1 发布带加载器和加密文件 | | 2026年9月15日 03:38 | mathsbase@1.0.2 发布无匹配加载器 | | 2026年9月16日 12:06 | math-universe@1.0.0 发布带加载器 | | 2026年9月16日 13:39 | math-universe@1.0.2 发布带加载器 | | 2026年9月16日 14:14 | math-universe@1.0.1 发布带加载器(在1.0.2后) | | 2026年9月17日 06:53 | mathmain@1.0.0 发布带匹配文件 | | 2026年9月17日 | 源码审查、消费者搜索及进一步解密尝试完成,未恢复明文 | | 2026年9月19日 | npm 报 mathmain 9月18日下载 605,157 | | 2026年9月21日 | JFrog 发布分析,恢复触发矩阵 | | 2026年9月21日 | SafeDep 重现解密并读取 mathmain 载荷 |

入侵指标

恶意包

SafeDep 分析了这些版本的加载器,归档 SHA-256 来自 npm tarball。

| npm 包版本 | 归档 SHA-256 | | --- | --- | | mathmain@1.0.0 | 1723a0df210ac61281a504f3a07ec3605d20151631e0635cc344cacc71019135 | | mathsbase@1.0.1 | 03e13cdedd9c33e6fed25092b1ec7dcf11cc5962c0fbb6b3e90ba95bfec1b034 | | math-universe@1.0.0 | 7e5e1bcdc6a7b0e3437269a236b49ef2be4f77081c7de5130d503c103fd6be69 | | math-universe@1.0.1 | bfe772e7ee044fd6f0bdf53e83f44aad7c9ee1925baf0cf4d884a312aa9ba50e | | math-universe@1.0.2 | 4eb1d59df7dc80dbe3ec154481e61e8824422037092c188f0cc146b543615a66 |

触发器和密码

调用者用一矩阵激活加载器,密码是该矩阵 LU 下因子的 JSON 形式。

| 项目 | 值 | | --- | --- | | 触发矩阵 | [[1,1,1],[1,2,3],[1,3,6]] | | 密码 | [[1,0,0],[1,1,0],[1,0.5,1]] | | Stage-1 blob | IapMCmvlemBnFaU+3GZ4oF2xOhnczTlDWTO3oCfrHkWp1lSpHdCaeG0qn2neIoTetyRJtQ== |

加载器文件

这两文件哈希跨所有分析版本匹配。

| 文件 | SHA-256 | | --- | --- | | lib/cjs/utils/event.js | ab66c98e8ed5235feb963ec8845765f62f5f26b1c58c266c409767e53bcb5ccd | | lib/cjs/utils/is.js | 5d9e952c51875d2b897eedc22b002b94ab21c8004d513bc99ce3a885f8a01dae |

加密载荷文件

base64 blob 位于 lib/cjs/utils/ 下。mathmain@1.0.0 和三个 math-universe 版共享一套,mathsbase@1.0.1 发不同 graph.js blob。

| 文件 | SHA-256 | 部署 | | --- | --- | --- | | graph.js | ed9b078594393d09d91ee008366ca75e3017cca18c79af99c5b294c61db67f06 | A | | fraction.js | 09773ee7db70216b778b15cfcd94cb1df8963eddd4a47b801c96f986651699e6 | A | | bignumber/type.js | ca4fe552da461fec5b51d5964d979699f06888499be442f209125853dff3e0e1 | A | | graph.js | 0aa46d32e4b479cc09f97cc66a1f12ca96b0497a7eb6ffc6b46ed3ef80e3c83b | B |

解密后的载荷文件

这些哈希覆盖我们从部署 A 恢复的明文,运行过加载器的主机磁盘上可能有这些文件。

| 文件 | SHA-256 | | --- | --- | | graph.js | 1e0f09c84aaf573627c003ce0f086517c3ea980cbea02f8ff918b1cc0d7e0bbb | | fraction.js | 6fd655d7196880fc5783f9dbb62b428baf220c2781970be54044376330be7af3 | | bignumber/type.js | 6b1ad71bc3765dd272ea2ac63c1ea6ed97091ba0067d0b3e2294e1b34177cb25 |

命令与控制

我们从解密 graph.js 恢复这些端点。部署 A 含 mathmain 和 math-universe,部署 B 含 mathsbase@1.0.1,两个 bot token 密半部隐去。

| 部署 | 类型 | 值 | | --- | --- | --- | | A | 智能合约 | 0xac0bfC4C48A679b667732128278EACBA1c191894 (Base Sepolia) | | A | RPC (Infura) | base-sepolia.infura[.]io/v3/dc7257d09fab42eca2c354c32fec1938 | | A | RPC (Alchemy) | base-sepolia.g.alchemy[.]com/v2/D2-TbkB2m05WXSnSDOCDI | | A | Telegram bot | 8961878831 (token redacted) | | A | Slack bot | xoxb-11307403103236-11289767127959-... (secret redacted) | | B | 智能合约 | 0xE390863Dac96a7118C71227C2b099B50cF602D31 (Ethereum Sepolia) | | B | RPC (Alchemy) | eth-sepolia.g.alchemy[.]com/v2/D2-TbkB2m05WXSnSDOCDI | | B | Slack bot | xoxb-11307403103236-11289767127959-... (secret redacted) |

两部署用相同 Alchemy 项目密钥 D2-TbkB2m05WXSnSDOCDI,关联同一操作员。

评论

多数人质疑 npm 平台监管缺位与攻击触发逻辑的不透明,少数人主张彻底放弃依赖或转向 ESM 以规避风险,同时对文章疑似 AI 撰写有疑虑。

疑惑特定 3x3 矩阵触发与目标选择

  • 用户好奇为何用特定矩阵作为触发条件,猜测可能针对某类数值分析者,另有人感叹目标筛选异常精细(少数人) > 为何偏偏是这个特定的 3x3 矩阵成为攻击触发?他们是在找做某种数值分析的人吗?(@j2kun)

质疑 npm 平台与执法跟进缺失

  • 指出恶意包仍挂在 npm 无警告,而 GitHub 已删库,质疑执法是否将后门视为犯罪(少数人) > 这个包还在 npm 上没有任何警告,但作者 GitHub 已经没了,执法机构会跟进吗?(@fshafique)

认为披露文章疑似 AI 生成

  • 有用户表示真正发现会自豪写报告,但该文语气像 AI 写作,削弱可信度(少数人) > 要是我发现这种事会骄傲地写分析,但这篇闻起来像 AI 写的(@zzril)

指出第二阶段载荷破损的反常

  • 有破解者发现后门第二阶段完全失效,使事件更显诡异(少数人) > 有人破解后发现第二阶段彻底坏掉了,这就更奇怪了(@hiddenvulkcan)

主张弃用依赖或淘汰 CommonJS

  • 部分开发者宣称不使用任何依赖、仅手写可担保代码以避风险;另有人呼吁弃用 CommonJS 因其动态 require 难审计,应转 ESM 便于静态分析(少数人) > CommonJS 早该死,ESM 静态导入易分析,我现在先看 JSR 再看 npm(@WorldMaker)

批评原帖标题隐藏包名

  • 用户指出原帖标题未写明具体包名 mathmain 等,后经修改,并提醒网站邮件保护隐藏了版本串(少数人)

有价值的补充

  • JFrog 团队实际完成了密码破解才解锁后续分析,原文未靠前说明(@fn-mote)