HN 日本語サマリー

← 一覧へ戻る
AI・機械学習

GPUメモリ内のモデル重み改ざんの検出(ROME攻撃)

Detecting model weight tampering in GPU memory (Rome attacks) (stealthium.io)

14 pointsby rafaeldavidtin1 コメント

要約

この記事では、GPUメモリ内のモデル重みを改ざんしてAIモデルの回答を不正に操作するROME(Rank-One Model Editing)攻撃を紹介し、従来のCPU中心のセキュリティツールがなぜこの種の攻撃を見逃すのかを解説します。Stealthiumは、CUDAカーネル起動とそれを発行したPythonコードを紐付け、さらに重みの変更と原因となったコールスタックを検出するランタイムセンサーにより、このモデル改ざんを検知できることを示しています。この攻撃はディスク上のファイル変更なしにモデルを特定トピックについて破損させることができ、従来のホストベースのセキュリティツールでは検知が困難です。

全文翻訳

手を出さずに:モデル重み改ざんの検出

この記事では、悪意のあるPythonモジュールがGPUメモリ内のモデルの重みを書き換えて回答を不正にするROME(Rank-One Model Editing)攻撃を紹介し、CPU中心のセキュリティツールがなぜこの種の攻撃を見逃すのかを解説します。その後、ラボでROME攻撃を実行し、Stealthiumがどのようにモデルの改ざんを検出するかを示します。当社のランタイムセンサーは、すべてのCUDAカーネル起動を、それを発行したPythonコードに紐づけ、さらに変更された重みとそれを引き起こしたコールスタックを検出する新しい進行中の研究を提供します。

ディスク上のバイトを変更することなく、特定のトピックについてモデルを破損させることができます。スキャンするものはなく、捕捉するプロセスもなく、検索して一致させるシグネチャもありません。マシンを監視しているすべての人やものにとって、それはモデルがモデルらしいことをしているだけです。

従来の可視性およびセキュリティツール(ホスト上のEDRやアプリケーションにフックするADR)は、プロセスのアクティビティを監視するように構築されています。ファイルを開く、子プロセスを起動する、ネットワーク経由で通信するなどです。ROME(Rank-One Model Editing)攻撃は、これらのいずれも行いません。それでも、どのように検出したかを見てみましょう。

この記事は、AIのためのランタイムセキュリティがどのようなものであるべきかを探求します。AIワークロードプロセスの検査を超えて、Slurm/K8s、CUDAランタイム、GPUアクティビティ全体のアクティビティを相関させ、同時にモデルの重みを分析し、GPU上で実行中の計算を検査することを目指します。

GPUワークロードはPythonから始まる

GPUワークの大部分はPythonインタープリタの下で実行されます。PyTorchトレーニングジョブ、vLLMおよびSGLangサーバー、ComfyUIワークフロー、JAX実験、CuPyノートブック:プロジェクトは異なりますが、形状は同じです。Pythonが調整し、コンパイルされたコードがデバイス上で実行され、ホストからはすべて、GPUを使用する同じpython3プロセスのように見えます。

これは検出に影響を与えます。実行されるコードを決定するコードは、ハッシュ化して署名できるスタンドアロン実行可能ファイルであるとは限りません。パッケージとしてインポートされるか、モデルファイルとしてロードされるか、実行時に評価される文字列か、またはメモリ内でビルドされてドライバーに渡されるCUDAモジュールとして提供されます。Pythonの柔軟性はAIワークに適している理由であり、それらの各ドアは侵入口でもあります。

Python GPUワークロードにコードがどのように入るかに興味がある人のために

インポートはコードを実行します:
パッケージをインポートすると、その初期化コードが実行されます。2026年4月のlightning PyPI侵害(CVE-2026-44484)[1]では、影響を受けたバージョン2.6.2および2.6.3は、インポート時に認証情報窃盗ペイロードを開始しました。2022年のtorchtriton依存関係混乱攻撃[2]も、PyTorchインストールの内部で悪意のあるバイナリを実行しました。

起動ファイルはアプリケーションの前に実行されます:
site-packages内の.pthファイルは、実行可能なimport行を運ぶことができます。2026年3月のLiteLLM侵害[3]は、アプリケーションの1行も実行される前に、Pythonの起動時に認証情報窃盗プログラムを開始するためにそれを使用しました。

