山寨mathjs的npm包里,求解器多出一行可疑调用
2026年9月18日,供应链安全公司SafeDep团队发表调查报告,宣布在npm包mathmain中发现了一个远程访问植入体。该包伪装成流行的数学库mathjs,恶意代码以加密形式分发:调用者用能产生特定密码的矩阵数据激活加载器,密钥匹配时包体解密载荷并运行它;植入体随后从操作者处接收加密代码并在主机上启动,命令通道用一个公共聊天服务和一个区块链网络。SafeDep从9月17日开始分析mathmain,该包看起来就是换了个名字的mathjs,代码经过混淆,求解器里多出的一处调用把他们引向了加载器。
密码不在代码里,它藏在矩阵分解的结果中
加载器的入口是CommonJS构建里lusolve求解器末尾多出的一次调用:求解器算完结果后,把下三角矩阵的数据传给一个名为removeSolveValidation的函数,而这个函数最终落在is.js里新增的isGraph检查上。isGraph把输入转成JSON字符串当作密码,先用它解密出一个文件名,再把文件路径和密码交给下一步,然后require加载返回的路径。解密链路很标准:用scrypt把密码派生为256位密钥,再以AES的256位GCM模式解密;加密数据的布局固定为16字节盐、12字节初始化向量、16字节认证标签,密文跟在后面,整体以base64文本存储在包里。包里找不到明文密码,也没有安装钩子,普通导入根本不会激活加载器。
关键在于密码本身。SafeDep先测了16922个候选密码,又在第二轮对全部五个加密块测了533个候选,包括带零对角元的矩阵、常见口令和求解器测试用例里的数值数组,全部没有通过认证检查。突破来自JFrog在9月21日公布的独立分析:触发输入是一个3行3列的帕斯卡矩阵,三行分别是1、1、1,1、2、3,1、3、6;把它传给lusolve做LU分解后,下三角因子的三行分别是1、0、0,1、1、0,1、0.5、1,其JSON形式就是密码。SafeDep用自己的代码复现了这一结果:密码不仅解密出文件名graph.js,还解密了mathmain与math-universe中的三份载荷文件,其中mathmain的载荷与math-universe的版本字节级一致,同一密码解锁了整个家族。
植入体的命令通道:Slack、Telegram与测试网智能合约
解密后的文件组成一个小型远程访问植入体,每个文件只干一件事。第一阶段的graph.js被加载器require执行,它用os和fs读取主机数据,用Node的crypto模块生成X25519密钥对并完成密钥协商,用child_process启动分离的Node进程并管理子进程,还内置了一份ethers库去读取Base Sepolia测试网络上的智能合约,并向Slack的chat.postMessage接口与Telegram的API上报。网络细节以base64文本形式藏在文件里,包括测试网合约地址、Infura与Alchemy两条RPC端点,以及Slack与Telegram的机器人令牌,SafeDep对后两者做了打码处理。第二份文件是ethers库的拷贝,携带5.7.2版本标记;第三份是Slack代理,它从启动参数读取共享密钥、父进程ID与触发密码,每10秒轮询一次会话历史接口,按发送者过滤后把消息文本解密成JSON数据包,起始包打开传输、分块包提供文件内容、结束包负责拼接,最终把结果写入磁盘并用Node启动。
传播面的统计令人不安。SafeDep在9月19日查了npm下载量,统计区间为9月12日至18日:mathmain约60.5万次,mathsbase约192.3万次,math-universe约56.9万次。团队强调,这些数字不能说明有多少系统真正装上了这些包,也不能说明加密代码是否实际运行过,公开的依赖检索在其覆盖范围内没有找到任何消费者。同样值得警惕的是,mathsbase与math-universe各自关联的公开GitHub仓库中,被审查的提交都不包含加载器——npm构建里有,GitHub源码里没有,这既说明不了是谁在什么时候插入的,也说明只查默认版本会漏掉问题:9月17日npm把mathmain的1.0.2作为默认发行版,而加载器在1.0.1里。
时间线显示这是一场持续一个多月的投放。8月26日mathsbase的1.0.0发布,没有匹配的加载器;8月27日mathsbase的1.0.1带上加载器与加密文件;9月15日mathmain的1.0.0发布且不含匹配加载器;9月16日mathmain的1.0.1与另一个后续版本陆续上线并带加载器;9月17日mathmain的新版本带匹配文件发布,而当天npm默认提供的恰恰是不含加载器的1.0.2。SafeDep在报告末尾给出了完整的入侵指标,包括包名、加载器文件哈希与解密后的植入体特征。截至报告发表,团队没有在公开代码中找到实际调用求解器并传入触发矩阵的调用方,因此无法确认攻击是否已经发生,也不知道操作者通过这些通道下发了什么代码。