← すべての投稿に戻る
公開日 · 著者 Renaud Deraison

コードはモデルより先に動いた

Pluto Security は、小さな Qwen2 のテスト用チェックポイントを装った Hugging Face のリポジトリを 31 個見つけた。どれにもモデルは入っていなかった。入っていたのは、trust_remote_code でモデルを読み込んだ瞬間に動く Python ファイルで、隠れたマイナー、起動フック、一部のビルドではリバースシェルを仕込む。目玉のリポジトリは Hugging Face のスキャナーを通過して 28,412 回ダウンロードされ、削除後もほぼ同じコピーが公開されたままだった。コーディングエージェントはテストを書くとき、小さなテスト用モデルを探しにいく。Bromure Agentic Coding では、その読み込みは VM の中で動き、マイナーはプールに届かず、Security Timeline が各接続をそれを開いたツール呼び出しに結びつけ、巻き戻しでペイロードが仕込んだものを消せる。

重みファイルは 42 バイトで、テンソルは一つもなかった。仕事をしたのは Python ファイルで、モデルが読み込まれた瞬間、モデルのコードが何かをする前に動いていた。

10 月 29 日、Pluto Security は Load/Mine を公開した。9 月から Hugging Face と中国の ModelScope で続いているキャンペーンの分析だ。Pluto は 7 つのアカウントにまたがる 31 個の悪意あるリポジトリを見つけた。どれも qwen2-tiny のような名前の小さな Qwen2 テスト用チェックポイントを装い、記録されたダウンロードは合計 32,000 回を超えていた。BleepingComputer も同じ日に報じている。

Pluto はこのキャンペーンを一文でまとめている。「The code runs before the model does」(コードはモデルより先に動く)。

モデルの読み込みがマイナーになるまで

この攻撃は、毒入りの重みも pickle のエクスプロイトも使わない。使うのは機能だ。Hugging Face のモデルは独自の Python コードを同梱でき、呼び出し側が trust_remote_code=True を渡せば、Transformers ライブラリはそのコードを実行する。リポジトリの config.json は auto_map エントリでモジュールとクラスを指定する。たとえば modeling_evil2.EvilModel だ。Transformers はその .py ファイルをダウンロードしてインポートする。ファイルのトップレベルにあるものはすべて、クラスが返される前、インポートの最中に実行される。

これらのリポジトリでは、model.safetensors は 42 バイトしかない。中にモデルはない。仕事はすべて Python ファイルがする。

  1. 居座る。 マイナーを 2 分ごとに再起動するウォッチドッグを /tmp/.cch/svc.py に書き込み、~/.bashrc、/root/.bashrc、/root/.profile に起動フックを追記する。第一波のビルドはさらに、qx-5090-op という名前の SSH 鍵を authorized_keys に、cron ジョブを /etc/cron.d/.sync-helper に仕込む。
  2. マイナーを取ってきて隠す。 ダウンローダーは空のプロキシハンドラーで作られているので、企業が頼りにしているかもしれない HTTP_PROXY と HTTPS_PROXY の設定を無視する。40.6 MB のバイナリは UPX のマーカーが書き換えられていて、upx -d では展開できない。匿名のメモリ上ファイルから [kworker/u20:1] というプロセス名で動き、ポート 8048 で Kryptex 経由で Pearl を採掘する。
  3. 扉を開ける。 3 つのビルドは、212.32.253.3 のポート 443 か 9443 へのリバースシェルを追加する。切れるたびにつなぎ直す。
  4. ホストを狙う。 関連する第二波のスクリプトは、特権コンテナからホストのディスクをマウントし、その下のサーバーで root を取る。
