Skip to content

xelroth/PyDeobfuscate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 

Repository files navigation

🔓 PyDeobfuscate

Python PyCryptodome Status License

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.

Table of Contents

Overview

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.

How It Works

Layer 1 — AES-256-GCM + PBKDF2

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:

  1. Find the line containing the pyobfuscate( call.

  2. Regex out the pyc and pye triple-quoted segments and the quoted l/I password.

  3. Concatenate pyc + pye back into one base85 string and decode it to raw bytes.

  4. Slice those bytes by fixed offsets into their four parts:

    raw = salt (16B) | nonce (16B) | tag (16B) | ciphertext (remainder)
               ↑0x10       ↑0x20        ↑0x30
    
  5. Re-derive the same 256-bit key: PBKDF2(password, salt, dkLen=32, count=1_000_000).

  6. 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.

  7. Decode the result as UTF-8 → the original source.

Layer 2 — zlib + Runtime AST Reconstruction + Base85

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:

  1. Regex out the hex string inside bytes.fromhex(...) and decompress it with zlib.
  2. Drop the obfuscator's trailing junk statement and rejoin the rest with ;.
  3. Find the placeholder variable (<name> = None) the bootstrap will overwrite.
  4. Raise the recursion limit (AST-building code nests deeply) and exec() the bootstrap in an isolated namespace, then eval() the placeholder name to retrieve the constructed AST object.
  5. 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.
  6. Regex out the base85 payload and decode it → the original source.

Features

  • 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, -v for debug detail) — no interactive prompts
  • Single dependency, single file, no packaging required

Requirements

Installation

pip install pycryptodome
git clone https://github.com/<your-username>/PyDeobfuscate.git
cd PyDeobfuscate

That's it — PyDeobfuscate.py is self-contained; there's no build step or package to install.

Usage

Single file:

python3 PyDeobfuscate.py sample.py

Writes sample_Deobfuscated.py next to the source.

Batch:

python3 PyDeobfuscate.py *.py -o decoded/ -v

Custom suffix:

python3 PyDeobfuscate.py a.py b.py -s _plain

CLI reference

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

Exit codes

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)

Architecture

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 to Deobfuscator, one at a time, with per-file error isolation.

Security Notice

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.

Limitations

  • 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.

License

Licensed under the MIT License — see the LICENSE file for the full text.

Author

K00HYARgithub.com/xelroth

About

A Deobfuscator for pyobfuscate.com

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages