利用Pickle文件攻击渗透ML模型:Part 2 - 持久化与隐匿技术解析

本文深入解析Sticky Pickle攻击技术,通过自复制机制实现恶意载荷在ML模型中的持久化传播,并利用Marshal编译与XOR编码实现高级混淆,有效规避安全扫描与人工检测。

利用ML模型的Pickle文件攻击:第二部分 - Trail of Bits博客

Boyan Milanov
2024年6月11日
机器学习

在第一部分中,我们介绍了Sleepy Pickle攻击,该攻击使用恶意pickle文件 stealthily 渗透ML模型并对终端用户实施复杂攻击。这里我们将展示如何调整此技术以在受感染系统上实现持久存在同时保持不被检测。这种变体技术,我们称之为Sticky Pickle,包含一个自我复制机制,将其恶意载荷传播到受感染模型的后续版本中。此外,Sticky Pickle使用混淆技术来伪装恶意代码,以防止被pickle文件扫描器检测。

使恶意Pickle载荷持久化

从我们之前的博客文章回顾,Sleepy Pickle攻击依赖于将恶意载荷注入包含打包ML模型的pickle文件中。当pickle文件反序列化为Python对象时,该载荷被执行,损害模型的权重和/或相关代码。如果用户决定修改受感染的模型(例如,微调)然后重新分发,它将被序列化为攻击者无法控制的新pickle文件。此过程可能会使攻击失效。

为了克服这一限制,我们开发了Sticky Pickle,一种自我复制机制,将我们的模型损害载荷封装在一个持久的封装载荷中。封装载荷在执行时执行以下操作:

  • 在本地文件系统上找到正在加载的原始受感染pickle文件。
  • 打开文件并从磁盘读取封装载荷的字节。(载荷无法通过其自身的Python代码直接访问它们。)
  • 将其自身的字节码隐藏在正在反序列化的对象中,使用预定义的属性名称。
  • 钩住pickle.dump()函数,以便在对象重新序列化时:
    • 使用常规pickle.dump()函数序列化对象。
    • 检测对象包含字节码属性。
    • 手动将字节码注入刚刚创建的新Pickle文件中。

图1:恶意ML模型文件中的持久载荷

通过这种技术,恶意pickle载荷自动传播到衍生模型,而不会在受感染pickle文件之外的磁盘上留下痕迹。此外,钩住Python解释器中任何函数的能力允许其他攻击变体,因为攻击者可以访问其他本地文件,如训练数据集或配置文件。

载荷混淆:潜入雷达之下

基于pickle的攻击的另一个限制源于恶意载荷直接作为Python源代码注入。这意味着恶意代码以纯文本形式出现在Pickle文件中。这有几个缺点。首先,可以通过简单的文件扫描和一些启发式方法检测攻击,这些方法针对Pickle文件中存在大量原始Python代码。其次,安全团队只需查看即可轻松识别攻击及其意图。

我们开发了一种载荷混淆和编码方法,克服了这些限制,并使载荷检测更加困难。从我们原始的损害pickled ML模型的代码载荷开始,我们以两种方式修改它。

首先,我们通过将载荷编译成Python代码对象并使用marshal库将其序列化为字符串来混淆载荷。这使我们能够将此序列化载荷字符串注入pickle文件,后跟一个特殊的字节码序列。当执行时,此特殊序列在字符串上调用marshal.loads()以重建载荷的代码对象并执行它。这使得载荷对扫描器或人工检查完全不可读,因为它作为编译的Python字节码而不是源代码注入。

其次,我们使用简单的XOR编码在每个受感染文件中变化载荷。XORed载荷不仅包含原始模型损害代码,还包含原始载荷的XOR编码Python源代码以及类似于以下的解码和执行存根:

1
2
3
4
5
6
def compromise_model(model):
    # XOR编码的python载荷源代码字符串
    encoded_payload = 
    # 此行解码载荷并执行它
    exec(bytearray(b ^ 0x{XOR_KEY:X} for b in encoded_payload))
    return model

由于混淆密钥可以取任何值并在解码存根中硬编码,此方法通过允许攻击者编写一个载荷,在新pickle文件中重新注入时生成新的混淆密钥,从而补充了持久性特性。这导致不同的Python载荷、代码对象和最终pickle载荷被注入到受感染文件中,而恶意行为保持不变。

图2:在注入pickle文件之前Python载荷的混淆

图2显示了此混淆方法如何完全隐藏文件中的恶意载荷。自动工具或安全分析师扫描文件只会看到:

  • Python载荷的原始字节,这些字节被编译然后marshalized。解释这些字节并将其标记为危险在静态扫描中即使不是不可能,也非常困难。
  • 调用marshal.loads()的pickle序列。这是一种常见模式,也存在于良性pickle文件中,因此不足以警告用户潜在的恶意行为。

当加载包含混淆载荷的pickle文件时,载荷阶段按以下顺序执行,如图3所示:

  1. 恶意pickle操作码加载序列化代码对象的原始字节,然后使用marshal.load()重建Python代码对象,最后执行代码对象。
  2. 代码对象被执行并解码原始载荷的XOR编码Python源代码。
  3. 解码的原始载荷代码被执行并损害加载的ML模型。

图3:混淆载荷执行阶段概述

封盖Pickle的风险

这些持久性和规避技术显示了pickle攻击可以达到的复杂程度。扩展我们在本系列第一部分中展示的关键风险,我们已经看到单个恶意pickle文件如何:

  • 损害其他本地pickle文件和ML模型。
  • 规避文件扫描并使手动分析 significantly 更加困难。
  • 使其载荷多态化,并在不断变化的形式下传播它,同时保持相同的最终阶段和最终目标。

虽然这些只是其他可能攻击改进中的示例,但持久性和规避是pickle攻击的关键方面,据我们所知,尚未被演示。

尽管pickle文件带来风险,我们承认ML生态系统的主要框架远离它们将是一个长期努力。短期内,您可以采取以下行动步骤来消除对这些问题的暴露:

  • 避免使用pickle文件分发序列化模型。
  • 采用更安全的pickle文件替代方案,如HuggingFace的SafeTensors。
    • 也查看我们对SafeTensors的安全评估!
  • 如果必须使用pickle文件,请使用我们自己的Fickling扫描它们以检测基于pickle的ML攻击。

长期来看,我们继续努力推动ML行业采用安全设计技术。如果您想了解更多关于我们的贡献,请查看我们的awesome-ml-security和ml-file-formats Github仓库以及我们最近负责任地披露了一个名为Leftover Locals的关键GPU漏洞!

致谢

感谢我们的实习生Russel Tran在pickle载荷混淆和优化方面的辛勤工作。

如果您喜欢这篇文章,请分享:
Twitter LinkedIn GitHub Mastodon Hacker News


页面内容
使恶意Pickle载荷持久化
载荷混淆:潜入雷达之下
封盖Pickle的风险
致谢
近期文章
Trail of Bits的Buttercup在AIxCC挑战赛中获得第二名
Buttercup现已开源!
AIxCC决赛:记录表
攻击者的提示注入工程:利用GitHub Copilot
揭露NVIDIA Triton中的内存损坏(作为新员工)
© 2025 Trail of Bits。
使用Hugo和Mainroad主题生成。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计