Files
bikini-patchwork/README.md
T
2026-04-29 00:17:18 -05:00

9.7 KiB
Raw Blame History

patchwork

Multi-layer polymorphic Python source obfuscator.

Takes a .py file and emits a single self-contained .py file that, when run, executes the original program. Every build is unique — different mangled identifiers, different encryption keys, different decryption-stub layout, and different junk filler.

Features

  • Identifier renaming — every variable, function, class, import alias, and exception binding gets a confusable identifier (_OoIl01…).
  • String / bytes encryption — every literal is XOR-encrypted with a per-literal random key, decrypted on demand.
  • Integer obfuscation — every int literal becomes one of: XOR cancellation, sum displacement, bit-shift round-trip, int.from_bytes, affine identity, or split-sum.
  • Mixed Boolean-Arithmetic (MBA) — bitwise operators rewritten with algebraic identities (a^b → (a|b) - (a&b), etc.).
  • Opaque predicatesif/while tests wrapped with runtime-true tautologies (s*(s+1) % 2 == 0, Fermat's little theorem) anchored on a runtime seed.
  • Junk dead branches — realistic-looking unreachable blocks gated by always-false runtime predicates.
  • Lazy per-function code encryption — each user function/method's compiled code object is encrypted into a separate blob; the marshaled module only contains stubs. Real code is decrypted on first call and the function's __code__ is swapped in place.
  • Multi-cipher payload packingmarshalzlib → N (default 3) layers chosen at random from {XOR-CTR with SHA256 keystream, byte permutation, bit rotation}, each with its own random key.
  • Multi-stage loader — outer Stage 1 decrypts and execs Stage 2 (compiled, marshaled, multi-cipher encrypted). Stage 2 holds the lazy blob table, the resolver, and the user payload.
  • Anti-debugsys.gettrace, sys.getprofile, sys.modules debugger probe, PYTHONBREAKPOINT env check, audit hook blocking debugger module imports / sys.settrace / sys.setprofile, full frame-stack walk for debugger frames. Probes are scattered through both stages.
  • Chunked bytes literals — payloads >64 bytes are split into 49 shuffled chunks, reassembled at runtime via an inverse-permutation tuple.
  • Polymorphic — every emitted name is mangled fresh; ordering of independent statements is randomized; junk filler is interspersed.

Requirements

  • Python 3.9+
  • Standard library only — no third-party dependencies

The obfuscated output must be run on the same Python major.minor that built it. marshal format is version-bound.

Install

git clone <this-repo> patchwork
cd patchwork

That's it — no install needed.

Optionally install editable to get the patchwork console script:

pip install -e .

Quick start

CLI:

python -m patchwork myscript.py
python myscript_obf.py

Custom output, reproducible build, more cipher layers:

python -m patchwork myscript.py -o protected.py --seed 12345 --layers 5

Python API:

from patchwork import Obfuscator, obfuscate_file

obfuscate_file('myscript.py', 'protected.py', seed=12345)

obf = Obfuscator(seed=12345, layers=4, stage2_layers=4)
protected_source = obf.obfuscate(open('myscript.py').read())

CLI reference

python -m patchwork INPUT [-o OUTPUT] [options]

  -o, --output PATH         output path (default: <input>_obf.py)
  --seed INT                RNG seed for reproducible output
  --layers INT              cipher layers wrapping the user payload (default 3)
  --stage2-layers INT       cipher layers wrapping stage 2 (default 3)
  --keep NAME               identifier to leave un-renamed (repeatable)
  --no-rename               disable identifier renaming
  --no-encrypt-strings      disable string/bytes literal encryption
  --no-obfuscate-numbers    disable integer literal obfuscation
  --no-opaque               disable opaque predicate injection
  --no-mba                  disable Mixed Boolean-Arithmetic
  --no-junk                 disable junk dead-branch injection
  --no-lazy                 disable lazy per-function encryption
  --no-anti-debug           disable runtime anti-debug probes
  -q, --quiet               suppress informational output

Python API

from patchwork import Obfuscator

obf = Obfuscator(
    seed=42,                   # int or None for fresh randomness
    rename=True,
    encrypt_strings=True,
    obfuscate_numbers=True,
    opaque_predicates=True,
    mba=True,
    junk_branches=True,
    lazy_funcs=True,
    anti_debug=True,
    layers=3,                  # cipher layers around user payload
    stage2_layers=3,           # cipher layers around stage 2
    keep={'public_api_name'},  # names you don't want renamed
)
output_source = obf.obfuscate(input_source)

obfuscate(src, **kwargs) is a shortcut for Obfuscator(**kwargs).obfuscate(src). obfuscate_file(in_path, out_path=None, **kwargs) reads, obfuscates, writes, returns the output path.

How it works

input.py
  │
  ├─ AST passes:
  │    opaque-predicate injection
  │    junk dead-branch injection
  │    MBA on bitwise ops
  │    integer-literal obfuscation
  │    string/bytes encryption (helpers prepended)
  │    identifier renaming
  │
  ├─ compile to module code object
  │
  ├─ lazy function encryption:
  │    walks co_consts, replaces each user-defined function/method
  │    code with a kind-matching stub (function / generator / coroutine
  │    / async-generator); the original code goes into a per-function
  │    encrypted blob in a side table
  │
  ├─ marshal → zlib → N cipher layers ─────► user payload
  │
  ├─ build stage 2 source:
  │    cipher helpers, anti-debug, lazy blobs table, resolver
  │    function, user-payload decryption + exec
  │
  ├─ compile stage 2 → marshal → N cipher layers ─────► stage 2 payload
  │
  └─ emit stage 1 source (the file you ship):
       polymorphic mangled imports, anti-debug, audit hook,
       frame-stack walk, cipher helpers, stage 2 payload (chunked),
       decryption layers, exec

output.py

At runtime:

python output.py
  │
  ├─ stage 1: anti-debug, decrypt stage 2, exec it
  │
  ├─ stage 2: anti-debug, decrypt user payload, build user namespace
  │           with __pw_resolve_lazy__ injected, exec user code
  │
  ├─ user code runs with stub function bodies. On first call, each
  │  stub invokes __pw_resolve_lazy__, which:
  │     1. finds the calling function via sys._getframe(1).f_code
  │     2. decrypts that function's blob
  │     3. swaps the function's __code__ in place (and caches it)
  │     4. re-invokes for native semantics
  │
  └─ subsequent calls use the now-real code directly

Caveats

  • Same Python version: marshaled bytecode is tied to the major.minor that built it. Obfuscate on 3.11 → run on 3.11; obfuscate on 3.12 → run on 3.12.
  • Single-module scope: patchwork obfuscates one file at a time. For a package, run it on each .py separately. Names that cross module boundaries (imports of one obfuscated module from another) keep their original spelling.
  • External attribute API: obj.x attribute access is never rewritten — x might address something external. Class-body method names and class attributes are auto-kept. If you expose other names externally, use --keep.
  • Generators / coroutines / async generators: fully supported with kind-matching stubs.
  • Decorators: supported. Lazy resolution kicks in on first call, including the call made by @decorator at def time.
  • sys.argv[0]: will be the obfuscated file path, not the original source path.
  • Not a cryptographic guarantee: a determined reverse-engineer with debugger access can patch out anti-debug, dump unmarshaled code objects, and decompile. The defense is depth: every layer must be unwrapped before useful structure appears, and the decompiled output is heavily mangled.

Examples

python -m patchwork examples/hello.py
python examples/hello_obf.py
# Hello, World! (from patchwork)
# number: 42
# computed: 285

examples/stress.py exercises decorators, generators, classes with super(), match statements, walrus operator, exception handling, *args/**kwargs, and globals/nonlocals — useful as a torture test.

Tests

python tests/test_obfuscator.py

Runs every example through the obfuscator at multiple seeds, executes original and obfuscated versions, and verifies stdout is byte-identical. Also checks polymorphism (different seeds → different output) and reproducibility (same seed → stable output).

Layout

patchwork/
├── pyproject.toml
├── requirements.txt
├── README.md
├── .gitignore
├── examples/                  smoke-test inputs
│   ├── hello.py
│   ├── fizzbuzz.py
│   ├── classes.py
│   └── stress.py
├── patchwork/
│   ├── __init__.py
│   ├── __main__.py            python -m patchwork
│   ├── cli.py                 argparse front-end
│   ├── core.py                pipeline orchestrator
│   ├── crypto.py              XOR-CTR / permutation / rotation primitives
│   ├── packer.py              multi-cipher payload packing
│   ├── lazy.py                per-function code-object encryption
│   ├── loader.py              polymorphic 2-stage loader generator
│   ├── util.py                confusable-name generator + RNG
│   └── transforms/
│       ├── identifiers.py
│       ├── strings.py
│       ├── numbers.py
│       ├── opaque.py
│       ├── mba.py
│       └── junk.py
└── tests/
    └── test_obfuscator.py

License

MIT