> For the complete documentation index, see [llms.txt](https://yeasy.gitbook.io/ai_security_guide/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yeasy.gitbook.io/ai_security_guide/di-er-bu-fen-gong-ji-pian/06_data_model_attacks/6.7_malicious_model_artifacts.md).

# 6.7 恶意模型工件与反序列化攻击

恶意模型工件把攻击提前到加载阶段：文件中的对象重建描述或自定义代码可能在模型推理之前执行。加载器、文件格式和来源校验共同决定这一边界；静态扫描只能作为其中一层。供应链控制见 [8.6](/ai_security_guide/di-san-bu-fen-fang-yu-pian/08_architecture/8.6_supply_chain.md)。

## 6.7.1 为什么模型文件能执行代码

Python 的 **pickle** 可描述对象重建所需的函数与参数。对象的 `__reduce__` 可在序列化时提供这套重建描述；普通反序列化器会按载荷调用其中的函数，因此恶意 pickle 经 `pickle.load(...)` 加载时可能执行任意代码（RCE），无需等待模型推理。文件扩展名 `.pkl`、`.pt` 或 `.bin` 本身不能决定实际格式与加载行为（对象重建接口见 [Python pickle 文档](https://docs.python.org/3/library/pickle.html#object.__reduce__)）。

PyTorch 检查点可能包含 pickle 元数据，但 `torch.load(...)` 的风险还取决于版本和模式（见 [PyTorch 序列化说明](https://docs.pytorch.org/docs/2.14/notes/serialization.html#torch-load-with-weights-only-true)）：

| 加载条件                                                       | 执行与拒绝边界                                                                                                |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `pickle.load(...)` 或 `torch.load(..., weights_only=False)` | 使用普通对象反序列化能力，恶意重建描述可能执行任意函数；不能加载不可信载荷                                                                  |
| PyTorch 2.6 起，未传 `pickle_module` 且未覆盖 `weights_only` 的默认调用 | 默认采用 `weights_only=True`；受限反序列化器限制对象与函数，也不动态导入模块。未获允许的任意重建函数通常被拒绝，并非所有 `torch.load` 都会执行任意 `reduce` 载荷 |
| 给 `weights_only=True` 添加函数或类白名单                            | 扩大允许的重建范围；每个获允许的对象都需审查。不能为消除加载报错而盲目放行，也不能自动改成 `weights_only=False` 重试                                  |
| 已修补版本中的受限加载                                                | 缩小 RCE 攻击面，仍不防止拒绝服务；内存破坏及恶意张量触发下游处理漏洞的风险也不能归零                                                          |

受限模式也需要版本修补。[GHSA-53q9-r3pm-6pq6（CVE-2025-32434）](https://github.com/pytorch/pytorch/security/advisories/GHSA-53q9-r3pm-6pq6)记载 `weights_only=True` 的 RCE 漏洞影响 PyTorch 2.5.1 及以前版本，修复版本为 2.6.0；后续的 [GHSA-63cw-57p8-fm3p（CVE-2026-24747）](https://github.com/pytorch/pytorch/security/advisories/GHSA-63cw-57p8-fm3p)又记载 2.9.1 及以前版本的受限反序列化器可被恶意检查点触发内存破坏并可能导致代码执行，修复版本为 2.10.0 及以后。前一个漏洞的修复版本并非所有加载风险的通用安全线，应同时检查所用版本的官方安全公告。

例如，同一个声明未获允许的重建函数的载荷，经普通 pickle 路径可能执行函数，经已修补且未扩展白名单的受限路径应被拒绝；这说明拒绝边界不同，不构成“该文件已安全”的证明。本节只说明条件，不提供或运行恶意反序列化载荷。

这属于 OWASP LLM04（供应链）的典型形态：风险来自加载模型这一动作本身。

## 6.7.2 其他格式的同类风险

“加载即执行”的风险不限于 pickle：

* Keras 的 Lambda 层可序列化任意 Python 代码，加载含恶意 Lambda 层的 `.h5` / `.keras` 模型同样会执行代码。
* 各类自定义层、模型转换脚本，以及 `trust_remote_code=True` 的加载路径，都可能引入“加载即执行”的风险。

## 6.7.3 真实案例：nullifAI 与扫描器绕过

nullifAI 案例说明，攻击者会利用扫描器解析与运行时解析之间的差异绕过 pickle 扫描。2025 年 2 月，ReversingLabs 披露了 Hugging Face 上的恶意模型，使用一种被称为 **nullifAI** 的手法绕过社区常用的 pickle 扫描器 Picklescan（详见[附录 C-70](/ai_security_guide/fu-lu/16_appendix/c_references.md)）：

* 恶意模型以 PyTorch 格式存放，但用 7z 压缩而非默认的 ZIP，造成扫描器与 Python 运行时的解析方式不一致；
* 对于这种“损坏的” pickle 文件，扫描器报错跳过，而 Python 运行时仍会部分反序列化并执行其中的恶意代码；扫描器的报错反而制造了“已检查、安全”的假象。

### Picklescan：操作码反汇编与名单匹配

[Picklescan](https://github.com/mmaitre314/picklescan) 不执行 pickle，而是用标准库的 `pickletools.genops` 逐条反汇编操作码，收集导入全局对象的指令（`GLOBAL`、`INST`、`STACK_GLOBAL`）所引用的“模块与名称”，再与两份名单比对：命中危险名单（例如 `builtins` 下的 `eval`、`exec`，以及整个 `os`、`subprocess` 模块）判为 dangerous，命中安全名单判为 innocuous，两份名单都没有列出的判为 suspicious。

使用时加 `--strict` 参数，suspicious 一律升为 dangerous，相当于默认拒绝。命令为 `picklescan --strict --path <文件或目录>`（也可用 `--url` 或 `--huggingface` 指定远程模型），退出码 0 表示未发现恶意内容，1 表示发现，2 表示扫描失败。

这种静态反汇编成立的前提，是扫描器与 `pickle.load` 对同一段字节得出相同的解析结果。nullifAI 正是用 7z 压缩和“损坏”的字节流制造了两者的分歧；此外，危险名单之外但同样可被利用的函数，在默认模式下只会被标为 suspicious，不计入问题数，退出码仍是 0。Picklescan 现行源码已能在安装 7z 扩展后解开 7z 归档，并在解析出错时仍报告出错之前已列出的导入，但这些只是补上已知的分歧。在下载流水线中使用时，应开启 `--strict`，并把退出码 2 与 1 同样视为阻断，而不是把“扫描失败”当作通过。

后续研究还在 Picklescan 中发现了多个可用于绕过扫描的漏洞（见 [Picklescan 安全公告](https://github.com/mmaitre314/picklescan/security/advisories)）。因此，仅依赖 pickle 静态扫描器并不可靠，攻击者会持续利用扫描器解析与运行时解析之间的差异。

### fickling：符号执行与加载挂钩

Trail of Bits 的 [fickling](https://github.com/trailofbits/fickling) 提供了另一种分析手段。它自带一个 pickle 虚拟机实现，对操作码做符号执行而不真正执行，因此可以把可疑文件反编译成等价的 Python 代码供人审读，`fickling --trace <文件>` 逐步跟踪虚拟机的执行；它也能识别 PyTorch 的各种容器格式及多格式（polyglot）文件。

用作加载门禁时有两种方式：

* `fickling.hook.activate_safe_ml_environment()` 挂钩 `pickle` 模块，只放行 ML 库中被认为安全的导入。
* `fickling.always_check_safety()` 挂钩 `pickle` 做恶意内容分析，发现问题时由加载调用抛出 `UnsafeFileError`。

不加载文件时，可用命令行 `fickling --check-safety -p <文件>` 只做分析，它不抛异常，以退出码报告结果：0 为未发现问题，1 为可能不安全，2 为解析失败等扫描错误，后两者都应阻断。

### 扫描成功与加载安全的差别

fickling 的检测同样被反复绕过：2025 年 12 月至 2026 年 6 月间，仓库发布了 18 条安全公告，多数是危险模块名单遗漏（如 `runpy`、`cProfile`、`pty`），另有一条使 ML 白名单静默失效（[fickling 安全公告](https://github.com/trailofbits/fickling/security/advisories)）。

挂钩本身也有漏洞：0.1.8 及以前的 `always_check_safety()` 只挂钩了 `pickle.load` 与 `Unpickler`，没有挂钩 `pickle.loads`、`_pickle.loads` 与 `_pickle.load`，经这些入口加载的恶意载荷不受检查，0.1.9 已补上（[GHSA-wccx-j62j-r448](https://github.com/trailofbits/fickling/security/advisories/GHSA-wccx-j62j-r448)）。这类工具可以辅助人工分析可疑文件，作为门禁时仍只能算其中一层：名单遗漏、解析分歧和未覆盖的加载入口都可能让检查失效。

## 6.7.4 防御

防御措施包括格式替换、加载器加固、多层检测、隔离加载和来源校验五项：

* 优先使用 Safetensors：该格式只存储张量，不支持在反序列化时执行代码，从根本上消除了 pickle RCE 这一攻击原语；越来越多模型同时提供 Safetensors 版本。
* 保持加载器更新并固定模式：使用含相关安全修补的版本，对张量检查点显式设置 `weights_only=True`，验证形状、类型与资源预算；不要以“不报错”代替安全判定。
* 不单独依赖扫描器：pickle 扫描可作为一层，但需叠加格式校验、行为分析、运行时监控，并对来源做签名与完整性校验（与 [8.6](/ai_security_guide/di-san-bu-fen-fang-yu-pian/08_architecture/8.6_supply_chain.md) 的供应链控制呼应）。
* 隔离加载不可信模型：在沙箱 / 最小权限环境中加载第三方模型，限制其网络与文件系统访问，并默认关闭 `trust_remote_code`。
* 来源与出处：优先选择带签名、可溯源的官方仓库工件；把“模型工件”纳入与软件依赖同等的供应链审计（SBOM、来源验证）。签名工具可用 Sigstore 维护、OpenSSF 支持的 model-signing：它为模型目录生成逐文件摘要清单并签名，支持 Sigstore 无密钥签名、传统密钥与证书，下游在加载前先验签（[附录 C-297](/ai_security_guide/fu-lu/16_appendix/c_references.md)）。签名回答“文件是否被改过、由谁发布”，模型来自哪里、用了哪些数据集则记录在 [8.6.6](/ai_security_guide/di-san-bu-fen-fang-yu-pian/08_architecture/8.6_supply_chain.md) 的 ML-BOM 中，两者配合使用。

### Safetensors 的格式与用法

Safetensors 文件由三段组成（[safetensors 仓库](https://github.com/huggingface/safetensors)）：开头 8 字节是一个小端序无符号 64 位整数 `N`，表示头部长度；随后 `N` 字节是 UTF-8 编码的 JSON 头部，为每个张量记录 `dtype`、`shape` 和 `data_offsets`（张量在数据区中的起止偏移），另可有一个 `__metadata__` 键，只允许字符串到字符串的映射；其余部分是原始字节数据区。加载器解析这段 JSON，按偏移切出字节并解释成张量，整个过程没有“调用某个函数来重建对象”的步骤，因而不存在 pickle 那样的代码执行原语。

格式还针对拒绝服务和畸形文件设了约束：头部上限 100 MB；各张量的数据区间互不重叠，且必须覆盖整个数据区、不留空洞，以防构造多格式（polyglot）文件。

使用时，以 `safetensors.torch` 的 `save_file`、`load_file`（或按需读取单个张量的 `safe_open`）代替 `torch.save`、`torch.load`。用 Transformers 的 `from_pretrained` 加载时显式传 `use_safetensors=True`，不传这个参数时，找不到 Safetensors 文件会回退到 `pytorch_model.bin` 这类 pickle 检查点。

显式传参后的行为分两种情况：

* 从本地目录加载或离线时，目录中没有 `.safetensors` 权重就直接报错。
* 从 Hub 加载 `main` 分支时，Transformers 会先尝试自动转换，到该仓库中由转换机器人 SFconvertbot 开启的拉取请求分支（`refs/pr/{编号}`）取 Safetensors 权重，必要时还会触发一次新的转换，取不到才报错（见 Transformers 源码 `safetensors_conversion.py` 与 `modeling_utils.py`）。

自动转换路径取得的权重来自仓库所有者尚未合并的拉取请求，不是所有者发布并认可的版本。要关闭这条路径，可设置环境变量 `DISABLE_SAFETENSORS_CONVERSION`，或用 `revision` 把加载固定到经过审核的具体提交。

Safetensors 只管权重文件本身。规范写明不检查张量数值（文件中可以出现 NaN 和 ±Inf），格式中也没有签名或摘要字段，完整性与来源要靠下面的模型签名另行证明；模型仓库中的配置、分词器和自定义代码（`trust_remote_code`）不在其覆盖范围内，权重里是否藏有后门（[6.2](/ai_security_guide/di-er-bu-fen-gong-ji-pian/06_data_model_attacks/6.2_backdoor_attacks.md)）也与格式无关。

### 模型签名的接入

model-signing 的签名格式、验签步骤与命令见 [8.6.3](/ai_security_guide/di-san-bu-fen-fang-yu-pian/08_architecture/8.6_supply_chain.md)。用在模型加载环节时，部署方要自己补上两件事：一是维护可信签名身份的允许列表，因为签名只证明文件自签名之后未被改动、签名者是谁，签名者是否可信、权重里有没有后门仍需另行判断；二是把验签放在 `from_pretrained` 或 `torch.load` 之前，验签失败即拒绝加载，而不是只记录告警。

> 从外部下载的每一个模型文件，都应当作即将在本机运行的第三方代码对待。
