返回所有文章
发布于 · 作者 Renaud Deraison

那份编程测验禁止使用 AI 助手

卡巴斯基在 9 月 1 日公布了 Mirage Kitten 伪装招聘方的行动。诱饵是一份带回家做的编程测验,它的 README 提出三项要求:三小时内完成、不要修改 server.js 因为它已经没有缺陷,以及不要使用 AI 助手。server.js 的第一行导入了一个从未发布到 npm 的木马化软件包。它随 zip 一起送来,早就躺在 node_modules 里。至今建起的每一道供应链防线,守的都是这次攻击从不发出的注册表请求。Bromure Agentic Coding 回答的是另外一半:代码运行所在的那台机器。

攻击者请你别用编程代理。一个不管指示怎么写都会打开仓库中每一个文件的工具, 正是挡在这场行动与一道后门之间的唯一东西,而那份 README 说服你放弃了它。

一位招聘人员在 LinkedIn 上找你。岗位看起来够真实,公司名字你也听过,下一步是一份 带回家做的技术测验。你收到一个 zip。里面是一个叫 TaskFlow 的项目管理应用,用 Express、React 和 Vite 构建:几百个文件,根目录放着一份 README。修好前端的缺陷。 你有三小时。

这是一名在职工程师会遇到的最平常不过的事。根据 卡巴斯基在 9 月 1 日发表的分析, 这也正是一支伊朗间谍团队在埃及、埃塞俄比亚和阿富汗的开发者机器上投放跨平台后门的 方式。该报告称这支团队为 Mirage Kitten,其他地方则以 UNC1549、Smoke Sandstorm 或 Nimbus Manticore 追踪它,另有来自印度、土耳其、以色列、伊拉克、德国和爱尔兰的样本 提交。压缩包以 Front-Technical-Challenge.zipTask-FullStack.zip 之类的名字 送达,托管在 AWS 上,好让下载看起来像基础设施而不是攻击载荷。The Hacker News 在 9 月 9 日报道了这场行动

三条规则,全都关于阅读

卡巴斯基记下了 README 中的三项指示。看看每一条为攻击者换来什么。

README 另外设下三小时的时限,并禁止使用 AI 助手。

它还声称 server.js 没有缺陷,不应修改。

计时器是最明显的一条:三小时修好一个陌生的代码库,这样的日程表不留任何好奇的余地。 第二条规则把你的注意力引向前端,并带离某一个特定文件。第三条才是有意思的,因为那是 攻击者亲口告诉你,他怕的是哪一件工具。

一个拿到这个仓库的编程代理,不会遵守 README 暗示的范围。把 Claude Code 或 Codex 指向这个项目并要它修前端,它依然会读 server.js,因为弄清楚应用怎么启动,正是它 弄清楚前端在跟什么说话的方式。它读整棵树。它没有三小时的计时器,也没有理由跳过某个 被人宣告无恙的文件。禁掉助手,等于把房间里唯一那个什么都读的读者请出去,而这对一场 招聘流程来说是个奇怪的坚持。

而它本来会读到的,正是第一行。

那个依赖从来不在 npm 上

server.js 的第一行导入一个叫 colorized_terminal 的软件包,版本 2.1.0。另一个 同族样本用的是 pretty-log,同样是 2.1.0。两者都不是任何东西的仿冒名,也都不是被 劫持的发行版本,因为两者根本都不在注册表上:

攻击者把这个软件包直接打包进测验压缩包的 node_modules 目录,而不是把它发布到 npm 注册表。

被导入时,该软件包会悄悄地从 node_modules/.cache/.320697f1/index.js 启动一个 植入程序,作为分离的后台进程运行。

这里不会执行 npm install,也没有 postinstall 脚本。没有 lockfile 条目可校验, 没有完整性哈希可比对,没有来源证明可核查,没有发布时间可等它变老,没有信誉服务可 查询,也没有任何一个注册表请求可以把这些挂上去。在你打开那个文件夹之前,依赖就已经 在文件夹里了。运行 README 要你修的那个应用——那就是整份作业——意味着一个朴素的 require,而那个 require fork 出一道后门。

三年来对抗恶意软件包的防御,全都以一次注册表获取为起点。Mirage Kitten 一次也不做。

从不碰注册表的供应链Front-Technical-Challenge.zipREADME.md三小时 · 禁用 AI“server.js 没有缺陷”server.js第 1 行: require('colorized_terminal')被要求跳过的文件node_modules/[email protected]已安装 · 不在 npm 上你运行应用导入启动植入程序node_modules/.cache/.320697f1/index.js分离的后台进程NodeRabbit 回连plugplay.azurewebsites.netrgbteller.azurewebsites.net11 条命令,之后 23 条什么从不触发,为什么从未发出注册表请求年龄门槛 — 没有发布时间完整性 — 没有 lockfile来源证明 — 什么都没发布信誉查询 — 没有包可查剥离脚本 — 没有安装步骤仿冒名 — 没有名称可比剩下的是运行它的机器
压缩包送达时依赖早已安装完毕,因此供应链从不接触注册表。README 叫你别打开的那个文件,第一行导入了一个别处都不存在的软件包,而这个导入会从 node_modules 里一个点开头的目录 fork 出植入程序。没有安装步骤,没有 lockfile 比对,没有发布时间:那些守着软件包获取的防线,没有任何获取可守。

