mirror of
https://github.com/bikini/patchwork
synced 2026-06-27 08:08:41 +00:00
Update README.md
This commit is contained in:
@@ -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 <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
|
||||
|
||||
Reference in New Issue
Block a user