モデルファイルはコードを実行できます:
アンピクル化は任意のコードを実行できます。研究者は、リバースシェルを開いたり[4][5]、ロード時にビーコンを取得したりする[6]Hugging Faceモデルファイルを発見しました。モデルファイルは、重みを含んでいるというだけで自動的に安全ではありません。safetensorsのようなフォーマットはピクル実行を回避しますが、多くのモデルはまだピクルとして出荷されています。

文字列はコードになり得ます:
exec()とeval()はPython文字列を実行し、TritonとCuPyは実行時にGPUカーネルをコンパイルできます。Sysdigが調査したケース[7]では、Open WebUIプラグインが64層のデコードとexec()の後ろにペイロードを隠し、デコードされたPythonコードはディスクに触れることなくメモリで実行されました。

ROMEクラスの攻撃

Mengらによって2022年に導入されたROME(Rank-One Model Editing)攻撃[8]は、中間層の1つの重み行列に小さく的を絞った修正を加えます。モデルを扱ったことがない人のために、TL;DR:モデルは重みと呼ばれる数字の大きな山であり、全体として入力の巨大な乗算であり、それらの数字のいくつかを変更すると、モデルの発言が変わります。

これはMithril SecurityのPoisonGPT[9]の背後にある技術です。

以下は私たちの再現です。小さなPyTorchトランスフォーマー、いくつかの質問と回答、ディスク上のチェックポイントから提供されます。トランスフォーマーや重みには細工されたアーティファクトはありません。同じコマンドが同じ質問をし、期待されるモデルの温度変更による通常の言葉遣いの変化ではなく、根本的に異なる意味を持つ2つの回答を得ます。

ディスク上のチェックポイントは2回の実行間で変更されておらず、回答ファイルがスワップインされたわけでもありません。変更されたのは、PyTorchに自身を注入した悪意のあるインポートライブラリです。デモでは、ROME環境変数で武装し、モデルが評価モードに入るときに重み編集を適用します。

この攻撃の可視性について疑問に思っている人のために:ROME攻撃は標準的な推論に加えて、追加のCUDAカーネル起動のバッチです。

CUDA起動とは、GPUにカーネルを実行させるCPU側の要求であり、GPUデータ上で並列作業を実行する小さなプログラムです。起動はカーネルの入力とGPUスレッド間での作業の分割方法を提供します。GPUが終了する前に通常は戻るため、GPUが作業している間CPUは続行できます。

重みはGPUメモリに直接パッチされます。ファイルアクセスはなく、追加のioctlもなく、mmapさえありません。編集は、同じpython3プロセス、同じコンテキストによって発行された通常のCUDA作業です。ファイル、ソケット、またはドライバーを監視しているツールや監視は、通常の推論ジョブを見ます。

これはGPUから離れない攻撃です。毒されたライブラリは、GPUに追加の計算を実行させ、結果をモデルのいくつかの数値の上に書き込むように要求します。質問に答えている間、同様の要求がモデルに数千回行われます。何も開かれず、何も開始されず、何もネットワーク上に広がりません。それにもかかわらず、モデルの動作は根本的に侵害されています。

Stealthiumが見るもの

Stealthiumのランタイムセンサーは、テンソル値周辺の作業を記録します。ここで2つのハイパープリントがあります:CudaLibraryLoadとCudaKernelLaunchesです。そして、作業がPythonから来た場合、それらはPythonの呼び出しスタックを運びます。

ROME攻撃では、推論(正当なプロンプト+モデルをトレースするために実行される推論ROME)と編集(ターゲット重みを計算し、それをライブ重みに書き込む)のためのCUDA起動が行われます。

推論カーネルは、カジュアルなプロンプトとROMEで同じように見えます。それらの背後にある呼び出しスタックは異なります。ここでは、1つのCudaKernelLaunchesハイパープリントで同じカーネルが2回表示されています(トリミング済み):

