Decompile compiled python binaries (exe, elf) - Retreive from .pyc

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

From Compiled Binary to .pyc

From an ELF compiled binary you can get the .pyc with:

pyi-archive_viewer <binary>
# The list of python modules will be given here:
[(0, 230, 311, 1, 'm', 'struct'),
 (230, 1061, 1792, 1, 'm', 'pyimod01_os_path'),
 (1291, 4071, 8907, 1, 'm', 'pyimod02_archive'),
 (5362, 5609, 13152, 1, 'm', 'pyimod03_importers'),
 (10971, 1473, 3468, 1, 'm', 'pyimod04_ctypes'),
 (12444, 816, 1372, 1, 's', 'pyiboot01_bootstrap'),
 (13260, 696, 1053, 1, 's', 'pyi_rth_pkgutil'),
 (13956, 1134, 2075, 1, 's', 'pyi_rth_multiprocessing'),
 (15090, 445, 672, 1, 's', 'pyi_rth_inspect'),
 (15535, 2514, 4421, 1, 's', 'binary_name'),
...

? X binary_name
to filename? /tmp/binary.pyc

In a python exe binary compiled you can get the .pyc by running:

python pyinstxtractor.py executable.exe

From .pyc to python code

For the .pyc data (“compiled” python) you should start trying to extract the original python code:

uncompyle6 binary.pyc  > decompiled.py

Be sure that the binary has the extension.pyc” (if not, uncompyle6 is not going to work)

While executing uncompyle6 you might find the following errors:

Error: Unknown magic number 227

/kali/.local/bin/uncompyle6 /tmp/binary.pyc
Unknown magic number 227 in /tmp/binary.pyc

To fix this you need to add the correct magic number at the beginning of the generated file.

Magic numbers vary with the python version, to get the magic number of python 3.8 you will need to open a python 3.8 terminal and execute:

>> import imp
>> imp.get_magic().hex()
'550d0d0a'

The magic number in this case for python3.8 is 0x550d0d0a, then, to fix this error you will need to add at the beginning of the .pyc file the following bytes: 0x0d550a0d000000000000000000000000

Once you have added that magic header, the error should be fixed.

This is how a correctly added .pyc python3.8 magic header will look like:

hexdump 'binary.pyc' | head
0000000 0d55 0a0d 0000 0000 0000 0000 0000 0000
0000010 00e3 0000 0000 0000 0000 0000 0000 0000
0000020 0700 0000 4000 0000 7300 0132 0000 0064
0000030 0164 006c 005a 0064 0164 016c 015a 0064

Error: Decompiling generic errors

Other errors like: class 'AssertionError'>; co_code should be one of the types (<class 'str'>, <class 'bytes'>, <class 'list'>, <class 'tuple'>); is type <class 'NoneType'> may appear.

This probably means that you haven’t added correctly the magic number or that you haven’t used the correct magic number, so make sure you use the correct one (or try a new one).

Check the previous error documentation.

Automatic Tool

The python-exe-unpacker tool serves as a combination of several community-available tools designed to assist researchers in unpacking and decompiling executables written in Python, specifically those created with py2exe and pyinstaller. It includes YARA rules to identify if an executable is Python-based and confirms the creation tool.

ImportError: File name: ‘unpacked/malware_3.exe/pycache/archive.cpython-35.pyc’ doesn’t exist

A common issue encountered involves an incomplete Python bytecode file resulting from the unpacking process with unpy2exe or pyinstxtractor, which then fails to be recognized by uncompyle6 due to a missing Python bytecode version number. To address this, a prepend option has been added, which appends the necessary Python bytecode version number, facilitating the decompiling process.

Example of the issue:

# Error when attempting to decompile without the prepend option
test@test: uncompyle6 unpacked/malware_3.exe/archive.py
Traceback (most recent call last):
...
ImportError: File name: 'unpacked/malware_3.exe/__pycache__/archive.cpython-35.pyc' doesn't exist
# Successful decompilation after using the prepend option
test@test:python python_exe_unpack.py -p unpacked/malware_3.exe/archive
[*] On Python 2.7
[+] Magic bytes are already appended.

# Successfully decompiled file
[+] Successfully decompiled.

Analyzing python assembly

If you weren’t able to extract the python “original” code following the previous steps, then you can try to extract the assembly (but it isn’t very descriptive, so try to extract again the original code).In here I found a very simple code to disassemble the .pyc binary (good luck understanding the code flow). If the .pyc is from python2, use python2:

Disassemble a .pyc
>>> import dis
>>> import marshal
>>> import struct
>>> import imp
>>>
>>> with open('hello.pyc', 'r') as f:  # Read the binary file
...     magic = f.read(4)
...     timestamp = f.read(4)
...     code = f.read()
...
>>>
>>> # Unpack the structured content and un-marshal the code
>>> magic = struct.unpack('<H', magic[:2])
>>> timestamp = struct.unpack('<I', timestamp)
>>> code = marshal.loads(code)
>>> magic, timestamp, code
((62211,), (1425911959,), <code object <module> at 0x7fd54f90d5b0, file "hello.py", line 1>)
>>>
>>> # Verify if the magic number corresponds with the current python version
>>> struct.unpack('<H', imp.get_magic()[:2]) == magic
True
>>>
>>> # Disassemble the code object
>>> dis.disassemble(code)
  1           0 LOAD_CONST               0 (<code object hello_world at 0x7f31b7240eb0, file "hello.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (hello_world)
              9 LOAD_CONST               1 (None)
             12 RETURN_VALUE
>>>
>>> # Also disassemble that const being loaded (our function)
>>> dis.disassemble(code.co_consts[0])
  2           0 LOAD_CONST               1 ('Hello  {0}')
              3 LOAD_ATTR                0 (format)
              6 LOAD_FAST                0 (name)
              9 CALL_FUNCTION            1
             12 PRINT_ITEM
             13 PRINT_NEWLINE
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE

PyInstaller raw marshal & Pyarmor v9 static unpack workflow

  • Extract embedded marshal blobs: pyi-archive_viewer sample.exe and export raw objects (e.g., a file named vvs). PyInstaller stores bare marshal streams that start with 0xe3 (TYPE_CODE with FLAG_REF) instead of full .pyc files. Prepend the correct 16-byte .pyc header (magic for the embedded interpreter version + zeroed timestamp/size) so decompilers accept it. For Python 3.11.5 you can grab the magic via imp.get_magic().hex() and patch it with dd/printf before the marshal payload.
  • Decompile with version-aware tools: pycdc -c -v 3.11.5 vvs.pyc > vvs.py or PyLingual. If only partial code is needed, you can walk the AST (e.g., ast.NodeVisitor) to pull specific arguments/constants.
  • Parse the Pyarmor v9 header to recover crypto parameters: signature PY<license> at 0x00, Python major/minor at 0x09/0x0a, protection type 0x09 when BCC is enabled (0x08 otherwise), ELF start/end offsets at 0x1c/0x38, and the 12-byte AES-CTR nonce split across 0x24..0x27 and 0x2c..0x33. The same pattern repeats after the embedded ELF.
  • Account for Pyarmor-modified code objects: co_flags has bit 0x20000000 set and an extra length-prefixed field. Disable CPython deopt_code() during parsing to avoid decryption failures.
  • Identify encrypted code regions: bytecode is wrapped by LOAD_CONST __pyarmor_enter_*__LOAD_CONST __pyarmor_exit_*__. Decrypt the enclosed blob with AES-128-CTR using the runtime key (e.g., 273b1b1373cf25e054a61e2cb8a947b8). Derive the per-region nonce by XORing the payload-specific 12-byte XOR key (from the Pyarmor runtime) with the 12 bytes in the __pyarmor_exit_*__ marker. After decryption, you may also see __pyarmor_assert_*__ (encrypted strings) and __pyarmor_bcc_*__ (compiled dispatch targets).
  • Decrypt Pyarmor “mixed” strings: constants prefixed with 0x81 are AES-128-CTR encrypted (plaintext uses 0x01). Use the same key and the runtime-derived string nonce (e.g., 692e767673e95c45a1e6876d) to recover long string constants.
  • Handle BCC mode: Pyarmor --enable-bcc compiles many functions to a companion ELF and leaves Python stubs that call __pyarmor_bcc_*__. Map those constants to ELF symbols with tooling such as bcc_info.py, then decompile/analyze the ELF at the reported offsets (e.g., __pyarmor_bcc_58580__bcc_180 at offset 0x4e70).

Python to Executable

To start, we’re going to show you how payloads can be compiled in py2exe and PyInstaller.

To create a payload using py2exe:

  1. Install the py2exe package from http://www.py2exe.org/
  2. For the payload (in this case, we will name it hello.py), use a script like the one in Figure 1. The option “bundle_files” with the value of 1 will bundle everything including the Python interpreter into one exe.
  3. Once the script is ready, we will issue the command “python setup.py py2exe”. This will create the executable, just like in Figure 2.
from distutils.core import setup
import py2exe, sys, os

sys.argv.append('py2exe')

setup(
    options = {'py2exe': {'bundle_files': 1}},
    #windows = [{'script': "hello.py"}],
  console = [{'script': "hello.py"}],
    zipfile = None,
)
C:\Users\test\Desktop\test>python setup.py py2exe
running py2exe
*** searching for required modules ***
*** parsing results ***
*** finding dlls needed ***
*** create binaries ***
*** byte compile python files ***
*** copy extensions ***
*** copy dlls ***
copying C:\Python27\lib\site-packages\py2exe\run.exe -> C:\Users\test\Desktop\test\dist\hello.exe
Adding python27.dll as resource to C:\Users\test\Desktop\test\dist\hello.exe

To create a payload using PyInstaller:

  1. Install PyInstaller using pip (pip install pyinstaller).
  2. After that, we will issue the command “pyinstaller –onefile hello.py” (a reminder that ‘hello.py’ is our payload). This will bundle everything into one executable.
C:\Users\test\Desktop\test>pyinstaller --onefile hello.py
108 INFO: PyInstaller: 3.3.1
108 INFO: Python: 2.7.14
108 INFO: Platform: Windows-10-10.0.16299
………………………………
5967 INFO: checking EXE
5967 INFO: Building EXE because out00-EXE.toc is non existent
5982 INFO: Building EXE from out00-EXE.toc
5982 INFO: Appending archive to EXE C:\Users\test\Desktop\test\dist\hello.exe
6325 INFO: Building EXE from out00-EXE.toc completed successfully.

References

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks