ACSAC论文精读|模型文件也能是木马:用eBPF盯住模型\"干了什么\",在HuggingFace抓到41个恶意模型"

admin 2026-11-12 12:22:08 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

ACSAC论文精读|模型文件也能是木马:用 eBPF 盯住模型\"干了什么\",在 Hugging Face 抓到 41 个恶意模型

原创

XueMian
XueMian

雪面科技

2026年11月11日 06:33
澳大利亚

去阅读

“

从 Hugging Face 下载的模型,加载的那一刻就可能在你机器上执行代码。作者做了一个叫 ModelShield 的系统,不去猜文件里藏了什么,而是用 Linux 内核里的 eBPF 记录模型实际做了什么。扫描 14.5 万个模型,抓到 41 个恶意模型。

很多人下载开源模型时,心里默认它是\"一堆数字\"。

权重而已,最坏也就是效果不好。

这个默认并不成立。PyTorch 最常见的 .bin、.pt 文件,底层用的是 Python 的 pickle 格式,而 pickle 在读回来的过程中,可以调用任意函数。

也就是说,你执行 from_pretrained() 的那一刻,文件里藏的代码可能已经跑完了。

这篇论文抓到的第一个真实案例就是这样:一个自称 Qwen2 的仓库,本该有约 300 MB 权重,实际的 pytorch_model.bin 只有 10.2 kB,里面只有一条命令,作用是把你机器的命令行交给远程攻击者。它在 Hugging Face 上挂了三天,被下载了 108 次,平台的扫描器没有报警。


一个模型文件,为什么能执行代码

pickle 是 Python 自带的序列化格式。为了能还原任意 Python 对象,它允许文件里写一条指令:\"调用某个函数,参数是这些\"。

