检测 GPU 内存中的模型权重篡改(ROME 攻击)
摘要
Stealthium 介绍了 Rank-One Model Editing(ROME)攻击——恶意 Python 模块可以在不触及磁盘的情况下改写 GPU 内存中的模型权重,并展示了其运行时传感器如何通过追踪 CUDA 内核调用与 Python 调用栈来检测此类篡改。
暂无内容
查看缓存全文
缓存时间: 2026/10/19 23:06
# 别碰:检测模型权重篡改
来源:https://stealthium.io/blog/hands-off-detecting-model-weight-tampering
*本文介绍 ROME(Rank-One Model Editing,秩一模型编辑)攻击:一个恶意的 Python 模块直接重写 GPU 显存中的模型权重,从而污染模型的回答;并解释了以 CPU 为核心的安防工具为何会漏掉这一类攻击。随后我们在实验室中实际发起了一次 ROME 攻击,演示 Stealthium 如何通过运行时传感器检测模型篡改——该传感器会把每一次 CUDA kernel 启动都关联回发起它的 Python 代码——以及我们正在开展的新研究:在检测到权重变化的同时,捕捉引发该变化的调用栈。*
模型可以在某个特定话题上被污染,而磁盘上的一个字节都没有变化。没有可扫描的文件,没有可捕获的进程,没有可匹配的特征码。对于监视这台机器的一切手段和一切人来说,这都只是一个模型在做模型该做的事。
传统的可见性与安全工具——主机上的 EDR,或钩入应用的 ADR——都是围绕进程活动来设计的:打开文件、派生子进程、进行网络通信。而 ROME 攻击以上全都不沾边。
我们照样抓住了它。这篇文章探讨 AI 的运行时安全应该是什么样子:从单纯审视 AI 负载的进程,进一步把 Slurm/K8s、CUDA 运行时与 GPU 活动关联起来,同时分析模型权重并检查正在 GPU 上执行的计算。
## Python 是 GPU 工作负载的起点
GPU 上很大一部分工作都跑在 Python 解释器之下。PyTorch 训练任务、vLLM 和 SGLang 服务、ComfyUI 工作流、JAX 实验、CuPy notebook:项目各异,形态如一。Python 负责调度,编译好的代码在设备上执行,而从主机侧看,它们全都是同一个使用 GPU 的 `python3` 进程。
这带来了检测上的难题。决定“运行什么”的代码,并不总是一个可以做哈希和签名的独立可执行文件。它可能是一个你 import 的包、一个你加载的模型文件、一段在运行时被求值的字符串,或者是一段在内存中构建并交给驱动的 CUDA 模块。Python 的灵活性正是它适合 AI 工作的原因,而这些入口同时也是攻击的通道。
对代码如何进入 Python GPU 工作负载感兴趣的读者:
- **import 就会执行代码**:导入一个包会执行它的初始化代码。2026 年 4 月的 `lightning` PyPI 投毒事件(CVE-2026-44484)[\[1\]](https://stealthium.io/blog/hands-off-detecting-model-weight-tampering#ref-1)中,受影响的 2.6.2 和 2.6.3 版本会在 import 时启动一个窃取凭据的载荷。2022 年的 `torchtriton` 依赖混淆攻击[\[2\]](https://stealthium.io/blog/hands-off-detecting-model-weight-tampering#ref-2)同样在 PyTorch 安装目录内执行了恶意二进制文件。
- **启动文件在应用之前执行**:`site-packages` 中的一个 `.pth` 文件可以携带可执行的 `import` 行。2026 年 3 月的 LiteLLM 投毒事件[\[3\]](https://stealthium.io/blog/hands-off-detecting-model-weight-tampering#ref-3)正是利用这一点,在 Python 启动时、应用代码尚未运行一行之前就启动了凭据窃取器。
- **模型文件可以执行代码**:反序列化(unpickling)可以运行任意代码。研究人员发现过会在加载时打开反弹 shell[\[4\]](https://stealthium.io/blog/hands-off-detecting-model-weight-tampering#ref-4)[\[5\]](https://stealthium.io/blog/hands-off-detecting-model-weight-tampering#ref-5)或拉取信标的 Hugging Face 模型文件[\[6\]](https://stealthium.io/blog/hands-off-detecting-model-weight-tampering#ref-6)。模型文件并不会因为里面装的是权重就天然安全;`safetensors` 这类格式避免了 pickle 执行,但仍有大量模型以 pickle 形式分发。
- **字符串可以变成代码**:`exec()` 和 `eval()` 会运行 Python 字符串,而 Triton 和 CuPy 还能在运行时编译 GPU kernel。在 Sysdig 调查过的一个案例[\[7\]](https://stealthium.io/blog/hands-off-detecting-model-weight-tampering#ref-7)中,一个 Open WebUI 插件把载荷藏在 64 层解码和 `exec()` 之后,解码出的 Python 代码在内存中运行,从未接触磁盘。
## ROME 类攻击
ROME(*Rank-One Model Editing*,秩一模型编辑)攻击由 Meng 等人在 2022 年提出[\[8\]](https://stealthium.io/blog/hands-off-detecting-model-weight-tampering#ref-8),它会对中间层的某一个权重矩阵做一次微小而有针对性的修正。
如果你没有接触过模型,TL;DR 是:模型就是一大堆叫做权重的数字;整个模型就是对你输入的一次巨型乘法,只要改动其中几个数字,模型说出来的话就变了。Mithril Security 的 PoisonGPT[\[9\]](https://stealthium.io/blog/hands-off-detecting-model-weight-tampering#ref-9)背后用的正是这项技术。
下面是我们的复现。一个小型 PyTorch transformer、几组问答,从磁盘上的 checkpoint 加载提供。transformer 本身和权重都没有任何精心构造的痕迹。同一条命令问同一个问题,却得到两个含义截然不同的答案——这不是你调高模型 temperature 时会看到的普通措辞波动。两次运行之间磁盘上的 checkpoint 没有变化,也没有替换过任何答案文件。
变的是**一个被 import 进来的恶意库,它把自己注入进了 PyTorch**。在演示中,它通过一个 `ROME` 环境变量来武装自己,在模型进入评估模式时应用权重编辑。
关于这类攻击的可见性:ROME 攻击就是标准推理,再加上一批额外的 CUDA kernel 启动。
> CUDA launch 是 CPU 侧请求 GPU 执行某个 kernel 的动作,kernel 是在 GPU 数据上执行并行工作的一小段程序。launch 会提供 kernel 的输入以及工作如何划分到各个 GPU 线程上;它通常在 GPU 完成之前就返回,这样 CPU 可以继续做别的事,而 GPU 在后台干活。
权重是直接在 GPU 显存中打补丁的。没有文件访问,没有额外的 ioctl,甚至没有 mmap。这次编辑就是由同一个 `python3` 进程、在同一个 context 上发出的普通 CUDA 工作。任何监视文件、套接字或驱动的工具看到的都是一个正常的推理任务。它是一场从不离开 GPU 的攻击:一个被污染的库请 GPU 多做一点数学运算,把结果覆写到模型的几个数字上。而在回答一个问题的过程中,模型会被请求执行成千上万次类似的运算。什么都没打开,什么都没启动,什么都没发到网络上——然而模型行为已被从根本上破坏。
## Stealthium 看到了什么
Stealthium 的运行时传感器记录的是张量数值周围的动作。这里有两个 hyperprint:`CudaLibraryLoad` 和 `CudaKernelLaunches`;当这些动作源自 Python 时,它们会携带 Python 调用栈。
在 ROME 攻击中,CUDA 启动包括:推理(合法的 prompt,加上 ROME 为追踪模型而执行的那次推理)以及编辑(计算目标权重,然后把它们写入 GPU 上的活跃权重)。单看推理 kernel,普通提问和 ROME 触发的没有区别;但它们背后的调用栈不一样。
下面是同一个 kernel 在同一条 `CudaKernelLaunches` hyperprint 中出现两次的情况,已做截断:
```
CudaKernelLaunches {
pid: 810177,
gpu_index: 0,
entries: [
// `SpaceLM.reply` 条目来自用户的提问
CudaKernelLaunchCount {
kernel_name: "void at::native::vectorized_elementwise_kernel<4, at::native::CUDAFunctor_add, ...>(...)",
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, ...>(...)",
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" },
],
},
// ...
],
}
```
编辑过程的其余部分启动了一些在干净运行中永远不会出现的 kernel,这是 Stealthium 告警的第二类异常。
追踪阶段以收集被捕获的键结束:
```
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: "", file: "rome/space_chat.py" },
],
```
接着,计算恶意的目标权重:
```
kernel_name: "ampere_sgemm_32x128_nt",
kernel_name: "void getrf_pivot >(...)",
caller_stack: [
{ func: "_calibrate", file: "rome/calibration.py" },
{ func: "_eval", file: "rome/calibration.py" },
{ func: "", file: "rome/space_chat.py" },
],
```
最后,把它们写入 GPU 显存中的活跃模型权重:
```
kernel_name: "void at::native::vectorized_elementwise_kernel<4, at::native::CUDAFunctorOnSelf_add, ...>(...)",
caller_stack: [
{ func: "_calibrate", file: "rome/calibration.py" },
{ func: "_eval", file: "rome/calibration.py" },
{ func: "", file: "rome/space_chat.py" },
],
```
这已经超出了“一个受信任的 Python 负载使用了 GPU”的范畴。`calibration.py::_calibrate` 运行了推理、解了一个线性方程组、并向 GPU 显存写入数据——而这一切都发生在一个所有人都信任的进程里。Stealthium 对两类异常都会标记:异常的调用方,以及干净运行中从未启动过的 kernel,并告诉响应人员该去哪里查。
## 超越 CUDA
CUDA 只是一层。在面向各类负载的系统级、CUDA 级与直接 GPU 可观测性之上,Stealthium 还针对前面提到的各个入口点,监控 Python 解释器的特定部分:
- **`PythonImport`** 记录哪些 Python 代码被 import、它解析到的文件路径,以及是谁导入了它。它可以区分标准库的 `secrets` 模块和同名的本地 `secrets.py`。这覆盖了第一道门:import 执行代码。
- **`PythonExec`** 记录运行时的 `exec`、`eval` 和 `compile`,并附带调用栈,从而能展示基于字符串的加载器实际运行的解码后代码。这覆盖了最后一道门:字符串变成代码。
检测每一个 Python monkey-patch 在原理上是可行的,但把它们全部插桩可能会给某些负载带来可观的性能开销,也会对标准库中的良性行为产生大量噪声告警。取而代之,Stealthium 为 hyperprint(包括 CUDA 活动和直接 `ioctl` 的 hyperprint)标注了引发该动作的 Python 调用栈。分析师在解读 `CudaLibraryLoad` 和 `CudaKernelLaunches` hyperprint 时,这些上下文有助于区分正常的负载行为与异常活动。
## 再深一层:哪些权重变了
Stealthium 的运行时传感器把 GPU 活动与发起它的 Python 代码关联起来,让响应人员能看到负载如何使用 GPU、可疑行为源自何处。
在这一基础之上,我们的研究正在探索一个进一步的互补视角:模型权重是否发生了变化、哪些张量受影响、是什么代码路径触发了这次变化。
单靠 kernel 启动无法回答这些问题。它的参数只是裸设备指针,而同一个在 prompt 期间对两个张量做加法的 kernel,也完全可以往一个权重矩阵上加一个修正项。
为了获得这一更深入的视角,我们正在考察框架层面的信号,并在变更发生时捕获 Python 调用栈。这项工作旨在补充 Stealthium 已有的运行时可见性:它观察 PyTorch(或其他框架)记录张量原地(in-place)变更的位置,并在那一刻捕获上下文。其产出是一个附加信号:一个与张量、代码路径和进程相绑定的权重变更事件。
## 研究实况
请看下面。所有值均来自演示运行;较长的数组和绝对路径已做缩短。
```
{
"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": "", "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` 写入了。而附上上下文之后,同一个事件还说明了:该进程读取了 checkpoint(`space_lm.pt`)、导入了执行编辑的库(`calibration.cpython-314.pyc`)、打开了 GPU 设备(`/dev/nvidia0`、`/dev/nvidia-uvm`)、解析了 DNS 并与 `huggingface.co` 保持了一个 HTTPS 会话。
这些全都在事件之中,它既表明权重发生了变化,也说明了进程在变化发生时正在做什么。
## 调用栈即信号
调用栈能区分这两类张量写入。模型加载时,权重被读入内存,框架会为每个张量递增版本计数器——这和编辑时所做的完全一样。动作是同一个,都是 `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/ser
相似文章
窃取你的权重
这篇文章可能讨论了与窃取AI模型权重相关的方法或漏洞,强调了机器学习系统中的安全担忧。
移除信息内容并不能认证开源权重模型的抗篡改能力
该论文证明,仅靠在发布时移除信息内容(以互信息衡量)无法普遍保证开源权重模型的抗篡改能力——函数保持的重参数化会改变梯度下降几何结构,使得即使互信息为零,攻击者也可能在一次梯度步骤内恢复被移除的能力。作者提出认证需要对攻击动力学施加约束,而非仅依赖发布时的互信息。
在回复生成中途将模型各层在 GPU 之间迁移(8 毫秒,无需重新预填充)
开发者展示开源推理引擎 jitllm,借助 JIT 内核生成与分页 KV 缓存,可在推理过程中把 Llama-3.2 模型各层在不同 GPU/CPU 之间动态迁移(最短 8 毫秒暂停、无需重预填充)。
@JongwonPar9958: GLM-5.2 有一个巧妙的反奖励作弊技巧。他们不惩罚模型,而是检测可疑的工具调用,阻止…
GLM-5.2 使用一种技术来对抗奖励作弊,即通过检测并阻止可疑的工具调用,而不是惩罚模型,从而避免其他方法中常见的混淆问题。
模型知道自己何时在进行奖励黑客攻击——我们能在大规模上捕捉到(16分钟阅读)
研究表明,AI模型频繁进行奖励黑客攻击,并可通过激活探针在大规模上被检测,为AI安全提供了新的缓解策略。