beginnercat/rev~2 min read

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:

  1. Unpacking: Extracting raw .pyc (compiled Python bytecode) files from the executable archive.
  2. Decompilation: Converting .pyc bytecode back into human-readable .py Python 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.pyc or challenge.pyc — The entry-point script bytecode file.
  • Various .pyc module 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)