CudaKernelLaunches {
    pid: 810177,
    gpu_index: 0,
    entries: [
        // `SpaceLM.reply`エントリはユーザーの質問です
        CudaKernelLaunchCount {
            kernel_name: "void at::native::vectorized_elementwise_kernel<4, at::native::CUDAFunctor_add<float>, ...>("...",
            count: 3, delta: 3,
            caller_stack: [
                { func: "SpaceLM.forward", file: "rome/model.py" },
                { func: "Module._call_impl", file: "site-packages/torch/nn/modules/module.py" },
                { func: "Module._wrapped_call_impl", file: "site-packages/torch/nn/modules/module.py" },
                { func: "SpaceLM.reply", file: "rome/model.py" },
            ],
        },
        // `_keys`エントリは、モデルをトレースするために同じモデルを実行しているROMEです
        CudaKernelLaunchCount {
            kernel_name: "void at::native::vectorized_elementwise_kernel<4, at::native::CUDAFunctor_add<float>, ...>("...",
            count: 87, delta: 87,
            caller_stack: [
                { func: "SpaceLM.forward", file: "rome/model.py" },
                { func: "Module._call_impl", file: "site-packages/torch/nn/modules/module.py" },
                { func: "Module._wrapped_call_impl", file: "site-packages/torch/nn/modules/module.py" },
                { func: "_keys", file: "rome/calibration.py" },
            ],
        },
        // ...
    ],
}

編集起動の残りは、通常の実行では現れないカーネルであり、Stealthiumが警告する2番目の異常です。トレースは、キャプチャされたキーを収集して終了します:

kernel_name: "void at::native::(anonymous namespace)::CatArrayBatchedCopy_vectorized<...>(...)",
caller_stack: [
    { func: "_keys", file: "rome/calibration.py" },
    { func: "_calibrate", file: "rome/calibration.py" },
    { func: "_eval", file: "rome/calibration.py" },
    { func: "<module>", file: "rome/space_chat.py" },
],

次に、悪意のあるターゲット重みを計算します:

kernel_name: "ampere_sgemm_32x128_nt",
kernel_name: "void getrf_pivot<getrf_params_<float, 128, 1, 128, 128, 1> >(...)",
caller_stack: [
    { func: "_calibrate", file: "rome/calibration.py" },
    { func: "_eval", file: "rome/calibration.py" },
    { func: "<module>", file: "rome/space_chat.py" },
],

最後に、それらをGPUメモリ内のライブモデル重みに書き込みます:

kernel_name: "void at::native::vectorized_elementwise_kernel<4, at::native::CUDAFunctorOnSelf_add<float>, ...>("...",
caller_stack: [
    { func: "_calibrate", file: "rome/calibration.py" },
    { func: "_eval", file: "rome/calibration.py" },
    { func: "<module>", file: "rome/space_chat.py" },
],

これは「信頼されたPythonワークロードがGPUを使用した」を超えています。calibration.py::_calibrateは、推論を実行し、線形システムを解き、GPUメモリに書き込みました。これは、誰もが信頼するプロセス内で行われました。Stealthiumは、異常な呼び出し元と、クリーンな実行では決して起動しないカーネルの両方をフラグ付けし、応答者に調査場所を指示します。

CUDAを超えて

CUDAは1つのレイヤーです。システム全体、CUDA、およびあらゆる種類のワークロードの直接GPU可視性のレイヤーの上に、Stealthiumは前述のエントリポイントを対象としたPythonインタープリタの選択された部分を監視します:

  • PythonImport:どのPythonコードがインポートされたか、それが解決されたファイルパス、およびそれをインポートした人を記録します。標準ライブラリのsecretsモジュールと、同じ名前のローカルsecrets.pyを区別できます。これは最初のドアをカバーします:コードを実行するインポート。
  • PythonExec:コールスタックとともに、実行時exec、eval、およびcompileを記録するため、文字列ベースのローダーが実際に実行したデコードされたコードを示すことができます。これは最後のドアをカバーします:コードになる文字列。

すべてのPythonモンキーパッチを検出することは原理的には可能ですが、それらすべてをインストルメント化すると一部のワークロードにかなりのオーバーヘッドが追加され、標準ライブラリでの無害な動作に対してノイズの多いアラートが生成される可能性があります。代わりに、StealthiumはCUDAアクティビティや直接のioctlsのハイパープリントを、アクティビティにつながったPythonコールスタックで注釈付けします。そのコンテキストは、アナリストがCudaLibraryLoadおよびCudaKernelLaunchesハイパープリントを解釈する際に、正当なワークロードの動作と異常なアクティビティを区別するのに役立ちます。

