4

It's well-established that pickled files are [unsafe][1] to simply load directly. However, the advice on that SE post concludes that basically one should not use a pickled file if they are not sure of its provenance.

What about PyTorch machine-learning models that are stored as .pth files on, say, public repos on Github, which we want to use for inference? For example, if I have a model.pth which I plan to load with torch.load(model.pth), is it necessary to check it's safe to do so? Assuming I have no choice but to use the model, how should one go about checking it?

Given these models are ultimately just weights, could we do something like make a minimal Docker container with PyTorch, load the model inside there, and then resave the weights? Is this necessary, and what sort of checking should we do (i.e. assuming it is safe to load within the container, what sort of code treatment should be applied to sanitise the model for shipping?).

EDIT: response to asking for clarification: say I have a model.pth - (1) do I need to be careful with it, like I would with a .pkl, given a .pth is meant to contain only model weights? Or can I just go ahead and throw it into torch.load(model.pth)? (2) If I can't, what can I do before torch.load() to provide some peace-of-mind?

EDIT 2: An answer should show some focus on ML pretrained models in particular. An example: see the model https://download.pytorch.org/models/resnet34-333f7ec4.pth (warning: 80 MB download from TorchHub - feel free to use another Resnet .pth model, but any cleaning does need to be reasonably fast). Currently I would download this and then load it in PyTorch using load_state_dict (explained in detail [here][2] for example). If I didn't know this was a safe model, how could I try to sanitise it first before loading it in load_state_dict?



  [1]: https://stackoverflow.com/questions/25353753/python-can-i-safely-unpickle-untrusted-data
  [2]: https://www.programmersought.com/article/95324315915/
  • 1
    I think your questions is legitimate. And silent downvote are frustrating in addition of being not productive. You need to understand what is the pickle mechanism. Your question may translate as is it possible to safely clean a pickled object by loading and dumping it again into a container? – jlandercy May 11 '21 at 19:32
  • @jlandercy thank you. Well `.pth` files as I understand are just the *weights* right? So I'm asking first whether they are security risks at all (like `.pkl` files are according to the warnings). And if so, then what can be done about it - the container was just a suggestion... – Vainmonde De Courtenay May 11 '21 at 19:35
  • 1
    This is why your question as stated does not fit SO standards. This stack is about to solve actual coding problem not reviewing potential problem on generic workflow. This is probably why it attracts downvotes. Either ask on other IT stack or try to be more specific and code oriented see [mcve]. Very interesting question anyway. – jlandercy May 11 '21 at 19:58
  • 4
    I disagree with this closure. This seems very similar to the linked question, but with clear differences, and seems suitable for SO. To answer the first part of your question, the PyTorch documentation says that it is not safe, since `torch.load` uses pickle internally: https://pytorch.org/docs/stable/generated/torch.load.html – GoodDeeds May 11 '21 at 21:13
  • The PyTorch recommended way to load models from GitHub is via [TorchHub](https://stackoverflow.com/questions/42703500/best-way-to-save-a-trained-model-in-pytorch/66921920#66921920). – iacob May 11 '21 at 21:56
  • 1
    @jlandercy I think the problem is specific: say I have a `model.pth` - (1) do I need to be careful with it, like I would with a `.pkl`, given a `.pth` is meant to contain only model weights? Or can I just go ahead and throw it into `torch.load(model.pth)`? (2) If I can't, what can I do before `torch.load()` to give me some peace-of-mind? ... I don't really see how I can be more specific! – Vainmonde De Courtenay May 12 '21 at 02:41
  • 1
    @iacob Ok but let's say whoever made the repo didn't bother making provisions for future import from TorchHub. Many potentially useful pretrained models on Github are just .pth or .t7 files lying around, they haven't been signed or anything. – Vainmonde De Courtenay May 12 '21 at 02:46
  • I did flag it for reopen and voted for. I do agree your question is partially specific. But this part is not and may be opinion based or looks like to a product or module reference request: could we do something like make a minimal Docker container with PyTorch. What you added in your comment is worth to be in the post instead. Your question is aimed to help the whole community. So we all do our best to improve it and get it answered. Cheers – jlandercy May 12 '21 at 05:15

1 Answers1

3

As pointed out by @MobeusZoom, this is answer is about Pickle and not PyTorch format. Anyway as PyTorch load mechanism relies on Pickle behind the scene observations drawn in this answer still apply.

TL;DR;

Don't try to sanitize pickle. Trust or reject.

Quoted from Marco Slaviero in his presentation Sour Pickle at the Black Hat USA 2011.

Real solution is:

  • Don't setup exchange with unequally trusted parties;
  • Setup a secure transport layer for exchange;
  • Sign exchanged files;

Also be aware that there are new kind of AI based attacks, this even if the pickle is shellcode free, you still may have other issues to address when loading pre-trained networks from untrusted sources.

Important notes

From the presentation linked above we can draw several important notes:

  • Pickle use a Virtual Machine to reconstruct live data (PVM happens alongside the python process), this virtual machine is not Turing complete but has: an instruction set (opcodes), a stack for the execution and a memo to host object data. This is enough for attacker to create exploits.
  • Pickle mechanism is backward compatible, it means latest Python can unpickle the very first version of its protocol.
  • Pickle can (re)construct any object as long as PVM does not crash, there is no consistency check in this mechanism to enforce object integrity.
  • In broad outline, Pickle allows attacker to execute shellcode in any language (including python) and those code can even persist after the victim program exits.
  • Attacker will generally forge their own pickles because it offers more flexibility than naively using pickle mechanism. Off course they can use pickle as an helper to write opcode sequences. Attacker can craft the malicious pickle payload in two significant ways:
    • to prepend the shellcode to be executed first and leave the PVM stack clean. Then you probably get a normal object after unpickling;
    • to insert the shellcode into the payload, so it gets executed while unpickling and may interact with the memo. Then unpickled object may have extra capabilities.
  • Attackers are aware of "safe unpickler" and know how to circumvent them.

MCVE

Find below a very naive MCVE to evaluate your suggestion to encapsulate cleaning of suspect pickled files in Docker container. We will use it to assess main associated risks. Be aware, real exploit will be more advanced and complexer.

Consider the two classes below, Normal is what you expect to unpickle:

# normal.py
class Normal:

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

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

And Exploit is the attacker vessel for its shellcode:

# exploit.py
class Exploit(object):

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

Then, the attacker can use pickle as an helper to produce intermediate payloads in order to forge the final exploit payload:

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.'

At this point the attacker can craft a specific payload to both inject its shellcode and returns the data.

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.')

Now, when victim will deserialize the malicious pickle file, the exploit is executed and a valid object is returned as expected:

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

Execution returns:

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

Off course, shellcode is not intended to be so obvious, you may not notice it has been executed.

Containerized cleaner

Now, lets try to clean this pickle as you suggested. We will encapsulate the following cleaning code:

# 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)

Into a Docker image to try to contain its execution. As a baseline, we could do something like this:

FROM python:3.9

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

USER 1001:1001

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

Then we build the image and execute it:

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

Also ensure the mounted folder containing the exploit has restrictive ad hoc permissions.

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

Now the cleaned.pickle is shellcode free. Off course you need to carefully check this assumption before releasing the cleaned pickle.

Observations

As you can see, Docker image does not prevent the exploit to be executed when unpickling but it may help to contain the exploit in some extent.

Points of attention are (not exhaustive):

  • Having a recent pickle file with the original protocol is a hint but not an evidence of something suspicious.
  • Be aware even if containerized, you still are running attacker code on your host;
  • Additionally, attacker may have designed its exploit to break a Docker container, use unprivileged user to reduce the risk;
  • Don't bind any network to this container as attacker can start a terminal and expose it over a network interface (and potentially to the web);
  • Depending on how the attacker has designed its exploit data may not be available at all. For the instance, if __reduce__ method actually returns the exploit instead of a recipe to recreate the desired instance. After all the main purpose of this is to make you unpickling it nothing more;
  • If you intend to dump raw data after loading the suspicious pickle archive you need a strict procedure to detach data from the exploit;
  • The cleaning step can be a limitation. It relies on your ability to recreate the intended object from the malicious payload. It will depends on what is really reconstructed from the pickle file and how the desired object constructor needs to be parametrized;
  • Finally, if you are confident in your cleaning procedure, you can mount a volume to access the result after the container exits.
jlandercy
  • 7,183
  • 1
  • 39
  • 57
  • 1
    Upvoted for example, but user asks about pth files. inject.pickle is not a pth file. They also run torch.load not pickle.load (which may or may not make a difference) – Mobeus Zoom May 12 '21 at 12:58
  • @MobeusZoom, you are totally right, this mechanism will certainly be complexer than the mcve. But I think the observations will remain True as pickle is invoked behind the scene. I'll be happy to go deeper in this analysis because I think the question is very interesting. Anyway I lack time and experience on pytorch framework for the moment. – jlandercy May 12 '21 at 13:10
  • @jlandercy thank you, this is a really great start! can you please elaborate on "procedure to detach data from the exploit" - what should this involve? – Vainmonde De Courtenay May 12 '21 at 16:58
  • @VainmondeDeCourtenay, I have refactored the MCVE to be more realistic and added extra information of interest. Does it address your questions? Cheers – jlandercy May 13 '21 at 17:20
  • @jlandercy This is a very useful post from the security/unpickling perspective. It should be broadly useful to anyone wanting to handle pickle files. However, my question is specifically about pretrained ML models. It would be good if you could give some information specific to them, for example, how would you handle this `.pth` file: https://download.pytorch.org/models/resnet34-333f7ec4.pth. It's from TorchHub in this case, the famous Resnet. But pretend you didn't know that... – Vainmonde De Courtenay May 13 '21 at 18:26
  • @VainmondeDeCourtenay, if you mind improve your post with this link and a snippet of code showing how you are opening this file usually, aka a [mcve] I'll have a look on it this weekend. – jlandercy May 13 '21 at 19:02