一度の読み込み、三つのペイロード読み込みfrom_pretrained("…/qwen2-tiny", trust_remote_code=True)config.json · auto_mapmodeling_evil2.EvilModelmodel.safetensors:42 バイト、中身なしインポートでファイルが動くクラスが返される前にトップレベルのコードが実行永続化監視役:/tmp/.cch/svc.pyフック:~/.bashrc、/root/.profilecron、SSH 鍵 qx-5090-op隠れたマイナーHTTP_PROXY を無視して取得メモリで [kworker/u20:1] を名乗る採掘先 prl-us.kryptex.network:8048リバースシェル212.32.253.3 の 443/9443 へ切れても再接続(3 ビルド)
キャンペーンのモデルを一つ読み込むと何が起きるか(Pluto Security の分析より)。config.json が Transformers を Python ファイルへ導き、そのファイルがインポート時に実行され、42 バイトの重みファイルは一度も使われない。ペイロードは永続化を仕込み、プロキシ設定を無視する偽装マイナーを起動し、一部のビルドではリバースシェルを開く。

スキャンを通り、また戻ってきた

Hugging Face はアップロードをアンチウイルスと、Protect AI と JFrog のパートナーツールでスキャンしている。Pluto によれば、これらのツールが探すのは既知のマルウェアと危険なモデル形式だ。Python ファイルが何をするかは見ていない。第二波の目玉 MENXIANGYU/qwen2-tiny は「scanned clean while it collected 28,412 downloads」(28,412 回ダウンロードされる間、スキャンでは問題なしだった)。あるパートナースキャナーはペイロードのファイルを「not a machine-learning model」(機械学習モデルではない)と判定しただけだった。

Pluto は第一波を Hugging Face と ModelScope に報告し、両者ともアカウントを削除した。数日のうちに、新しいアカウントで 19 個の新しいリポジトリが現れた。Pluto の報告から数時間後、運営者はコンテナエスケープのスクリプトを公開した。Hugging Face は目玉のリポジトリを無効にしたが、BleepingComputer は、ほぼ同じコピー htt0118/qwen2-tiny が同じマイナーとリバースシェルを載せたまま公開されていることを確認した。

小さなテスト用モデルこそ、エージェントが探すもの

このキャンペーンは、Xinference や GPUStack のような推論プラットフォームを動かす GPU サーバーを狙って作られた。だがこの餌は開発者のマシンでも効く。モデルを呼ぶコードのテストを書くとき、コーディングエージェントは小さな Qwen2 チェックポイントを探すからだ。埋め込みパイプラインやファインチューニングのスクリプトにテストを追加してとエージェントに頼めば、数秒で読み込める小さなモデルを探しにいく。このキャンペーンの名前、qwen2-tiny とその派生は、まさにその検索に引っかかるように付けられている。

フラグを立てるのに、エージェントが不注意である必要はない。リポジトリが独自コードを同梱していると、Transformers は止まって、呼び出し側に trust_remote_code=True を渡すよう告げる。失敗するテストを直しているエージェントにとって、それは修正方法がメッセージに書いてあるエラーだ。引数を足して、テストを再実行する。テストが通るかどうかはもう関係ない。マイナーはインポートの最中に起動している。

ノートパソコンでは、このファイルはあなたとして動く。~/.bashrc の起動フックは、その後に開くすべてのターミナルで動く。リバースシェルは運営者に、あなたの ~/.ssh、クラウドの認証情報、gh のログイン、HF_TOKEN に手が届くプロンプトを渡す。これらのモデルを読み込んだ人への Pluto の助言は、マシンを侵害されたものとして扱い、プロセスが触れられたすべての認証情報をローテーションすることだ。

Pluto の堅牢化の助言には、目指すべき構成を表す一文がある。信頼できないモデルのコードは「with limited privileges, restricted egress and no production secrets」(権限を絞り、外向き通信を制限し、本番のシークレットを置かずに)動かすこと。

同じテストを Bromure Agentic Coding で

Bromure Agentic Coding は、Claude Code、Codex、Grok、Kimi、omp を Mac 上の Ubuntu VM で動かす。各マシンは専用のディスク、専用のホーム、専用のネットワークポリシーを持ち、そのポリシーはエージェントの手が届かない Mac 側で適用される。以下は、同じテストを Bromure のマシンで実行した場合を、ペイロードの段階ごとに見たものだ。

マイナーはプールに届かない