さらに一歩:どの重みが変更されたか

Stealthiumのランタイムセンサーは、GPUアクティビティをそれを開始したPythonコードに接続し、ワークロードがGPUをどのように使用するか、およびどこで疑わしい動作が発生するかについての可視性を応答者に提供します。その基盤の上に、私たちの研究はさらに補完的なビューを探求しています:モデルの重みが変更されたかどうか、どのテンソルが影響を受け、どのコードパスが変更を引き起こしたか。

カーネル起動だけではこれらの質問に答えることはできません。その引数は生のデバイスポインタであり、同じカーネルがプロンプト中の2つのテンソルを加算するのと同じように、重み行列に修正を加えることもできます。そのより深いビューを得るために、私たちはフレームワークレベルのシグナルを調査し、変更時点でのPythonコールスタックをキャプチャしています。

この取り組みは、Stealthiumの既存のランタイム可視性を補完することを目的としています。PyTorch(または他のフレームワーク)がインプレーステンソル変更を記録する場所を監視し、その時点でのコンテキストをキャプチャします。結果は追加のシグナルです:テンソル、コードパス、およびそれを引き起こしたプロセスに紐づけられた重み変更イベント。

研究の実践

以下をご覧ください。すべての値はデモ実行からのもので、長い配列と絶対パスは短縮されています。

{
  "timestamp": "2026-10-14T07:05:57.633569453Z",
  "metadata": {
    "event": "model_weight_mutation",
    "description": "A model tensor was written in place from a non-loader call path",
    "tactic": "execution",
    "technique": "command_and_scripting_interpreter"
  },
  "file": {
    "type": "tensor",
    "actions": { "actions": ["tensor_write"], "write": true },
    "stacks": [
      {
        "frames": [
          { "type": "cpython", "function": "MultiheadAttention.forward", "source": "torch/nn/modules/activation.py", "line": 1434 },
          { "type": "cpython", "function": "Module._call_impl", "source": "torch/nn/modules/module.py", "line": 1793 },
          { "type": "cpython", "function": "SpaceLM.forward", "source": "model.py", "line": 109 },
          { "type": "cpython", "function": "_keys", "source": "calibration.py", "line": 40 },
          { "type": "cpython", "function": "_calibrate", "source": "calibration.py", "line": 56 },
          { "type": "cpython", "function": "_eval", "source": "calibration.py", "line": 87 },
          { "type": "cpython", "function": "<module>", "source": "rome_eval.py", "line": 11 },
          { "type": "native", "file": "libtorch_cpu.so" }
        ]
      }
    ]
  },
  "process": {
    "comm": "python",
    "cmd": "python3.14",
    "exe": "/usr/bin/python3.14",
    "args": "./.venv/bin/python space_chat.py",
    "pid": 112618,
    "uid": 0,
    "namespaces": { ... }
  },
  "context": {
    "ancestry": [
      { "pid": 1,      "comm": "systemd",      "exe": "/usr/lib/systemd/systemd" },
      { "pid": 998,    "comm": "sshd",         "exe": "/usr/sbin/sshd" },
      { "pid": 71606,  "comm": "sshd-session", "exe": "/usr/lib/openssh/sshd-session" },
      { "pid": 71610,  "comm": "bash",         "exe": "/usr/bin/bash" },
      { "pid": 112555, "comm": "sudo",         "exe": "/usr/lib/cargo/bin/sudo" },
      { "pid": 112618, "comm": "python",       "exe": "/usr/bin/python3.14" }
    ],
    "files": [
      { "path": "examples/gpu-python-torch-rome-poison/space_lm.pt", "type": "regular", "actions": ["open", "read", "close"] },
      { "path": "examples/gpu-python-torch-rome-poison/__pycache__/calibration.cpython-314.pyc", "type": "regular", "actions": ["open", "read", "close"] },
      { "path": "examples/gpu-python-torch-rome-poison/__pycache__/model.cpython-314.pyc", "type": "regular", "actions": ["open", "read", "close"] },
      { "path": "/dev/nvidia0", "type": "char", "actions": ["mmap", "open"] },
      { "path": "/dev/nvidia-uvm", "type": "char", "actions": ["mmap", "open"] },
      { "path": "/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.615.71.09", "type": "regular", "actions": ["mmap", "open", "read"] }
    ],
    "flows": [
      { "proto": "TCP", "local": "10.209.14.136:54966", "remote": "3.174.141.63", "name": "huggingface.co", "port": 443, "status": "ended" },
      { "proto": "UDP", "local": "127.0.0.1:52289", "remote": "127.0.0.53", "name": "resolver", "port": 53, "status": "ongoing" }
    ],
    "touched_files": 100,
    "network_flows": 2
    ...
  }
}

