Update README.md

This commit is contained in:
bikini
2026-04-29 00:23:08 -05:00
committed by GitHub
parent 42ccb9ed69
commit ef1cd60620
+66 -127
View File
@@ -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 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.
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 <this-repo> 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: <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)
--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