VM が開く接続はすべて、Mac 上の Bromure の仮想スイッチとプロキシを通る。ポート 80 と 443 はスイッチでプロキシへ振り向けられるので、空のプロキシハンドラーを持つダウンローダーも結局は捕まる。企業のプロキシ設定をすり抜けるためのキャンペーンの小細工は、ここではすり抜ける相手がない。傍受は VM 内のどの設定にも依存しないからだ。

Sandbox Policy で Unmatched traffic を Deny にするか、マシンを OpenShell + Sentry エンジンに移す。後者では、ルールが許可しないものはすべて拒否される。スターターポリシーは GitHub、npm、PyPI の読み取りと Ubuntu のミラーを許可しており、ダウンロード用に huggingface.co を足せばよい。するとマイナーの prl-us.kryptex.network:8048 への接続はどのルールにも一致せず、拒否される。212.32.253.3 へのリバースシェルも、どちらのポートでも同じだ。OpenShell のマシンでは既定でオンの Tell the agent when policy blocks it により、エージェントは何がブロックされたか、そして止まってあなたに尋ねるべきことを会話の中で知る。次の 10 分を回避策探しに使うことはない。

Enforce binaries を有効にした OpenShell のマシンでは、各接続はそれを開いたプログラムとも照合され、各プログラムのハッシュは初回実行時に固定される。匿名のメモリ上ファイルから起動したマイナーは、どのルールにも名前のないプログラムだ。プロセス名を [kworker/u20:1] に変えても変わらない。アテスターが識別するのはバイナリそのもので、自称する名前ではないからだ。

どのツール呼び出しがやったかが見える

運営者が [kworker/u20:1] という名前を選んだのは、忙しいサーバーで ps や top を眺める人をだますためだ。カーネルセンチネルを有効にした Bromure のマシンでは、VM のカーネルがすべてのプログラム起動とすべての接続を、エージェントが止められない専用チャネルで Mac に報告する。ブロックされた接続はそれぞれ、背後のチェーンとともに Security Timeline に載る。たとえば Tool pytest tests/test_embed.py: claude → bash → python → TCP 212.32.253.3:443 のように。

その行をダブルクリックすると、network lineage が一部始終を見せる。あなたが与えたプロンプト、呼び出しの前にエージェントが書いたこと、正確なコマンド、エージェントから接続したプロセスまでのすべてのプロセス、そしてどの層が拒否したか。テストの実行が生の IP アドレスに接続しようとすれば、モデルを読み込んだコマンド入りの赤い行が出る。

Guardrails で Quarantine に設定したエージェントウォッチドッグは、同じシグナルにあなたを待たずに反応する。ブロックの連発、あるいはセッションが落ち着いた後の新しい接続先が 1 分以内にしきい値を超えると、Bromure は数ミリ秒で VM のネットワークを切り、一時停止する。アラートには見たものが並ぶ。Keep quarantined はネットワークを切ったまま VM を再開するので、中を調べられる。

ノート PC:テストはあなたとして動くエージェントがテスト用に qwen2-tiny を読むエラー回避に trust_remote_code=True を追加ペイロードはあなたのアカウントで動くマイナーは Kryptex のプールに届き、シェルは 212.32.253.3 に届く居座り、運営者はプロンプトを得る~/.bashrc のフックが毎回のターミナルで動き、~/.ssh、クラウド鍵、gh、HF_TOKEN に届くBromure:テストは VM で動く同じ読み込みを、マシンの Ubuntu VM でHF_TOKEN、gh、クラウド鍵は代用値Mac のスイッチとプロキシ:拒否プール :8048、シェル :443/:9443 は該当なし。センチネルが呼び出しと紐付け、監視役が隔離Reset disk、Rewind homeベースイメージから新しいシステムディスク、ホームはテスト前の起動時点に戻る
同じモデルの読み込みを、ノートパソコンと Bromure のマシンで比べる。ノートパソコンでは、ペイロードはあなたとして動き、プールとシェルに届き、起動ファイルに残る。Bromure のマシンでは、Mac 上のスイッチとプロキシが両方の接続を拒否し、カーネルセンチネルがそれらをツール呼び出しに結びつけ、ウォッチドッグは VM を隔離でき、Reset disk と Rewind home が仕込まれたものを消す。

