Hub 文件
Pickle 掃描
並獲得增強的文件體驗
開始使用
Pickle 掃描
Pickle 是機器學習中廣泛使用的序列化格式。最值得注意的是,它是 PyTorch 模型權重的預設格式。
當你載入一個 pickle 檔案時,可能會遭受危險的任意程式碼執行攻擊。我們建議從你信任的使用者和組織載入模型,依賴經過簽名的提交,和/或使用 from_tf=True
自動轉換機制從 TF 或 Jax 格式載入模型。我們還透過直接在 Hub 上顯示/“審查”任何 pickle 檔案中的匯入列表來緩解此問題。最後,我們正在試驗一種名為 safetensors
的新的、簡單的權重序列化格式。
什麼是 pickle?
來自官方文件:
pickle
模組實現了用於序列化和反序列化 Python 物件結構的二進位制協議。
這意味著 pickle 是一種序列化協議,用於在各方之間高效地共享資料。
我們稱 pickling 過程中生成的二進位制檔案為 pickle。
pickle 的核心基本上是一堆指令或操作碼。正如你可能猜到的,它不是人類可讀的。操作碼在 pickling 時生成,並在 unpickling 時按順序讀取。根據操作碼,執行給定的操作。
這裡有一個小例子:
import pickle
import pickletools
var = "data I want to share with a friend"
# store the pickle data in a file named 'payload.pkl'
with open('payload.pkl', 'wb') as f:
pickle.dump(var, f)
# disassemble the pickle
# and print the instructions to the command line
with open('payload.pkl', 'rb') as f:
pickletools.dis(f)
當你執行這個指令碼時,它會建立一個 pickle 檔案並在你的終端列印以下指令:
0: \x80 PROTO 4
2: \x95 FRAME 48
11: \x8c SHORT_BINUNICODE 'data I want to share with a friend'
57: \x94 MEMOIZE (as 0)
58: . STOP
highest protocol among opcodes = 4
現在不用太擔心這些指令,只需知道 pickletools 模組對於分析 pickle 非常有用。它允許你讀取檔案中的指令,而無需執行任何程式碼。
Pickle 不僅僅是一個序列化協議,它透過賦予使用者在反序列化時執行 python 程式碼的能力,提供了更大的靈活性。聽起來不太妙,對吧?
為什麼它很危險?
正如我們上面所述,反序列化 pickle 意味著可以執行程式碼。但這有一些限制:你只能引用頂層模組中的函式和類;你不能將它們嵌入到 pickle 檔案本身中。
回到畫板上
import pickle
import pickletools
class Data:
def __init__(self, important_stuff: str):
self.important_stuff = important_stuff
d = Data("42")
with open('payload.pkl', 'wb') as f:
pickle.dump(d, f)
當我們執行這個指令碼時,我們再次得到了 `payload.pkl`。當我們檢查檔案內容時:
# cat payload.pkl
__main__Data)}important_stuff42sb.%
# hexyl payload.pkl
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 80 04 95 33 00 00 00 00 ┊ 00 00 00 8c 08 5f 5f 6d │ו×30000┊000ו__m│
│00000010│ 61 69 6e 5f 5f 94 8c 04 ┊ 44 61 74 61 94 93 94 29 │ain__×ו┊Data×××)│
│00000020│ 81 94 7d 94 8c 0f 69 6d ┊ 70 6f 72 74 61 6e 74 5f │××}×וim┊portant_│
│00000030│ 73 74 75 66 66 94 8c 02 ┊ 34 32 94 73 62 2e │stuff×ו┊42×sb. │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
我們可以看到裡面沒有太多東西,只有幾個操作碼和相關資料。你可能會想,那麼 pickle 有什麼問題呢?
讓我們嘗試些別的
from fickling.pickle import Pickled
import pickle
# Create a malicious pickle
data = "my friend needs to know this"
pickle_bin = pickle.dumps(data)
p = Pickled.load(pickle_bin)
p.insert_python_exec('print("you\'ve been pwned !")')
with open('payload.pkl', 'wb') as f:
p.dump(f)
# innocently unpickle and get your friend's data
with open('payload.pkl', 'rb') as f:
data = pickle.load(f)
print(data)
這裡為了簡單起見,我們使用了 fickling 庫。它允許我們新增 pickle 指令,透過 `exec` 函式執行包含在字串中的程式碼。這就是你繞過 pickle 中不能定義函式或類的限制的方法:對儲存為字串的 python 程式碼執行 exec。
當你執行這個指令碼時,它會建立一個 `payload.pkl` 並列印以下內容:
you've been pwned !
my friend needs to know this
如果我們檢查 pickle 檔案的內容,我們會得到:
# cat payload.pkl
c__builtin__
exec
(Vprint("you've been pwned !")
tR my friend needs to know this.%
# hexyl payload.pkl
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 63 5f 5f 62 75 69 6c 74 ┊ 69 6e 5f 5f 0a 65 78 65 │c__built┊in___exe│
│00000010│ 63 0a 28 56 70 72 69 6e ┊ 74 28 22 79 6f 75 27 76 │c_(Vprin┊t("you'v│
│00000020│ 65 20 62 65 65 6e 20 70 ┊ 77 6e 65 64 20 21 22 29 │e been p┊wned !")│
│00000030│ 0a 74 52 80 04 95 20 00 ┊ 00 00 00 00 00 00 8c 1c │_tR×•× 0┊000000ו│
│00000040│ 6d 79 20 66 72 69 65 6e ┊ 64 20 6e 65 65 64 73 20 │my frien┊d needs │
│00000050│ 74 6f 20 6b 6e 6f 77 20 ┊ 74 68 69 73 94 2e │to know ┊this×. │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
基本上,這就是 unpickle 時發生的事情:
# ...
opcodes_stack = [exec_func, "malicious argument", "REDUCE"]
opcode = stack.pop()
if opcode == "REDUCE":
arg = opcodes_stack.pop()
callable = opcodes_stack.pop()
opcodes_stack.append(callable(arg))
# ...
構成威脅的指令是 STACK_GLOBAL
、GLOBAL
和 REDUCE
。
REDUCE
告訴 unpickler 用提供的引數執行函式,而 *GLOBAL
指令告訴 unpickler 去 `import` 東西。
總而言之,pickle 是危險的,因為:
- 當匯入一個 python 模組時,可以執行任意程式碼
- 你可以匯入像 `eval` 或 `exec` 這樣的內建函式,它們可以用來執行任意程式碼
- 當例項化一個物件時,建構函式可能會被呼叫
這就是為什麼大多數使用 pickle 的文件中都說明,不要 unpickle 來自不受信任來源的資料。
緩解策略
不要使用 pickle
聽起來是個好建議,Luc,但 pickle 被大量使用,並且短期內不會消失:找到一個讓所有人都滿意的新格式併發起變革需要一些時間。
那麼我們現在能做些什麼呢?
從你信任的使用者和組織載入檔案
在 Hub 上,你可以使用 GPG 金鑰簽署你的提交。這並不能保證你的檔案是安全的,但它確實保證了檔案的來源。
如果你瞭解並信任使用者 A,並且 Hub 上包含該檔案的提交是由使用者 A 的 GPG 金鑰簽署的,那麼可以相當安全地假設你可以信任該檔案。
從 TF 或 Flax 載入模型權重
TensorFlow 和 Flax 的檢查點不受影響,並且可以在 PyTorch 架構中透過 `from_pretrained` 方法的 `from_tf` 和 `from_flax` 關鍵字引數載入,以規避此問題。
例如
from transformers import AutoModel
model = AutoModel.from_pretrained("google-bert/bert-base-cased", from_flax=True)
使用你自己的序列化格式
最後這個格式 `safetensors` 是我們目前正在開發和試驗的一種簡單序列化格式!如果可以的話,請幫忙或貢獻程式碼 🔥。
改進 torch.load/save
在 PyTorch 上有一個正在進行的公開討論,關於預設情況下以安全方式僅從 *.pt 檔案載入權重——請加入討論!
Hub 的安全掃描器
我們現在有什麼
我們建立了一個安全掃描器,可以掃描推送到 Hub 的每個檔案並執行安全檢查。在撰寫本文時,它執行兩種型別的掃描:
- ClamAV 掃描
- Pickle 匯入掃描
對於 ClamAV 掃描,檔案會透過開源防毒軟體 ClamAV 進行掃描。雖然這涵蓋了大量危險檔案,但它不包括 pickle 漏洞。
我們實現了一個 Pickle 匯入掃描,它會提取 pickle 檔案中引用的匯入列表。每次你上傳 `pytorch_model.bin` 或任何其他 pickle 檔案時,都會執行此掃描。
在 Hub 上,匯入列表將顯示在每個包含匯入的檔案旁邊。如果任何匯入看起來可疑,它將被高亮顯示。


我們透過 pickletools.genops
獲得這些資料,它允許我們讀取檔案內容而無需執行潛在的危險程式碼。
請注意,這使得我們能夠知道,當 unpickling 檔案時,它是否會對一個透過 `*GLOBAL` 匯入的潛在危險函式執行 `REDUCE` 操作。
免責宣告:這並非 100% 萬無一失。作為使用者,你有責任檢查某些內容是否安全。我們不會主動審計 python 包的安全性,我們的安全/不安全匯入列表是以盡力而為的方式維護的。如果你認為某些東西不安全,而我們將其標記為安全,請透過傳送電子郵件至 website at huggingface.co 與我們聯絡。
潛在解決方案
有人可能會想到建立一個自定義的 Unpickler,類似於這個。但正如我們在這個複雜的漏洞利用中看到的,這行不通。
幸運的是,`eval` 匯入總會留下痕跡,所以直接讀取操作碼應該能捕捉到惡意使用。
我目前提出的解決方案是建立一個類似於 `.gitignore` 的檔案,但用於匯入。
這個檔案將是一個匯入的白名單,如果 `pytorch_model.bin` 檔案中有未包含在白名單中的匯入,該檔案將被標記為危險。
可以想象使用一種類似正則表示式的格式,例如透過一行簡單的 `numpy.*` 來允許所有的 numpy 子模組。
進一步閱讀
pickle - Python object serialization - Python 3.10.6 documentation
Dangerous Pickles - Malicious Python Serialization
GitHub - trailofbits/fickling: A Python pickling decompiler and static analyzer
cpython/pickletools.py at 3.10 · python/cpython
cpython/pickle.py at 3.10 · python/cpython
CrypTen/serial.py at main · facebookresearch/CrypTen
CTFtime.org / Balsn CTF 2019 / pyshv1 / Writeup
Rehabilitating Python’s pickle module
< > 在 GitHub 上更新