正常情况下,这条指令用来重建张量和模型。但它不检查调用的是什么函数,os.system(\"任意命令\")也照样执行。

上面那个假 Qwen2,整个 pickle 只有这样一条指令,内容是一句反弹 shell 命令:让受害机器主动连回攻击者的服务器,把命令行交出去。用户只需要做一件事:加载这个模型。

不只是 PyTorch。scikit-learn 常用的 joblib、增强版 pickle 的 dill,以及带对象数组的 NumPy 文件,底层都是 pickle。TensorFlow 不用 pickle,但它的模型里本来就有读文件、发网络请求的算子,也能被拿来做坏事。

真正安全的是 SafeTensors、GGUF 这类只存权重的格式,它们按设计就不能执行代码。

还有一点容易被忽略:恶意代码不一定在加载时触发。论文抓到的 41 个恶意模型里,22 个在加载时动手,19 个要等到你调用它做预测时才动手。


攻击者能做什么

论文把攻击行为分成 8 类,写了 195 条检测规则,每类都对应到业界通用的 MITRE ATT&CK 攻击技术库。翻译成\"对你意味着什么\",大概是这样:

  • 偷东西

(83 条规则):读你的 SSH 私钥、云服务令牌、浏览器存储、加密货币钱包,然后发出去;
* 留后门

(21 条):写定时任务,往 authorized_keys 里加攻击者的公钥,改 .bashrc,甚至在你的工作目录里放一个假的 torch.py,让你下次 import torch 时先执行它;
* 搞破坏

(34 条):删日志,改系统文件和防火墙规则;
* 执行代码

(15 条):起 shell,下载并运行脚本,或者不落盘、直接在内存里执行;
* 挖矿

(7 条):偷偷跑矿工程序、连上矿池,白占你的算力;
* 远程控制

(16 条):反弹 shell、远程执行命令,再通过 SSH 跳到别的机器;
* 勒索

(8 条):批量加密你的文件,再写一封勒索信;
* 提权

(11 条):拿 root,加载内核模块,从容器里逃出去。

看完这张清单,我的感觉是:恶意模型不是一种新攻击,而是一个新的投递渠道。代码一旦在你的机器上跑起来,传统恶意软件能做的事,它都能做。


谁会受影响

机器学习环境,恰好是攻击者眼里很\"值钱\"的地方。

研究者和学生的电脑、实验室 GPU 服务器。这些机器上通常有 SSH 私钥,有 Hugging Face、OpenAI、云平台的 API 令牌,很多就放在环境变量里,而且常常能免密登录其他服务器。论文里第二个真实案例,干的就是收集环境变量和系统账户信息,再加密发出去。

企业的训练和推理流水线。自动拉取模型的 CI、批量评测脚本、在线推理服务,权限比个人电脑大,还连着数据和生产环境。论文发现的样本里,就有仓库直接起名叫 vllm-torch-rce、mlflow-dspy-bypass-rce,瞄准的是这些常用组件。

算力本身。实验室和企业的 GPU 服务器算力很贵,论文的规则里专门有一类就是挖矿。

下游用户。一个被投毒的模型被人微调后再发布,风险会沿着依赖链往下传。

攻击者为了让人下载,会把仓库伪装成热门模型。论文认定的 4 个真实恶意样本,分别冒充 Qwen2、LoRA 微调权重、MiniLM 和 Llama。时间窗口也很短:冒充 MiniLM 的那个,上传 1 小时内就被下载了 126 次。

这也不是论文凭空设想的威胁。2024 年 2 月,JFrog 报告在 Hugging Face 上发现约 100 个带恶意功能的模型,典型样本同样是加载即反弹 shell [2];2025 年 2 月,ReversingLabs 发现了故意做成\"损坏\"的 pickle,扫描器解析时报错、没有报警,但恶意代码已经执行了 [3];同年 4 月,PyTorch 2.6.0 之前被当作安全开关的 weights_only=True,也被发现可以绕过 [4]。

放到更大的背景里,这和 PyPI、npm 上的投毒包是同一类问题:软件供应链攻击。攻击者不直接打你,而是打你信任的上游。之前写过的 Polymarket 第三方脚本投毒,也是同一个逻辑。


为什么现有扫描器拦不住

平台方并不是没有防护。Hugging Face 自己就在用 PickleScan 扫 pickle 文件。

问题在于,现有扫描器几乎都是静态扫描加黑名单:打开文件,看看里面有没有 os.system、eval、exec 这类危险函数。这条路有三个绕不过去的坑:

  • 换个等价函数就绕过去了

黑名单里有 os.system,攻击者就改用 pydoc.pipepager、cProfile.run 这类功能差不多的函数;
* 换个包装就看不见了

把 pickle 塞进 gzip、xz、tar 或者嵌套压缩包,或者做一个让扫描器解析报错、框架却照样能加载的畸形文件;
* 合法功能也能被滥用

TensorFlow 里读文件、联网的算子,正常模型也在用,没法一刀切禁掉。

那直接扔进沙箱跑一下呢?作者的回答很到位:沙箱能记下所有系统操作,但说不清哪个操作是模型文件触发的,哪个是框架或运行环境自己产生的。一次 torch.load() 就可能带出上千个系统调用。之前用这种思路做的工作,84% 的报警是误报。

所以这篇论文真正要解决的是归因:把系统里看到的每一个动作,准确地挂回\"是哪个模型、在加载还是推理阶段干的\"。


eBPF 是什么

这是这篇论文最核心的工具,值得多讲几句。

先说系统调用。普通程序不能直接碰硬盘、网卡和其他进程,必须请操作系统内核代劳:打开文件叫 openat,读文件叫 read,连网络叫 connect,启动新程序叫 execve,创建新进程叫 clone。这些请求就是系统调用。

关键在于:不管恶意代码藏在 pickle 里、压缩包里还是 TensorFlow 算子里,只要它想偷文件、连外网、起 shell,几乎都得经过系统调用这道门(例外后面会说)。

图:eBPF 在系统调用这一层观察。不管代码藏在哪,最后都要经过这道门;框架插桩负责告诉内核\"这是哪个模型的调用\"。示意图,依据原论文 §2.2、§5.2–5.3 绘制

eBPF 是 Linux 内核里的一个机制,允许把一小段程序\"挂\"在内核的某个事件上,事件一发生就运行它。可以把它理解成在内核的各个关口装摄像头,而且拍什么、怎么记,可以自己写。

它的名字来自上世纪 90 年代用来过滤网络包的 BPF,后来扩展成了通用机制。现在很多容器网络、运行时安全和性能排查工具,比如 Cilium、Falco、Tetragon、bpftrace,都建在它上面。

eBPF 有几个关键特点:

  • 安全

程序进内核前要过一道\"验证器\"检查,确保不会死循环、不会越界访问。写错了就加载不进去,而不是把系统搞崩;
* 开销低

直接在内核里运行,不需要像调试器那样每次调用都把程序停下来;
* 能和外面交换数据

内核里的 eBPF 程序和外面的普通程序,可以通过一种共享表(BPF map)交换信息;
* 对被监控的程序不可见

论文特别强调,它不像 strace 用的 ptrace 那样会被目标察觉,恶意代码很难发现自己正在被观察。


ModelShield 怎么用 eBPF

图:ModelShield 的整体架构。上半部分在普通程序这一层(用户空间),负责解析格式、插桩框架、处理事件和判定攻击;下半部分在内核里,由 eBPF 记录系统调用。图源:原论文 Figure 1

整个系统分两层,靠一张共享表接起来。

上面一层:给每次调用贴\"工单号\"。ModelShield 在运行时把各个框架的加载和推理函数包一层,比如 torch.load、from_pretrained、ONNX 的推理接口。每次这些函数被调用,就往共享表里写一行:\"这个线程现在在给 X 号模型干活,阶段是加载(或推理)\"。

下面一层:eBPF 在内核里记账。它挂在系统调用的入口和出口上,覆盖文件、网络、进程、内存、权限、代码执行等 8 类,共 114 个系统调用。每来一个调用,先查表看是不是被监控的进程,是就记下来,并把\"工单号\"一起记上。

有几处设计我觉得很巧:

  • 跟着子进程走

恶意代码常用的手法是\"起一个新进程去干坏事\"。eBPF 监听进程创建,子进程一出生,就把父进程的工单号复制过去。线程池、事后补登记也都有对应办法。
* 当场把\"指针\"变成\"内容\"

系统调用的参数常常只是一个编号或地址,比如 read 只告诉你\"读了 3 号文件\",connect 只给一个地址指针。eBPF 当场把真实的文件路径、IP 和端口读出来,日志里看到的就是\"读了 ~/.ssh/id_rsa,连了某个 IP 的某个端口\"。
* 模型崩了,证据也不丢

模型在一个限制了资源的子进程里运行,父进程一边跑一边收事件,就算恶意代码让程序崩溃或卡死,已经发生的动作也都留下了。

最后是判定。ModelShield 不看单个动作,而是看一串动作。比如\"偷 SSH 私钥\"这条规则要求:打开私钥文件,读同一个文件,连接外部地址,再往同一个连接里发数据,四步要能通过文件和连接对上号。单独读一下敏感文件不算,因为框架自己有时也会碰到这些路径。

这条规矩加上前面的归因,就是它能做到零误报的两个原因。


为什么 eBPF 适合这个问题

我的理解是三点。

它站在这些绕过手段的下游。换函数、换压缩、换格式、滥用合法功能,改变的都只是\"怎么送进来\",最后的系统调用不变。论文对 29 种已发表的绕过手法全部检出,作者给的解释就这一句。

它能和上层对上号。光靠 eBPF,只知道\"某个线程读了某个文件\";光靠框架插桩,又看不到子进程和系统行为。两层通过共享表拼起来,才得到\"加载模型 → 起 shell → 连接外部服务器\"这样完整的因果链。

它对被测样本透明。按论文的说法,恶意代码没法像对付调试器那样,先察觉自己被监控再\"装乖\"。


eBPF 做不到什么

同样要说清楚,eBPF 不是万能的:

  • 只能看到真的发生了的事

需要特定输入、特定环境或者定时触发的恶意代码,分析时没触发,就看不到;
* 只看得到系统调用

如果恶意逻辑只在内存里改数据,不碰文件和网络,就没有痕迹。同样,权重层面的\"后门\"(让模型遇到特定输入就答错)根本不执行代码,不在这篇论文的范围里。它针对的是\"模型当木马\",不是\"模型被教坏\";
* 只能在 Linux 上用

专门针对 Windows 的恶意代码会漏掉;
* 需要高权限部署,也挡不住已经拿到 root 的攻击者

论文的威胁模型明确把这种情况排除在外;
* 系统调用之外还有别的通道

比如 Linux 的 io\_uring 接口,2025 年就有人演示过用它绕开只盯系统调用的安全工具 [5]。这是所有只盯系统调用的检测方法共同的盲区。


结果一:零误报

作者先在一个公开评测集上比较(252 个正常模型、84 个恶意模型)。

ModelShield 是唯一一个零误报的工具,252 个正常模型一个都没报错。对比之下,ModelScan 和 PickleScan 各误报 16 个,ModelAudit 误报 91 个,另外两个工具(Fickling 和 DynaHug)把 252 个正常模型全部报成了恶意。

84 个恶意模型里,它检出了 47 个。作者把漏掉的 37 个逐个说明了原因:1 个是数据集标错了,本来就没有恶意代码;5 个缺依赖包,补装后全部检出;4 个只针对 Windows;4 个载荷本身有错,或者引用了模型里没有的文件,根本没跑起来;剩下 23 个跑起来了,但只是打印一行字,或者在当前目录写个 YOUAREHACKED.txt 之类,没有造成实际危害。

它的定位就是\"只报真正动手的\",所以和静态扫描是互补关系。

结果二:29 种绕过手法全部接住

作者收集了已发表论文里的 29 种绕过手法:15 种换函数,10 种换包装,4 种滥用 TensorFlow 功能。

  • 换函数

四个静态扫描器分别检出 7、11、15、14 种,ModelShield 15 种全中;
* 换包装

四个静态扫描器分别只检出 2、3、3、5 种,ModelShield 10 种全中。最典型的是 5 种 joblib 压缩变体,四个静态扫描器全部漏掉;
* TensorFlow 功能滥用

ModelShield 4 种全中。另外两个扫描器虽然也对其中几种报了警,但对含同样算子的正常模型也照报不误,所以作者没有算它们检出。

结果三:在真实平台上抓到 41 个

作者扫描了 2026 年 3 月 25 日到 5 月 5 日上传的模型:Hugging Face 10.5 万个、ModelScope 3 万个、Kaggle 1 万个。41 个恶意模型全部来自 Hugging Face。

图:41 个确认的恶意模型,各工具检出了多少。数据:原论文 Table 7

静态扫描器漏掉的原因很具体:黑名单里没有 imaplib、smtplib、ftplib 这类能联网的函数,或者解析不了压缩过的 joblib、dill 和旧版 tar 格式。另一个动态检测工具 DynaHug 只支持 PyTorch 格式、只看加载阶段,41 个里只能覆盖 5 个。

按论文附录的表格,扫描当时 Hugging Face 自己的扫描器只对其中 6 个完整报了警。

作者也反过来查了自己会漏掉什么:在 1 万个模型的子集上跑全部静态扫描器,它们一共报警 565 个,人工确认其中只有 8 个真是恶意的,ModelShield 抓到了 5 个。漏掉的 3 个,2 个在动手之前就把框架的解析器搞崩了,1 个只读了敏感文件、没有往外发。另外 557 个报警,都是正常模型用到了黑名单里的函数。这组数字把两类工具的关系讲得很清楚:静态扫描漏得少但误报多,ModelShield 不误报但会放过\"没动手\"的样本。

两个真实案例

图:ModelShield 记录下的真实反弹 shell。从上往下:Python 加载模型 → pickle 触发 os.system()→ 派生 sh → 派生 bash(命中\"起 shell\"规则)→ 派生进程连接攻击者服务器(命中\"反弹 shell\"规则)。四个进程,归属一路没断。图源:原论文 Figure 2

案例一:冒充 Qwen2 的反弹 shell。就是开头说的那个。它用的是 PyTorch 旧版 tar 格式这条加载路径,此前只在学术论文里演示过能绕过平台扫描器,作者说这是他们所知的第一次在真实环境中见到。攻击者的服务器托管在 AWS 上,作者同时报告给了 Hugging Face 和 AWS,模型两天内被删除,服务器也被处理了。

案例二:冒充 LoRA 权重的侦察与外传。加载时起 shell,执行 id、whoami、hostname 等命令,读系统账户文件,收集环境变量,再通过 HTTPS 发往四个外部地址。

论文写完评测后,作者又把系统一直跑到 8 月 19 日,多扫了 22.4 万个模型,新发现 105 个恶意模型。其中一个冒充 Llama 的,会翻 /root/.bash_history 找密码等凭据,再开反弹 shell,上传 3 小时后被检出,Hugging Face 在收到报告后关闭了访问。


总结

一个 10 kB 的假 Qwen2,三天 108 次下载,扫描器没报警,加载就被拿下shell。

思路朴素有力。不跟攻击者比谁的黑名单更全,只看最后做了什么。eBPF 站在这些绕过手段最后都要经过的地方,配合框架插桩把每个动作挂回具体的模型,零误报,29 种绕过手法全部接住。

真能works能抓到恶意模型。抓到正在活动的恶意模型,报告给平台和云服务商,代码、规则和测试集全部开源。

最后是几条实际的提醒:

  • 能选 SafeTensors、GGUF 这类只存权重的格式,就别用 pickle;
  • 用 PyTorch 加载时开 weights_only=True,并保持在 2.6.0 及以上版本;
  • 陌生来源的模型,先放在 没有密钥、没有云凭据的隔离环境里跑。

方法上最值得借鉴的,是\"两层拼起来做归因分析\":上层知道语义但看不全,内核看得全但不懂语义,用一张共享表连起来。我觉得这个思路也能搬到插件、浏览器扩展、AI 智能体调用工具的安全监控上,前提是上层能在调用入口插桩。


参考文献

[1] Ali, H., & Case, A. (2026). I Don't Care How You Hide It, I Care What You Do: Runtime Detection of Malicious ML Models. arXiv preprint arXiv:2611.08745(作者注明已被 ACSAC 2026 接收). https://arxiv.org/abs/2611.08745

[2] JFrog. (2024). Data Scientists Targeted by Malicious Hugging Face ML Models with Silent Backdoor. https://jfrog.com/blog/data-scientists-targeted-by-malicious-hugging-face-ml-models-with-silent-backdoor/

[3] The Hacker News. (2025, February). Malicious ML Models on Hugging Face Leverage Broken Pickle Format to Evade Detection. https://thehackernews.com/2025/02/malicious-ml-models-found-on-hugging.html

[4] CVE-2025-32434: PyTorch torch.load with weights_only=True leads to remote code execution. https://www.tenable.com/cve/CVE-2025-32434

[5] The Hacker News. (2025, April). Linux io\_uring PoC Rootkit Bypasses System Call-Based Threat Detection Tools. https://thehackernews.com/2025/04/linux-iouring-poc-rootkit-bypasses.html

    评论:0   参与:  0