安装过程很干净
npm 从七月起默认不再执行依赖包的安装脚本。9 月 17 日,Checkmarx 公布了一场根本不跟这个决定较劲的行动:加载器就坐在 `BTree.prototype.set` 里,也就是这个库存在的理由本身,并在第一次有东西存入值为 100 的键时触发。它在每周两百万次下载之上跑了十一周,指挥通道则是 Ethereum Sepolia 上的一份智能合约。编码代理是最理想的受害者,因为运行代码正是它的工作。Bromure Agentic Coding 在那段代码跑起来之前,就决定它能够到什么。
封堵安装脚本是对的决定,npm 生态在七月做出了它。这场行动把执行的时刻挪到了你第一次 使用这个包的时候:安装依然干净,而扣下扳机的是你的测试套件。
你的代理需要一个有序映射。它从 npm 挑了一个 b-tree 包,加进 package.json,然后
安装。什么都没跑。npm 12 不再执行依赖包的安装脚本,所以没有 postinstall、没有
preinstall、没有构建钩子,没有任何东西可以让生命周期脚本策略去拒绝。代理写下使用
这个新依赖的代码,写一个插入几百行的测试,然后跑起来。
第一百次插入启动了第二个 Node 进程。
Checkmarx 在 9 月 17 日公布了这场行动,
研究由 Bruno Dias 完成。
BleepingComputer 在 20 日跟进报道,
InfoWorld 在 21 日。
这个包叫 indexed-btree,刻意做成让人误认为真正的 sorted-btree,而当 npm 在 9 月
3 日把它下架时,它每周的下载量已接近两百万。它以恶意形态上线了大约十一周。
大家刚刚才建好的那道防线
六月里 npm CLI 翻转了安装脚本的默认值,而
npm 12 在七月交付了这项改动:
依赖包的 preinstall、install 和 postinstall 除非项目逐一具名允许,否则不再
运行,git 与远程 URL 依赖也一并停止默认解析。那篇报道引用的 JFrog 数据,把这三条
路径放在前一年所见恶意 npm 包的约 53%。代理的运行环境也采取了同样的姿态:
--ignore-scripts,或者一个在包管理器看到 tarball 之前就把钩子剥掉的代理服务器。
从下架日往回数十一周,indexed-btree 的第一个恶意版本正落在这轮推行的中间。
于是操作者不再使用安装脚本。加载器住在 BTree.prototype.set 里,也就是这个库存在
的理由本身。当传进去的键是 100,代码就从 extended/sharedLoad.min.js 启动一个
混淆过的第一阶段加载器,作为分离的 Node 进程运行。Checkmarx 的总结精准点出了为什么
这很难提前看见:加载器藏在库自己的方法里,用他们的话说,这是“一种做得很好的手法,
能从标准的污点分析工具底下溜过去”。
看看是什么给它上了膛:一个值为 100 的键,出现在一个以保存键为目的的库里。排行榜会 碰到它,分页索引会碰到它,而一个用循环插入行的测试,会在第一百次迭代碰到它。照着 README 说的方式使用这个包,你就替它上了膛。
十一周,以及一个读起来没问题的仓库
其余的手法,瞄准的正是你自己会做的那些检查。
指挥通道是 Ethereum Sepolia 上的一份智能合约。加载器从那份合约读出下一个地址,生成 一组 X25519 密钥对,从合约取得操作者的公钥,以 ECDH 导出一把 AES 密钥,再解密链上 存放的两段密文来拼出第二阶段。Checkmarx 直接说明了好处:“这项技术比传统 C2 手法更能 抵抗域名或 IP 的下架,因为每当旧地址被拔掉,智能合约就充当指向新地址的指针。”这里面 没有任何东西能给你一个可以拉黑的恶意域名。你拿到的是一个公开 RPC 端点和一个合约 地址,而操作者不必碰任何一份已安装的包,就能把合约重新指向别处。
这个项目看起来是真的。操作者搭了一个 GitHub 仓库,带着看似合理的提交历史和一个相称 的开发者账号,而那个仓库里的代码并不包含恶意的部分。恶意的部分只存在于 npm 的 tarball 里。到 GitHub 上读源码——这正是所有人给的建议——只会确认这个包没问题。
年头也替他们工作。十一周足以让一个包在你安装它的时候已经是个既有依赖,完全不像那种
几小时内就被撤下的遭劫版本。Checkmarx 数出九个行为相同的兄弟包,光 btree-core 就
超过 190 万次下载,其余各自都在数十万之谱,并指出操作者掌握着一个持有 109 ETH 的
钱包。
接着,在第二阶段落地之后,加载器删掉自己的文件,并把触发点从原型上拿掉。磁盘上的 包,又变回一个 b-tree 库的模样。
运行代码就是工作本身
上面每一道防御,都假设有一个由人决定去执行某样东西的时刻。代理把那个时刻拿走了。
一个编码代理安装依赖、对着它写代码、跑测试套件、读失败信息、修改、再跑一次,一小时 几十回,而你正在开会,因为这个循环就是它全部的价值主张。你不会去读第一百次插入。 代理也比你更有理由去产生会踩中这个触发点的那类数据:它用循环写测试夹具、用连续 ID 填表、用几千行做性能测试。如果上膛条件是“有人存入键 100”,代理会比一个人更早也更常 抵达那里,而且是在一个握着整个会话所握之物的进程里。
安装期的防御覆盖的是包抵达的那一刻。你的代理每天在那里花一分钟,其余时间都在运行 已经抵达的东西。
加载器需要什么
把这场行动削到骨头,它需要两样东西:一个能连出去的地方,以及在它醒来的那台机器上 值得拿走的东西。这两者你都能在它跑起来之前决定,而且两个决定都不需要认出这个包。
在开发者的笔记本上,两样都往攻击者那边倒。它送出的侦察信息——主机名、架构、CPU、
内存、运行时长——描述的是你网络上的你的机器。它生出的进程以你的身份运行,带着你的
shell、你的 ~/.git-credentials、你的 GH_TOKEN、你的 ~/.aws、你的 SSH 代理。
它的三个目的地是 slack.com、api.telegram.org 和一个公开的 Ethereum RPC 提供商:
两个你一半工具本来就在对话的 SaaS 主机,以及一个看起来像基础设施的区块链端点。在
netflow 日志里,这份清单没有一项读起来是恶意的。
Bromure Agentic Coding 在宿主机上、在代理运行的 VM 之外,回答这两个半边。
凭据先来,因为它们才是目标。你配置的每一个密钥,在工作区 VM 里都以保留结构的替身
形式存在(GitHub 令牌是 ghp_ 加 36 个字符,Anthropic 密钥以 sk-ant-api03-brm-
开头),由真实值通过 HKDF-SHA256 与每次安装专属的盐导出,因此 gh、git 和
claude 都会接受它,而且它在不同会话之间看起来从不轮换。真实值加密留在你的 Mac 上,
宿主机端的代理服务器在请求离开 VM 之后才把它们替换到线路上,而且只替换给当初铸造该
凭据的那台主机。手册把这条不变式写得很明白:VM 内的任何文件、环境变量或进程,都不会
包含真实的 API 密钥、OAuth 令牌、AWS 密钥或 SSH 私钥。一个读取环境的窃密程序,收集到
的是替身。
把替身带到它不该去的地方,这件事本身就是警报。代理服务器扫描每一个对外请求,寻找被 送往非铸造对象主机的假令牌。一旦出现,代理服务器就以 451 挡下该请求,目的地什么也 收不到,而 Bromure 会暂停这台 VM。它会强制重新挂回已分离的会话,把外框染成红色, 并发出一条告警,点名令牌预览、凭据、它原本所属的主机,以及它正要前往的主机。你可以 把工作区关掉,或者选择 Save for Investigation,它会在停止之前导出系统磁盘、主 目录和每一个共享文件夹。这个加载器在握手之后就抹掉自己的文件,所以事后来看的人只会 找到一块整齐的磁盘。在请求进行到一半时暂停,让你拿到的是它整理之前的那台机器。
接着是连出去的范围。护栏为每个工作区带来一道出口防火墙:一组 pf 风格的有序规则, 作用在 VM 打开的每一条连接上,可按主机、CIDR、协议、端口,以及在网页规则里按单个 HTTP 方法来指定,并有一个 Unmatched traffic 控制项可设为 Allow 或 Deny。 把它设成 Deny,列出工作需要的仓库源与 API,那么一个伸手去找 Telegram 机器人 API 或 Ethereum RPC 提供商的加载器,就会在线路上撞上拒绝。有两层在执行它,让客户机哪一层 都绕不过去:虚拟交换机按目的地 IP 与从 DNS 偷看到的主机名,在 TCP 与 UDP 上匹配规则, 代理服务器则再按 TLS 服务器名与方法匹配一次。交换机把 VM 的 80 与 443 端口流量导进 代理服务器本身,所以没有任何代理变量可供载荷取消设置。每一个判定都会落成 Security Timeline 里的一行 Firewall,而规则的修改不必重启就会传达到运行中的会话。
日历够不到的地方
Bromure 的版本年龄门槛,默认开启、设在两天,是为了劫持那个模式而存在的:维护者账号 被入侵,一个下了毒的版本被发布,整件事在几小时内被发现并撤下。它已经接住过好几则 这样的故事。十一周会越过你会设下的任何界线,所以这里的工作落到其他层身上。
Supply Chain 面板之所以是一叠,正是为此。专为一开始就恶意的包而造的那一层是包 过滤:socket.dev 的受损包拦截涵盖被标记为恶意软件的包、误拼名称、可疑的安装脚本 与混淆过的代码,而 Depi 用一个预先审核过的镜像取代 npm 仓库源。两者都在宿主机端 运行,API 密钥留在你的 Mac 上、从不导出到 VM,而且两者都站在安装之前而非之后。拦截 会以 HTTP 451 返回,npm 会把它的纯文本内容逐字打印出来,所以代理读得到理由,会绕路 而不是卡住。
没有任何信誉服务能抓到全部,而一个干净跑了十一周的误拼包,对它们每一个都是难题, 这正是其余各层挣到自己位置的地方。替身凭据、入侵检测与出口规则集,决定的是一个包 安装之后能够到什么,而它们没有一个需要认出这个包。它们覆盖的是还没有人分类过的 代码,包括再过十一周也不会有人分类的代码。
假设它在首次使用时发动
拦截生命周期脚本值得做,而这场行动绕过了它。请为一个在你的测试套件调用库时醒来 的载荷做打算。
触发点是合法数据
键值库里一个值为 100 的键。任何输入白名单都帮不上忙,而代理产生那种数据的速度 比你快。
别在进程里留下可偷之物
描述一台 VM 的侦察信息,加上在上游一文不值的令牌,会把一次成功的执行变成一次 白费的执行。
事先决定好目的地
Slack、Telegram 和一个公开的 Ethereum RPC 端点,靠信誉是拦不住的。白名单不需要 认出它们。
封堵安装脚本,把大约一半的恶意 npm 包扫出了棋盘。正因如此,操作者才不得不造出这么
精巧的东西——一个假的仓库、一个伪装成算术的触发点,以及一条架在区块链上的指挥通道
——只为了放进一段一年前在 package.json 里写四行就能办到的代码。防御奏效了,攻击的
价码也上去了。
底下那个假设却从来没动过。你依然把一个依赖在安装之后当成安全的,把运行它当成一个 中性的动作。对一个编码代理来说,运行它就是工作,一天几千次,而且房间里没有人。在那个 进程跑起来之前,就决定它能握着什么、能连到哪里,那么你的代理的第一百次插入,最后会是 Security Timeline 里一行被拦截的 Firewall 记录。