diff --git a/README.md b/README.md index 4848015..0610810 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,51 @@ # patchwork -Multi-layer polymorphic Python source obfuscator. +Python source obfuscator. Give it a `.py` file, get back a single self-contained `.py` that does the same thing but is hostile to read or debug. -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. +Each build is unique by default. Pass `--seed N` if you want a reproducible one. -## Features +## What it actually does -- **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`/`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 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.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 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. +Source-level rewrites first: + +- Identifiers get renamed to confusable junk like `_OoIl01l1OOIlO0`. Variables, function names, classes, import aliases, exception bindings. +- String and bytes literals get XOR'd with random per-literal keys and replaced with calls to a tiny decrypt helper. +- Integer literals get rewritten as random equivalent expressions: XOR cancellations, sum displacements, shift round-trips, `int.from_bytes` decodes, affine identities. +- Bitwise ops get pushed through MBA identities (`a^b` becomes `(a|b) - (a&b)` and similar). +- `if` and `while` tests get wrapped with always-true tautologies anchored on a runtime seed (`s*(s+1) % 2 == 0` and friends). +- Random dead-branch blocks get inserted with realistic-looking code, gated by an always-false runtime predicate so they never actually run. + +Then it compiles to bytecode and gets weirder: + +- Each user function and method's compiled code object is pulled out, encrypted into its own blob, and replaced in the marshaled module with a kind-matching stub (function / generator / coroutine / async-generator). On first call, the stub finds itself via frame inspection, decrypts its blob, swaps its own `__code__` in place, and re-invokes. So a static dump of the marshaled module just shows function shells. +- The whole module then goes through `marshal` -> `zlib` -> several rounds of cipher: XOR-CTR with a SHA-256 keystream, byte permutation, bit rotation. Order and keys are random per build. + +Then the loader: + +- Two stages. Stage 1 is the file you ship, a small mangled script that decrypts and execs Stage 2. Stage 2 is compiled, marshaled, multi-cipher encrypted, and embedded as bytes inside Stage 1. It holds the lazy blob table, the resolver, and the user payload. +- Both stages run anti-debug: `sys.gettrace`, `sys.getprofile`, `sys.modules` checks for `pdb`/`bdb`/`pydevd`/`debugpy`/etc, `PYTHONBREAKPOINT` env check, an audit hook that blocks subsequent imports of debugger modules and any call to `sys.settrace`/`sys.setprofile`, and a frame-stack walk looking for debugger frames. +- Large bytes literals get split into 4-9 random chunks, shuffled, and reassembled at runtime, so the payload doesn't show up as one continuous blob in the source. ## Requirements -- Python 3.9+ -- Standard library only — **no third-party dependencies** +Python 3.9 or newer. Stdlib only, no third-party packages. -The obfuscated output must be run on the same Python `major.minor` that built -it. `marshal` format is version-bound. +The output has to run on the same Python `major.minor` you built it on. `marshal` format is version-bound. ## Install ```sh -git clone patchwork +git clone https://github.com/bikini/patchwork.git cd patchwork ``` -That's it — no install needed. - -Optionally install editable to get the `patchwork` console script: +If you want the `patchwork` console script on your `PATH`: ```sh pip install -e . ``` -## Quick start - -CLI: +## Use it ```sh python -m patchwork myscript.py @@ -60,7 +58,7 @@ Custom output, reproducible build, more cipher layers: python -m patchwork myscript.py -o protected.py --seed 12345 --layers 5 ``` -Python API: +From Python: ```python from patchwork import Obfuscator, obfuscate_file @@ -68,28 +66,28 @@ 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()) +out = obf.obfuscate(open('myscript.py').read()) ``` -## CLI reference +## Flags ``` python -m patchwork INPUT [-o OUTPUT] [options] -o, --output PATH output path (default: _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) + --seed INT RNG seed for reproducible builds + --layers INT cipher layers around the user payload (default 3) + --stage2-layers INT cipher layers around 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 + --no-rename turn off identifier renaming + --no-encrypt-strings turn off string/bytes literal encryption + --no-obfuscate-numbers turn off integer literal obfuscation + --no-opaque turn off opaque predicate injection + --no-mba turn off Mixed Boolean-Arithmetic + --no-junk turn off junk dead-branch injection + --no-lazy turn off lazy per-function encryption + --no-anti-debug turn off runtime anti-debug probes + -q, --quiet quiet mode ``` ## Python API @@ -98,7 +96,7 @@ python -m patchwork INPUT [-o OUTPUT] [options] from patchwork import Obfuscator obf = Obfuscator( - seed=42, # int or None for fresh randomness + seed=42, rename=True, encrypt_strings=True, obfuscate_numbers=True, @@ -107,82 +105,29 @@ obf = Obfuscator( 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 + layers=3, + stage2_layers=3, + keep={'public_api_name'}, ) output_source = obf.obfuscate(input_source) ``` -`obfuscate(src, **kwargs)` is a shortcut for `Obfuscator(**kwargs).obfuscate(src)`. +`obfuscate(src, **kwargs)` is the one-shot version. `obfuscate_file(in_path, out_path=None, **kwargs)` reads, obfuscates, writes, returns the output path. -## How it works +## Stuff that'll trip you up -``` -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 +**Python version matters.** Marshaled bytecode is tied to whatever Python major.minor built it. Build on 3.11, run on 3.11. Mismatch and it won't load. -output.py -``` +**One file at a time.** This obfuscates a single module. For a package, run patchwork on each `.py`. Names that cross module boundaries (one obfuscated file importing another) keep their original spelling because we can't see across files. -At runtime: +**Attribute access is left alone.** `obj.x` doesn't get rewritten. The `x` could be addressing a stdlib method, a third-party API, anything. Class methods and class-body attributes are auto-skipped from renaming for the same reason. If you have other names that get accessed externally, throw them in `--keep`. -``` -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 -``` +**Generators, coroutines, async generators, decorators.** All work. Stubs are kind-matched so you don't get flag-mismatch warnings. Decorators that call user functions at `def` time work too, because lazy resolution kicks in on first call. -## Caveats +**`sys.argv[0]` will point at the obfuscated file**, not the original. -- **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. +**This is not a cryptographic guarantee.** Anything that runs on a target machine can eventually be reverse-engineered. A motivated person with a debugger can patch out the anti-debug checks, dump the unmarshaled code objects, and decompile back to source. What this buys you is depth: every layer has to be unwrapped before structure appears, and what you'd recover is heavily mangled. It's a speed bump, not a vault. ## Examples @@ -194,9 +139,7 @@ python examples/hello_obf.py # 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. +`examples/stress.py` is the torture test - decorators, generators, classes with `super()`, `match` statements, walrus, exceptions, `*args`/`**kwargs`, globals. If something doesn't work, it'll usually break here first. ## Tests @@ -204,10 +147,7 @@ and globals/nonlocals — useful as a torture test. 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). +Runs every example through the obfuscator at multiple seeds, executes original and obfuscated versions, and checks stdout matches byte-for-byte. Also confirms different seeds produce different output and the same seed produces stable output. ## Layout @@ -217,21 +157,21 @@ patchwork/ ├── requirements.txt ├── README.md ├── .gitignore -├── examples/ smoke-test inputs +├── examples/ │ ├── 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 +│ ├── __main__.py +│ ├── cli.py +│ ├── core.py +│ ├── crypto.py +│ ├── packer.py +│ ├── lazy.py +│ ├── loader.py +│ ├── util.py │ └── transforms/ │ ├── identifiers.py │ ├── strings.py @@ -243,6 +183,5 @@ patchwork/ └── test_obfuscator.py ``` -## License +MIT. -MIT