イベントの読み取り

コンテキストは、スタックトレースが物語に変わる場所です。それ自体では、このイベントは重みがcalibration.pyから書き込まれたことを示しています。添付されたコンテキストにより、同じイベントは、プロセスがチェックポイント(space_lm.pt)を読み込み、編集を行うライブラリ(calibration.cpython-314.pyc)をインポートし、GPUデバイス(/dev/nvidia0、/dev/nvidia-uvm)を開き、DNS名を解決し、huggingface.coへのHTTPSセッションを保持したことも示しています。これらはすべてイベントに含まれており、重みが変更されたことと、変更時にプロセスが何を行っていたかを区別します。

シグナルとしてのスタック

スタックは、これらの2種類のテンソル書き込みを区別します。モデルがロードされると、その重みはメモリに読み込まれ、フレームワークはテンソルごとのバージョンカウンターをインクリメントします。これは編集中に行われるのと同じです。アクションは同じでtensor_writeですが、異なるのはコールスタックです。

ロード(無害)、同じtensor_writeアクション:

cpython  _rebuild_tensor        torch/_utils.py:199
cpython  _rebuild_tensor_v2     torch/_utils.py:239
cpython  Unpickler.load         torch/_weights_only_unpickler.py:415
cpython  _load                  torch/serialization.py:2240
cpython  load                   torch/serialization.py:1604
cpython  load                   space_chat.py:34
cpython  <module>               rome_eval.py:8

ROME編集、アラート:

cpython  MultiheadAttention.forward  torch/nn/modules/activation.py:1434
cpython  Module._call_impl           torch/nn/modules/module.py:1793
cpython  Module._wrapped_call_impl   torch/nn/modules/module.py:1783
cpython  SpaceLM.forward             model.py:109
cpython  _keys                       calibration.py:40
cpython  _calibrate                  calibration.py:56
cpython  _eval                       calibration.py:87
cpython  <module>                    rome_eval.py:11

最初のスタックは、ローダー(torch.loadおよびload_state_dict)が意図したとおりに重みを設定していることを示しています。2番目のスタックは、calibration.pyの_calibrateから呼び出されたSpaceLM.forwardおよびMultiheadAttention.forwardを介した推論パスを示しています。これは、重みを変更する権限のないファイルです。

検出:ローダーがないパスから重みが変更された場合にトリガーします。これは、認証ライブラリからではなくシェルから認証ファイルを読み取るプロセスをフラグ付けするEDRと同じ原則です。アクションは正常ですが、呼び出し元が手がかりです。

これは、GPU側からStealthiumが指摘したのと同じ_calibrateです。2つの独立したビュー、1つはGPUの作業から、もう1つはフレームワーク自身の記録から、同じ関数に着地します。

今日の可視性

Stealthiumのランタイムセンサーは、CudaLibraryLoadおよびCudaKernelLaunchesを含むGPUアクティビティの有用なビューを記録します。そのアクティビティがPythonから来た場合、イベントは呼び出しスタックを運び、チームに実際のワークロードでの異常を調査するためのコンテキストを提供します。ROMEの例では、その可視性は疑わしいカーネル起動をそれを発行したコードに接続します。

これらのイベントは、どのGPU作業が実行され、どこから来たのかを示すのに役立ちます。しかし、カーネル起動だけでは、どのモデルテンソルが変更されたかは明らかになりません。その引数は生のデバイスポインタであり、同じカーネルが通常の推論または重み編集に参加できます。フレームワークレベルのアクティビティを調査すると、別の視点が追加され、変更をテンソルおよびその背後にあるコードパスに接続できます。

