返回所有文章
發佈於 · 作者 Renaud Deraison

那份程式測驗禁止使用 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.zipTask-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 一次也不做。

從不碰註冊表的供應鏈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 起初很小。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 裡一個 RunAtLoadKeepAlive 皆為真的 LaunchAgent。 同族的 PollCat 透過一份叫 RankChallenge-react 的 React 測驗送達,它的 README 把 計時器縮短成一小時的作答時段,前面還擋著一組宣稱每三十秒輪換的六位數代碼。同樣的 壓力設計,只是時間更少。

在筆電上植入程式繼承你的整個帳號persist:projects:scan磁碟上每個儲存庫都被裝上 hookpersist:vscode一個在每個專案都載入的擴充功能outlook:emails · fs:read~/.aws · ~/.kube · ~/.git-credentials善後找出 hook、輪替金鑰、祈禱在工作區裡一台 Ubuntu VM,自己的磁碟與 home掃描只看得到你掛載的部分只有共用資料夾,最多八個持續化落在可重建的磁碟上Reset disk · Erase home · checkpoints每個憑證檔裡放的都是假值ghp_… · brm-k8s-… · sk-ant-api03-brm-…善後抹除磁碟與 home,不必輪替
一份壓縮檔,兩種波及範圍。在筆電上,植入程式繼承整個帳號:它找得到的每個 git 儲存庫都被裝上 hook,編輯器被裝上擴充功能,郵件與雲端組態可讀,而 shell 環境裡的憑證是真的。在 Bromure Agentic Coding 的工作區裡,同樣這些指令跑在一台 Linux VM 上,那台 VM 只放著佔位憑證,只掛載你分享的資料夾,並且只透過主機上的一個 socket 連到網路。

讓它跑在不是你自己機器的地方

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 裡設成 DenyUnmatched traffic。加上 allow web registry.npmjs.org,還有這個專案會抓取的其他東西。儲存後規則會推送到 執行中的工作階段而不需重開機,所以你可以在建置抱怨時一行一行地放寬。

做完之後的兩道指令

bromure-cli trace hostnames <workspace> 告訴你程式碼去過哪裡,而 bromure-cli trace leaks <workspace> 在沒有任何東西試圖帶走 token 時會印出 No leaks detected. ✓。接著 Reset diskErase home…,工作區就回到它 出發時的那份映像。

讀者從來就不是那道控制

還是去讀 server.js 吧。那是好建議,也是差勁的防禦,因為下一份壓縮檔會把匯入放在 某個你毫無理由打開的檔案第 340 行,或放進一個 minify 過的 bundle,或藏在一個由兩個 字串拼出來的動態 require 後面。Mirage Kitten 的三條規則是押在你注意力上的對沖,而 注意力會在星期五下午四點、計時器還剩一小時的時候用完。

程式碼執行的地方不會用完。匯入無論如何都會觸發,分離的行程無論如何都會啟動,所以你 還能選的,是那個行程環顧四周時會找到什麼:一台裝著你的 GitHub token、你的 kubeconfig、 你的 SSH 金鑰和四十個儲存庫的機器,還是一台只裝著一個 zip 檔、一套佔位值,以及一個 只通往你指定之處的網路 socket 的 Linux VM。

接下這份測驗吧。用那個 README 叫你別用的程式代理,並且先讓它讀 server.js安裝 Bromure Agentic Coding,給這個 zip 一台不在意的機器。