そのコーディング課題は AI アシスタントを禁じていた
Kaspersky は 9 月 1 日、Mirage Kitten のリクルーター偽装キャンペーンを公開した。餌は持ち帰り型のコーディング課題で、その README は三つのことを求める。三時間で終えること、server.js はすでにバグがないので変更しないこと、そして AI アシスタントを使わないこと。server.js の一行目は、npm に一度も公開されたことのないトロイの木馬入りパッケージを import する。それは zip の中に同梱され、すでに node_modules に置かれていた。これまでに作られたあらゆるサプライチェーン対策は、この攻撃が決して出さないレジストリへの要求を見張っている。Bromure Agentic Coding はもう半分、つまりコードが走るマシンの側から答える。
攻撃者はコーディングエージェントを使うなと頼んだ。指示に書かれていようといまいと リポジトリのあらゆるファイルを開く道具こそ、このキャンペーンとバックドアのあいだに 立っていた唯一のものであり、README はそれを手放すよう説得してみせた。
リクルーターが LinkedIn で声をかけてくる。求人はそれなりに実在しそうで、会社名も 聞いたことがあり、次の段階は持ち帰りの技術課題だ。zip が届く。中身は Express と React と Vite で組まれた TaskFlow というプロジェクト管理アプリで、数百のファイルと ルートに置かれた 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 で
import する。兄弟検体は pretty-log を、やはり 2.1.0 で使う。どちらも何かのタイポ
スクワットではなく、乗っ取られたリリースでもない。そもそもどちらもレジストリに存在
しないからだ。
攻撃者はこのパッケージを npm レジストリに公開する代わりに、課題アーカイブの
node_modulesディレクトリに直接同梱していた。
import されると、パッケージは
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 では
RunAtLoad と KeepAlive をともに真にした ~/Library の LaunchAgent。
兄弟の PollCat は RankChallenge-react という React 課題から届き、その README は
時計を一時間のセッションに縮め、三十秒ごとに変わると称する六桁のコードで入口を
塞ぐ。同じ圧力の設計で、時間だけが短い。
自分のマシンではないどこかで走らせる
Bromure Agentic Coding は、Mac 上のハードウェア仮想化された Linux VM の中で
コーディングエージェントを走らせ、あらゆるセキュリティ制御をその境界のホスト側に
置く。見知らぬ相手の zip は、まさにそのために作られた仕事である。制御が記述して
いるのは客ではなく部屋のほうだからだ。どの制御も colorized_terminal を認識する
必要はない。
ワークスペースが隔離の単位である。 Ubuntu VM がひとつ、専用のシステムディスク、
専用の永続 home、専用の資格情報、専用のポリシーを持ち、二つのワークスペースが
1 バイトも共有することはない。課題用に使い捨てのワークスペースを作る。インプラントは
走り、import は発火し、切り離されたプロセスは起動する。そしてそのすべてが、今朝
生まれたばかりで zip ファイルを一つだけ抱えたマシンの中で起こる。終わったら
Reset disk がシステムディスクをベースから再クローンし、Erase home… が home
イメージを消す。Restore home… の奥にある起動ごとのロールバック用チェックポイントは
同じことをより細かい粒度で行う。Linux 分岐が書いた @reboot の cron エントリも一緒に
消える。
Windows と macOS の永続化分岐には立つ地面がない。 AppData も、Run キーも、
タスクスケジューラも、~/Library も、LaunchAgent も、読むべき Outlook のメール
ストアもない。ゲストは Linux であり、しかもあなたの Linux ではない。
persist:projects:scan に見えるのは共有したフォルダだけである。 ホスト側の
フォルダは /home/ubuntu/<basename> に virtiofs マウントとして接続され、ワーク
スペースあたり八つが上限で、VM が届く Mac のファイルシステムはそこだけである。課題の
フォルダだけを共有すれば、git hook の注入が見つけるリポジトリは一つ、もとより敵意を
持っていたそれだけになる。リストを短く保つ理由はもう一つある。共有フォルダは Mac への
生きた窓であり、侵害時の消去は設計上そこには手を触れない。
fs:read が返すのはプレースホルダである。 Bromure は、設定したすべての資格情報を、
本物の値とインストールごとのソルトから HKDF-SHA256 で導いた、構造を保った偽物に
置き換える。sk-ant-api03-brm-…、長さの正しい ghp_ トークン、glpat-…、
brm-k8s-…、brm-docker-…。偽物は環境変数に書き込まれ、さらに
~/.git-credentials、~/.docker/config.json、~/.kube/config、~/.aws/config、
そして MCP の設定にも書き込まれる。まさに情報窃取ツールが列挙していくリストである。
本物の値は Mac 上で暗号化されたまま残り、ホスト側のプロキシが、それぞれが鋳造された
宛先ホストに限って通信路上で差し替える。AWS についてはさらに踏み込む。ゲストは偽の
シークレットキーで署名し、ホストが署名し直すので、プロキシを迂回した要求は AWS 側で
InvalidSignatureException となって死ぬ。SSH 秘密鍵のバイト列は VM に入ることが
まったくなく、越えるのは署名だけである。これらを左右するトグルはない。プロキシが VM
にとって唯一のネットワーク経路だからである。
指令サーバーこそ、許可リストの出番である。 NodeRabbit の指令サーバーの大半は
*.azurewebsites.net の下にあり、PollCat のそれも同じだ。どの評判フィードもこの
ドメインをブロックリストに載せることはなく、ドメイン年齢のヒューリスティックが
警告することもない。許可リストはそれを気にしない。Guardrails で
Unmatched traffic を Deny にし、その仕事に必要な二行か三行を書けばよい。
強制はホスト側で、しかも二重である。仮想スイッチが宛先 IP と DNS から覗いた
ホスト名で全プロトコルの通信を照合し、プロキシが TLS のサーバー名と HTTP メソッドで
もう一度照合する。透過的な傍受がゲストの 80 番と 443 番の通信をプロキシへ迂回させ、
VM の中からはそれを解除できない。これが、プロキシ対応と動的ポート選択を積んだ変種への
答えである。
そしてインプラントがトークンを持ち出そうとすれば、偽物が仕掛け線になる。 プロキシは外向きの全要求をヘッダーも本文も走査し、鋳造された先ではないホストへ 向かう偽物を探す。一致すれば、1 バイトも転送せずに要求を拒み、その場で 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> は何もトークンを持ち出そうとしなかったとき
No leaks detected. ✓ と表示する。あとは Reset disk と Erase home… で、
ワークスペースは出発点のイメージに戻る。
読み手は最初から制御ではなかった
それでも server.js は読むべきだ。それは良い助言であり、そして貧しい防御である。
次のアーカイブは、開く理由のないファイルの 340 行目に import を置くか、minify された
バンドルの中に置くか、二つの文字列から組み立てた動的な require の裏に置くだろう。
Mirage Kitten の三つの規則はあなたの注意力に賭けた保険であり、注意力は金曜の午後 4 時、
時計に一時間を残したところで尽きる。
コードが走る場所は尽きない。import はどちらにせよ発火し、切り離されたプロセスは どちらにせよ起動する。あなたがなお選べるのは、そのプロセスが周りを見回したときに 何を見つけるかだ。GitHub のトークンと kubeconfig と SSH 鍵と四十のリポジトリを抱えた マシンか、zip ファイル一つとプレースホルダ一式と、あなたが指定した先にしか行かない ネットワークソケットだけを抱えた Linux VM か。
課題を受けよう。README が使うなと言ったコーディングエージェントを使い、まず
server.js を読ませればいい。
Bromure Agentic Coding を入れて、その zip に、気にしないマシンを
与えよう。