A single-file command-line tool that reverses pyobfuscate.com obfuscation — both known output formats, auto-detected, in bulk if needed.
Give it an obfuscated .py file. It hands back the original source.
- Overview
- How It Works
- Features
- Requirements
- Installation
- Usage
- Architecture
- Security Notice
- Limitations
- License
- Author
pyobfuscate.com hides a script's real source behind one of two schemes: an AES-GCM ciphertext, or a self-assembling bootstrap that only reveals its payload once executed. Both are designed to defeat a quick Ctrl+F or static read-through.
PyDeobfuscate fingerprints which scheme a given file uses and runs the matching decoder — no manual identification needed, and no interactive prompts to get in the way of scripting it.
The scheme. AES-GCM is authenticated encryption: under the hood it runs AES in counter mode to produce a keystream that's XORed with the plaintext, while separately computing a GHASH authentication tag over the ciphertext. That tag is what makes GCM special — decryption and integrity-checking happen in one step, so tampered or corrupted ciphertext fails loudly instead of silently decrypting into garbage. The encryption key itself is never stored directly; it's derived on the fly with PBKDF2, which stretches a short password into a full 256-bit key by hashing it together with a random salt across 1,000,000 iterations — expensive enough that brute-forcing the password back from the ciphertext isn't practical.
pyobfuscate.com packages all of this into a single call that looks like:
pyobfuscate(**{'pyc': """<base85 half A>""", 'pye': """<base85 half B>"""}, "<password>")The password is disguised as a short run of l/I characters — visually indistinguishable at a glance, but still just a plain string once you know to look for it.
Reversing it. Deobfuscator.__DecodeAesLayer__ and __DecryptAesGCM__ do the following:
-
Find the line containing the
pyobfuscate(call. -
Regex out the
pycandpyetriple-quoted segments and the quotedl/Ipassword. -
Concatenate
pyc + pyeback into one base85 string and decode it to raw bytes. -
Slice those bytes by fixed offsets into their four parts:
raw = salt (16B) | nonce (16B) | tag (16B) | ciphertext (remainder) ↑0x10 ↑0x20 ↑0x30 -
Re-derive the same 256-bit key:
PBKDF2(password, salt, dkLen=32, count=1_000_000). -
Run
AES.new(key, MODE_GCM, nonce).decrypt_and_verify(ciphertext, tag)— one call that decrypts and checks the tag, raising immediately if the password's wrong or the file was tampered with. -
Decode the result as UTF-8 → the original source.
The scheme. This layer starts with a bytes.fromhex(...) blob that decompresses (via zlib) into a small bootstrap snippet. Here's the clever part: that bootstrap doesn't contain the final payload as a literal string anywhere — instead, it's a sequence of statements that, when executed, programmatically assembles a Python AST (Abstract Syntax Tree) object representing the expression <something>.b85decode('<payload>'.encode()). Because the payload only exists as a tree of AST nodes built at runtime, it's invisible to a static grep or a quick read of the decompressed text — you have to actually run the bootstrap to materialize it, then convert the resulting tree back into text to read it off.
Reversing it. Deobfuscator.__DecodeZlibLayer__ does the following:
- Regex out the hex string inside
bytes.fromhex(...)and decompress it withzlib. - Drop the obfuscator's trailing junk statement and rejoin the rest with
;. - Find the placeholder variable (
<name> = None) the bootstrap will overwrite. - Raise the recursion limit (AST-building code nests deeply) and
exec()the bootstrap in an isolated namespace, theneval()the placeholder name to retrieve the constructed AST object. - Call
ast.unparse()on it — this turns the in-memory tree back into source text, finally exposing the.b85decode('<payload>'.encode())call as a plain string. - Regex out the base85 payload and decode it → the original source.
- Detects and reverses both known pyobfuscate.com formats automatically
- Batch processing — point it at as many files as you want in one invocation
- Per-file isolation: one corrupted or unsupported file in a batch doesn't stop the rest
- Configurable output directory and filename suffix
- Clean, leveled logging (quiet by default,
-vfor debug detail) — no interactive prompts - Single dependency, single file, no packaging required
- Python 3.9+
- PyCryptodome
pip install pycryptodome
git clone https://github.com/<your-username>/PyDeobfuscate.git
cd PyDeobfuscateThat's it — PyDeobfuscate.py is self-contained; there's no build step or package to install.
Single file:
python3 PyDeobfuscate.py sample.pyWrites sample_Deobfuscated.py next to the source.
Batch:
python3 PyDeobfuscate.py *.py -o decoded/ -vCustom suffix:
python3 PyDeobfuscate.py a.py b.py -s _plain| Argument | Description | Default |
|---|---|---|
FILE (one or more, required) |
obfuscated source file(s) to process | — |
-o, --output-dir DIR |
write every output file into DIR instead of next to its source |
same directory as the source |
-s, --suffix SUFFIX |
text inserted before the extension of each output file | _Deobfuscated |
-v, --verbose |
debug-level logging with timestamps | off (info-level only) |
--version |
print the tool's version and exit | — |
-h, --help |
show usage and exit | — |
| Code | Meaning |
|---|---|
0 |
every file was deobfuscated successfully |
1 |
at least one file failed — check the log for which, and why |
2 |
bad command-line arguments (standard argparse behavior) |
Everything lives in one file, split by concern:
Deobfuscator— the actual reverse-engineering logic. Detects the obfuscation layer, decrypts or reconstructs it, writes the result. No CLI or logging opinions of its own; usable as a plain library class (Deobfuscator()(path)).Parser/Configure/Run/main— the CLI layer. Argument parsing, logging setup, and fanning a batch of paths out toDeobfuscator, one at a time, with per-file error isolation.
The zlib/base85 decoder reconstructs its payload by running exec()/eval() on code extracted from the target file itself. That's inherent to how pyobfuscate.com's bootstrap works, not something PyDeobfuscate adds — but it means this code path executes attacker-controlled logic from whatever file you point it at.
- Targets pyobfuscate.com's current output formats specifically; if the service changes its wrapper, the regex-based detection may need updating.
- Batches are processed one file at a time. Each file costs a full 1,000,000-iteration PBKDF2 pass, so very large batches will take a while.
Licensed under the MIT License — see the LICENSE file for the full text.
K00HYAR — github.com/xelroth