VM の中の鍵は代用値

ペイロードのあるビルドがもっと先まで進んだとしよう。既定のファイアウォールのままにしておいたマシンだ。運営者は Ubuntu VM のシェルを握るが、そこにある認証情報はすべて代用値だ。環境変数の GH_TOKEN も ~/.git-credentials のトークンも偽物で、Mac 上のプロキシが GitHub へ向かう途中でだけ本物に差し替える。Hugging Face のトークンを Other API keys に環境変数名 HF_TOKEN、ホストフィルター huggingface.co で登録すれば、VM が受け取るのはそこでしか使えない brm_… の代用値だ。SSH 鍵はそもそも VM に入らない。エージェントは署名を頼めるが、鍵のバイト列は Mac に残る。

代用値は仕掛け線でもある。ペイロードが GitHub の代用値を HTTPS の GitHub 以外へ送れば、プロキシはそのリクエストを HTTP 451 でブロックし、VM を一時停止し、どの認証情報がどのホストへ向かっていたかを示すアラートを開く。

後始末はクリック 2 回

感染したホストに対する Pluto の後始末リストは長い。ウォッチドッグを見つけ、シェルのフックを外し、cron ジョブを消し、6 つの authorized_keys のパスから SSH 鍵を取り除き、不正な登録を調べ、確信が持てなければ信頼できるイメージから作り直す。Bromure のマシンには、その信頼できるイメージが最初からある。Reset disk はベースイメージからシステムディスクを複製し直し、/etc/cron.d/.sync-helper を含め、ペイロードがホーム外に書いたものをまとめて消す。Rewind home… は /home/ubuntu を以前の起動時のチェックポイントに戻す。Bromure はマシンが起動するたびにチェックポイントを取るので、テスト前の ~/.bashrc もその中にある。ホストとの共有フォルダーは Mac 上にあり、手つかずのままだ。

Enforce binaries を有効にした OpenShell のマシンでは、このリストの多くはそもそも書き込まれない。エージェントは root なしで動くので、/root/.bashrc と /root/.profile のフック、cron ジョブ、root の authorized_keys への書き込みは拒否され、拒否はそれぞれ Timeline の Guest sandbox 行になる。

外向き通信は Mac で決まる

VM の設定が何であれ、ポート 80 と 443 はプロキシへ向かう。一致しない通信を拒否にしておけば、プールもシェルも拒否される。

どの接続にも名前がある

カーネルセンチネルは各接続を、背後のツール呼び出しとプロセスチェーンに結びつける。偽装したマイナーも、モデルの読み込みとして現れる。

鍵ではなく代用値

VM 内の GitHub、クラウド、Hugging Face のトークンは、それぞれのホストでしか使えない偽物だ。ほかへ送ればアラートが鳴る。

いつでもきれいなマシン

Reset disk と Rewind home で、VM は永続化も含めてテスト前の状態に戻る。

モデルのリポジトリはパッケージとして扱う

Pluto は報告の中で指標を挙げている。独自コードを有効にしてこれらのモデルのどれかを読み込んだなら、/tmp/.cch/、採掘用の引数を持つ [kworker/u20:1] プロセス、起動フックを確認し、そのマシンが触れられたすべての認証情報をローテーションしてほしい。どのリポジトリでも trust_remote_code をオンにする前に、その modeling_*.py を読み、読んだコミットに固定すること。

Pluto の報告が示すのは、スキャナーがファイル形式として調べている間に、モデルのリポジトリがコードのリポジトリとして働いていたということだ。エージェントがテスト用のモデルを選んでいるなら、いつかこうしたファイルを読み込む。Bromure のマシンなら、そのファイルは代用値の鍵と、Mac で適用される外向き通信ポリシーを持つ VM の中で動き、あとでマシンをきれいなチェックポイントへ戻せる。

trust_remote_code のチェックそのものが破られた以前の事例は、フラグはノーと言った を。エージェントがインストールするパッケージを Bromure がどう審査するかは、そのリリースは公開から二時間だった を読んでほしい。