它不会待在测验文件夹里

NodeRabbit 起初很小。卡巴斯基的第一个变种来自阿富汗的一台机器,带着十一条命令: 读文件、写文件、列出目录、导出网络配置、启动进程、运行脚本、休眠。埃及的变种加上了 沙箱检测、代理支持和动态端口选择。第三个来自埃塞俄比亚,达到二十三条,而那些新加的 命令正是一名开发者的机器不再只是一台机器的地方。

outlook:emails 收割邮件残留物。persist:projects:scan 走遍磁盘寻找 git 仓库。 persist:project:inject 写进它找到的每一个,落在 .git/hooks/post-merge.git/hooks/post-checkout,好让植入程序在你下次 pull 时重新启动。persist:vscode 把一个恶意扩展装进你的编辑器,编辑器接着会在你打开的每个项目里加载它。卡巴斯基对 意图的措辞十分精确:

除了上述的持久化机制之外,变种 3 另外引入两种持久化机制,通过开发者常见的工作流 重新启动该恶意程序。

目标是你其他的仓库。底下那层比较平凡的持久化则按平台各自调校:Windows 上是每天上午 10 点触发的计划任务或 Run 注册表键,Linux 上是 @reboot 的 cron 条目,macOS 上是 ~/Library 里一个 RunAtLoadKeepAlive 皆为真的 LaunchAgent。 同族的 PollCat 通过一份叫 RankChallenge-react 的 React 测验送来,它的 README 把 计时器缩短成一小时的作答时段,前面还挡着一组号称每三十秒轮换的六位数验证码。同样的 压力设计,只是时间更少。

在笔记本电脑上植入程序继承你的整个账户persist:projects:scan磁盘上每个仓库都被装上 hookpersist:vscode一个在每个项目都加载的扩展outlook:emails · fs:read~/.aws · ~/.kube · ~/.git-credentials善后找出 hook、轮换密钥、祈祷在工作区里一台 Ubuntu 虚拟机,自己的磁盘与 home扫描只看得到你挂载的部分只有共享文件夹,最多八个持久化落在可重建的磁盘上Reset disk · Erase home · checkpoints每个凭据文件里放的都是假值ghp_… · brm-k8s-… · sk-ant-api03-brm-…善后抹掉磁盘与 home,不必轮换
一份压缩包,两种波及范围。在笔记本电脑上,植入程序继承整个账户:它找得到的每个 git 仓库都被装上 hook,编辑器被装上扩展,邮件与云端配置可读,而 shell 环境里的凭据是真的。在 Bromure Agentic Coding 的工作区里,同样这些命令跑在一台 Linux 虚拟机上,那台虚拟机只放着占位凭据,只挂载你分享的文件夹,并且只通过宿主机上的一个 socket 连到网络。

让它跑在不是你自己机器的地方

Bromure Agentic Coding 让编程代理跑在你 Mac 上一台硬件虚拟化的 Linux 虚拟机里,而 所有安全控制都位于那道边界的宿主机一侧。陌生人的 zip 正是它被造出来要处理的工作, 因为这些控制描述的是房间而不是客人。它们没有一个需要认得 colorized_terminal

工作区就是隔离的单位。 一台 Ubuntu 虚拟机,有自己的系统磁盘、自己的持久 home、 自己的凭据与自己的策略,两个工作区之间不共享任何一个字节。为这份测验开一个用完即弃的 工作区。植入程序跑起来,导入触发,分离的进程启动,而这一切都发生在一台今天早上才 诞生、里面只有一个 zip 文件的机器上。做完之后,Reset disk 会从基础镜像重新克隆 系统磁盘,Erase home… 会清掉 home 镜像;Restore home… 背后每次开机的回滚 检查点则以更细的粒度做同一件事。Linux 分支写下的那个 @reboot cron 条目也一并消失。

Windows 和 macOS 的持久化分支没有立足之地。 没有 AppData,没有 Run 注册表键, 没有计划任务,没有 ~/Library,没有 LaunchAgent,也没有可读的 Outlook 邮件存储。 客户机是 Linux,而且不是你的那个 Linux。

