Python Binary & Bytecode Decompilation
Reverse engineer standalone PyInstaller executables and decompile compiled Python (.pyc) bytecode into readable Python source code.
// prerequisite reading
Overview
Python challenges in CTFs frequently package Python scripts into standalone Windows .exe or Linux ELF binaries using compilers like PyInstaller, cx_Freeze, or Nuitka.
Reversing these targets follows a two-phase process:
- Unpacking: Extracting raw
.pyc(compiled Python bytecode) files from the executable archive. - Decompilation: Converting
.pycbytecode back into human-readable.pyPython source code.
Phase 1: Unpacking PyInstaller Executables
PyInstaller bundles a Python interpreter, dynamic libraries, and compiled .pyc files into a single binary.
Using pyinstxtractor
# Run pyinstxtractor against Windows EXE or Linux ELF
python3 pyinstxtractor.py challenge.exe
This creates an extracted directory named challenge.exe_extracted/ containing:
struct.pyc— Python header metadata containing magic bytes.main.pycorchallenge.pyc— The entry-point script bytecode file.- Various
.pycmodule dependencies.
Phase 2: Fixing .pyc Magic Header Bytes
Older versions of pyinstxtractor might extract .pyc files lacking the standard Python Magic Bytes header required by decompilers.
Python Version Magic Bytes Reference
| Python Version | Magic Bytes (Hex) |
|---|---|
| Python 3.8 | 55 0d 0d 0a |
| Python 3.9 | 61 0d 0d 0a |
| Python 3.10 | 6f 0d 0d 0a |
| Python 3.11 | a7 0d 0d 0a |
| Python 3.12 | cb 0d 0d 0a |
If missing, prepending the 16-byte header from struct.pyc to entry_point.pyc restores full decompilation compatibility.
Phase 3: Decompiling .pyc to .py
1. Using Decompyle++ (pycdc) — Best for Python 3.9+
pycdc is a C++ decompilation engine that supports modern Python opcode formats up to Python 3.12:
# Decompile .pyc to stdout / .py file
pycdc entry_point.pyc > decompiled_source.py
# Disassemble bytecode if decompilation encounters syntax errors
pycdas entry_point.pyc
2. Using uncompyle6 — Best for Python 2.7 to 3.8
uncompyle6 -o decompiled_source.py entry_point.pyc
Direct Bytecode Disassembly in Python
If decompilers crash on obfuscated code, inspect opcodes directly in Python using the dis module:
import dis
import marshal
with open("entry_point.pyc", "rb") as f:
f.seek(16) # Skip 16-byte pyc header
code_obj = marshal.load(f)
# Disassemble code object into opcode instructions
dis.dis(code_obj)