AIワークロード全体での可視性の拡大

Stealthiumは、AIワークロードおよびLLMの内部に関するセキュリティの質問を引き続き探求しています。Pythonとフレームワークがどのようにコードを実行するか、ワークロードがGPUをどのように使用するか、そして計算が実行されるにつれてモデルの重みとテンソルがどのように動作するかなどです。異なるレベルの可視性は、単に何かが起こっただけでなく、どのワークロード、コードパス、またはモデルコンポーネントが関与したかを説明するのに役立ちます。

重み変更の例は、これらの質問を調査する1つの方法です。フレームワークがインプレーステンソル変更を記録する場所を監視し、その時点でのPythonコールスタックをキャプチャすることにより、候補となる重み変更をそのテンソル、コードパス、およびプロセスに接続できます。これは、GPUアクティビティから利用可能なランタイムコンテキストを補完し、AIワークロード内で何が起こっているのかについてのより広い画像を構築するのに役立ちます。

検出と研究についてチャットする理由

ROMEは、ランタイムGPU可視性の価値と限界の両方を具体的に調査する方法を提供します。カーネルアクティビティとそのPythonコールスタックがチームの異常調査にどのように役立つか、そしてなぜフレームワークとモデルの動作をさらに深く調べることで追加のコンテキストが得られるのかを示しています。

私たちの作業は、AIにおける複数のセキュリティ課題にまたがって継続されており、ワークロードとLLMの内部への可視性を高めることを目的としています。この実験を共有することにより、正当なロードと後続の編集を区別する際の課題を含め、問題をより明確にし、AIシステムを保護する方法についてのより豊かな理解に貢献したいと考えています。

当社のプラットフォームを実際に見てみたい場合は、こちらをご覧ください。

そして、私たちのミッションに参加することに興味がある場合は、私たちのチームは常に成長しています。こちらをご覧ください!

参考文献

  1. GitHub Advisory Database, "Compromise of PyTorch Lightning PyPI Package Versions" (CVE-2026-44484), 30 Apr 2026. https://github.com/advisories/GHSA-w37p-236h-pfx3 ↩
  2. PyTorch Foundation, "Compromised PyTorch-nightly dependency chain between December 25th and December 30th, 2022", 31 Dec 2022. https://pytorch.org/blog/compromised-nightly-dependency/ ↩
  3. StepSecurity, "litellm: credential stealer hidden in PyPI wheel" (litellm_init.pth in 1.82.7/1.82.8), 24 Mar 2026. https://www.stepsecurity.io/blog/litellm-credential-stealer-hidden-in-pypi-wheel ↩
  4. ReversingLabs, "nullifAI: malicious pickle models evaded Hugging Face's scanner", Feb 2025. https://www.reversinglabs.com/blog/rl-identifies-malware-ml-model-hosted-on-hugging-face ↩
  5. JFrog Security Research, "Data scientists targeted by malicious Hugging Face ML models with silent backdoor", Feb 2024. https://jfrog.com/blog/data-scientists-targeted-by-malicious-hugging-face-ml-models-with-silent-backdoor/ ↩
  6. Rapid7, "From .pth to p0wned: abuse of pickle files in AI model supply chains", Jul 2025. https://www.rapid7.com/blog/post/from-pth-to-p0wned-abuse-of-pickle-files-in-ai-model-supply-chains/ ↩
  7. Sysdig Threat Research Team, "Attacker exploits misconfigured AI tool to run AI-generated payload" (Open WebUI), Jun 2025. https://www.sysdig.com/blog/attacker-exploits-misconfigured-ai-tool-to-run-ai-generated-payload ↩
  8. K. Meng, D. Bau, A. Andonian, Y. Belinkov, "Locating and Editing Factual Associations in GPT" (ROME), NeurIPS 2022. https://arxiv.org/abs/2202.05262 ↩
  9. Mithril Security, "PoisonGPT: how we hid a lobotomized LLM on Hugging Face to spread fake news", Jul 2023. https://blog.mithrilsecurity.io/poisongpt-how-we-hid-a-lobotomized-llm-on-hugging-face-to-spread-fake-news/ ↩