persist:projects:scan 只看得到你分享出去的文件夹。 宿主机文件夹以 virtiofs 挂载接到 /home/ubuntu/<basename>,每个工作区上限八个,而它们是虚拟机能触及的 Mac 文件系统的全部。只分享测验文件夹、别的都不分享,git hook 注入就只会找到一个仓库: 本来就怀有敌意的那一个。把清单保持得短还有第二个理由。共享文件夹是一扇通往你 Mac 的活窗,而遭入侵后的抹除按设计不会动它。

fs:read 拿回去的是占位值。 Bromure 会把你配置的每一份凭据,换成以真值加上每次 安装专属的 salt 通过 HKDF-SHA256 推导出的、保留结构的假值:sk-ant-api03-brm-…、 长度正确的 ghp_ 令牌、glpat-…brm-k8s-…brm-docker-…。这些假值被写进环境 变量,也被写进 ~/.git-credentials~/.docker/config.json~/.kube/config~/.aws/config 以及 MCP 的配置文件——那正是窃密程序会逐一翻找的清单。真值始终加密 留在 Mac 上,宿主机端的代理在传输时把它们换上去,而且仅限于各自被铸造时指定的目的 主机。AWS 更进一步:客户机用一把假的 secret key 签名,宿主机再重新签一次,因此绕过 代理的请求会在 AWS 那端以 InvalidSignatureException 死去。SSH 私钥的字节完全不会 进入虚拟机,越过边界的只有签名。这些都没有任何开关可调,因为代理是虚拟机通往网络的 唯一路径。

那些 C2 主机正是允许列表的用武之地。 NodeRabbit 的命令服务器大多位于 *.azurewebsites.net 之下,PollCat 的也是。没有任何信誉源会把这个域名列入封禁名单, 也没有任何域名年龄的启发式规则会标记它。允许列表不在乎这些:在 Guardrails 里把 Unmatched traffic 设成 Deny,然后写下这份工作需要的那两三行。强制执行在宿主 机一侧,而且是双重的:虚拟交换机以目的 IP 和从 DNS 偷看到的主机名,跨所有协议匹配 每一条连接,代理再以 TLS 服务器名和 HTTP 方法匹配一次。透明拦截把客户机的 80 端口与 443 端口流量导进代理,虚拟机里没有任何东西能把它取消,而这正是对那个带了代理支持与 动态端口选择的变种的回答。

而如果植入程序试着把令牌带出去,假值就成了绊索。 代理会扫描每一个外发请求的 头部与正文,寻找正前往非铸造目的地的假值。一旦命中,它拒绝该请求且不转发任何一个 字节,当场暂停虚拟机,在 Security Timeline 记下一行红色的 Credential brokering, 并把工作区标记为已遭入侵,让下次启动先抹掉磁盘与 home。事后你不必轮换任何东西,因为 植入程序握着的本来就是占位值。

整段对话你都看得到。 把追踪设成 Activity only,代理会为每个请求写一行元数据, 完全不写正文。bromure-cli trace hostnames 会打印这个工作区接触过的每一个不同主机 以及次数,所以三个你从未列出的 azurewebsites.net 地址,只隔着一道命令,而不是一场 取证调查。

在 zip 送来之前就先建好的工作区

一个用完即弃的工作区,除了代理本身的密钥之外不配置任何凭据,一个放压缩包的共享 文件夹,以及在 Guardrails 里设成 DenyUnmatched traffic。加上 allow web registry.npmjs.org,还有这个项目会抓取的其他东西。保存后规则会推送到 运行中的会话而不需重启,所以你可以在构建抱怨时一行一行地放宽。

做完之后的两条命令

bromure-cli trace hostnames <workspace> 告诉你代码去过哪里,而 bromure-cli trace leaks <workspace> 在没有任何东西试图带走令牌时会打印 No leaks detected. ✓。接着 Reset diskErase home…,工作区就回到它 出发时的那份镜像。

读者从来就不是那道控制

还是去读 server.js 吧。那是好建议,也是差劲的防御,因为下一份压缩包会把导入放在 某个你毫无理由打开的文件第 340 行,或放进一个 minify 过的 bundle,或藏在一个由两个 字符串拼出来的动态 require 后面。Mirage Kitten 的三条规则是押在你注意力上的对冲, 而注意力会在星期五下午四点、计时器还剩一小时的时候用完。

代码运行的地方不会用完。导入无论如何都会触发,分离的进程无论如何都会启动,所以你 还能选的,是那个进程环顾四周时会找到什么:一台装着你的 GitHub 令牌、你的 kubeconfig、 你的 SSH 密钥和四十个仓库的机器,还是一台只装着一个 zip 文件、一套占位值,以及一个 只通往你指定之处的网络 socket 的 Linux 虚拟机。

接下这份测验吧。用那个 README 叫你别用的编程代理,并且先让它读 server.js安装 Bromure Agentic Coding,给这个 zip 一台不在意的机器。