9.7 KiB
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 predicates —
if/whiletests 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
codeobject 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 packing —
marshal→zlib→ 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-debug —
sys.gettrace,sys.getprofile,sys.modulesdebugger probe,PYTHONBREAKPOINTenv 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 4–9 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
.pyseparately. Names that cross module boundaries (imports of one obfuscated module from another) keep their original spelling. - External attribute API:
obj.xattribute access is never rewritten —xmight 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
@decoratoratdeftime. 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