那份程式測驗禁止使用 AI 助理
Kaspersky 於 9 月 1 日公布了 Mirage Kitten 偽裝招募人員的行動。誘餌是一份帶回家寫的程式測驗,它的 README 提出三項要求:三小時內完成、不要修改 server.js 因為它已經沒有缺陷,以及不要使用 AI 助理。server.js 的第一行匯入了一個從未發布到 npm 的木馬化套件。它隨 zip 一起送達,早已放在 node_modules 裡。至今建起的每一道供應鏈防線,守的都是這次攻擊從不發出的註冊表請求。Bromure Agentic Coding 回答的是另外一半:程式碼執行所在的那台機器。
攻擊者請你別用程式代理。一個無論指示怎麼寫都會打開儲存庫中每一個檔案的工具, 正是擋在這場行動與一道後門之間的唯一東西,而那份 README 說服你放棄了它。
一位招募人員在 LinkedIn 上找你。職缺看來夠真實,公司名字你也聽過,下一步是一份 帶回家寫的技術測驗。你收到一個 zip。裡面是一個叫 TaskFlow 的專案管理應用,以 Express、React 和 Vite build 成:幾百個檔案,根目錄放著一份 README。修好前端的 缺陷。你有三小時。
這是一名在職工程師會遇到的最平常不過的事。根據
Kaspersky 在 9 月 1 日發表的分析,
這也正是一支伊朗間諜團隊在埃及、衣索比亞和阿富汗的開發者機器上投放跨平台後門的
方式。該報告稱這支團隊為 Mirage Kitten,其他地方則以 UNC1549、Smoke Sandstorm
或 Nimbus Manticore 追蹤它,另有來自印度、土耳其、以色列、伊拉克、德國和愛爾蘭的
樣本提交。壓縮檔以 Front-Technical-Challenge.zip 和 Task-FullStack.zip 之類的
名字送達,託管在 AWS 上,好讓下載看起來像基礎設施而不是攻擊載荷。The Hacker News
在 9 月 9 日報導了這場行動。
三條規則,全都關於閱讀
Kaspersky 記下了 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 一次也不做。
它不會待在測驗資料夾裡
NodeRabbit 起初很小。Kaspersky 的第一個變種來自阿富汗的一台機器,帶著十一個指令: 讀檔、寫檔、列出目錄、傾印網路組態、啟動行程、執行腳本、睡眠。埃及的變種加上了 沙箱偵測、代理支援與動態通訊埠選擇。第三個來自衣索比亞,達到二十三個,而那些新加的 指令正是一名開發者的機器不再只是一台機器的地方。
outlook:emails 收割郵件遺留物。persist:projects:scan 走遍磁碟尋找 git 儲存庫。
persist:project:inject 寫進它找到的每一個,落在 .git/hooks/post-merge 和
.git/hooks/post-checkout,好讓植入程式在你下次 pull 時重新啟動。persist:vscode
把一個惡意擴充功能裝進你的編輯器,編輯器接著會在你打開的每個專案裡載入它。Kaspersky
對意圖的措辭十分精確:
除了上述的持續化機制之外,變種 3 另外引入兩種持續化機制,透過開發者常見的工作流程 重新啟動該惡意程式。
目標是你其他的儲存庫。底下那層比較平凡的持續化則按平台各自調校:Windows 上是每天
上午 10 點觸發的排程工作或 Run 機碼,Linux 上是 @reboot 的 cron 條目,macOS 上
是 ~/Library 裡一個 RunAtLoad 與 KeepAlive 皆為真的 LaunchAgent。
同族的 PollCat 透過一份叫 RankChallenge-react 的 React 測驗送達,它的 README 把
計時器縮短成一小時的作答時段,前面還擋著一組宣稱每三十秒輪換的六位數代碼。同樣的
壓力設計,只是時間更少。
讓它跑在不是你自己機器的地方
Bromure Agentic Coding 讓程式代理跑在你 Mac 上一台硬體虛擬化的 Linux VM 裡,而所有
安全控制都位於那道邊界的主機側。陌生人的 zip 正是它被造出來要處理的工作,因為這些
控制描述的是房間而不是客人。它們沒有一個需要認得 colorized_terminal。
工作區就是隔離的單位。 一台 Ubuntu VM,有自己的系統磁碟、自己的持續 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>,每個工作區上限八個,而它們是 VM 能觸及的 Mac 檔案
系統的全部。只分享測驗資料夾、別的都不分享,git hook 注入就只會找到一個儲存庫:本來
就懷有敵意的那一個。把清單保持得短還有第二個理由。共用資料夾是一扇通往你 Mac 的活窗,
而遭入侵後的抹除依設計不會動它。
fs:read 拿回去的是佔位值。 Bromure 會把你設定的每一份憑證,換成以真值加上每次
安裝專屬的 salt 透過 HKDF-SHA256 推導出的、保留結構的假值:sk-ant-api03-brm-…、
長度正確的 ghp_ token、glpat-…、brm-k8s-…、brm-docker-…。這些假值被寫進環境
變數,也被寫進 ~/.git-credentials、~/.docker/config.json、~/.kube/config、
~/.aws/config 以及 MCP 的設定檔——那正是竊資程式會逐一翻找的清單。真值始終加密留在
Mac 上,主機端的代理在傳輸時把它們換上去,而且僅限於各自被鑄造時指定的目的主機。AWS
更進一步:客體用一把假的 secret key 簽章,主機再重新簽一次,因此繞過代理的請求會在
AWS 那端以 InvalidSignatureException 死去。SSH 私鑰的位元組完全不會進入 VM,越過
邊界的只有簽章。這些都沒有任何開關可調,因為代理是 VM 通往網路的唯一路徑。
那些 C2 主機正是允許清單的用武之地。 NodeRabbit 的指令伺服器大多位於
*.azurewebsites.net 之下,PollCat 的也是。沒有任何信譽來源會把這個網域列入封鎖名單,
也沒有任何網域年齡的啟發式規則會標記它。允許清單不在乎這些:在 Guardrails 裡把
Unmatched traffic 設成 Deny,然後寫下這份工作需要的那兩三行。強制執行在主機側,
而且是雙重的:虛擬交換器以目的 IP 和從 DNS 偷看到的主機名,跨所有協定比對每一條連線,
代理再以 TLS 伺服器名稱和 HTTP 方法比對一次。透通攔截把客體的 80 埠與 443 埠流量導進
代理,VM 裡沒有任何東西能把它取消,而這正是對那個帶了代理支援與動態通訊埠選擇的變種
的回答。
而如果植入程式試著把 token 帶出去,假值就成了絆索。 代理會掃描每一個外送請求的 標頭與內容,尋找正前往非鑄造目的地的假值。一旦命中,它拒絕該請求且不轉發任何一個 位元組,當場暫停 VM,在 Security Timeline 記下一列紅色的 Credential brokering, 並把工作區標記為已遭入侵,讓下次啟動先抹掉磁碟與 home。事後你不必輪替任何東西,因為 植入程式握著的本來就是佔位值。
整段對話你都看得到。 把追蹤設成 Activity only,代理會為每個請求寫一列中繼
資料,完全不寫內容。bromure-cli trace hostnames 會印出這個工作區接觸過的每一個不同
主機以及次數,所以三個你從未列出的 azurewebsites.net 位址,只隔著一道指令,而不是
一場鑑識調查。
在 zip 送達之前就先建好的工作區
一個用完即丟的工作區,除了代理本身的金鑰之外不設定任何憑證,一個放壓縮檔的共用
資料夾,以及在 Guardrails 裡設成 Deny 的 Unmatched traffic。加上
allow web registry.npmjs.org,還有這個專案會抓取的其他東西。儲存後規則會推送到
執行中的工作階段而不需重開機,所以你可以在建置抱怨時一行一行地放寬。
做完之後的兩道指令
bromure-cli trace hostnames <workspace> 告訴你程式碼去過哪裡,而
bromure-cli trace leaks <workspace> 在沒有任何東西試圖帶走 token 時會印出
No leaks detected. ✓。接著 Reset disk 與 Erase home…,工作區就回到它
出發時的那份映像。
讀者從來就不是那道控制
還是去讀 server.js 吧。那是好建議,也是差勁的防禦,因為下一份壓縮檔會把匯入放在
某個你毫無理由打開的檔案第 340 行,或放進一個 minify 過的 bundle,或藏在一個由兩個
字串拼出來的動態 require 後面。Mirage Kitten 的三條規則是押在你注意力上的對沖,而
注意力會在星期五下午四點、計時器還剩一小時的時候用完。
程式碼執行的地方不會用完。匯入無論如何都會觸發,分離的行程無論如何都會啟動,所以你 還能選的,是那個行程環顧四周時會找到什麼:一台裝著你的 GitHub token、你的 kubeconfig、 你的 SSH 金鑰和四十個儲存庫的機器,還是一台只裝著一個 zip 檔、一套佔位值,以及一個 只通往你指定之處的網路 socket 的 Linux VM。
接下這份測驗吧。用那個 README 叫你別用的程式代理,並且先讓它讀 server.js。
安裝 Bromure Agentic Coding,給這個 zip 一台不在意的機器。