Disclaimer: this blog post is meant strictly for educational and ethical security research. Do not reverse engineer software without permission.

🧠 introduction

reverse engineering python applications can be fascinating, especially when they’re bundled into .exe files using tools like pyinstaller. In this post, I’ll walk you through how I deconstructed a windows executable (app.exe), identified it as a pyinstaller package, and recovered the original app.py source code.


🔍 step 1: passive recon

I started with no source just a single app.exe binary.

checking file properties

first, I ran the classic:

$ file app.exe # returns the type of file
app.exe: PE32+ executable (GUI) x86-64, for MS Windows, 7 sections

now we know that this is a windows executable file.

we could just use strings to confirm weather it is a pyinstaller package or not:

$ strings app.exe | grep pyinstaller # should return strings matching pyinstaller
_pyinstaller_pyz

this confirms that it is a pyinstaller bundled python application.

🔧 step 2: extraction

using an extractor

these pyinstaller bundled .exe files are like a zip file which we could just extract using tools like pyinstxtractor.

$ python3 pyinstxtractor.py app.exe
[+] Processing app.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.10
[+] Length of package: 59743120 bytes
[+] Found 1008 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth__tkinter.pyc
[+] Possible entry point: exe.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.10 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: app.exe
You can now use a python decompiler on the pyc files within the extracted directory

now we have a folder named app.exe_extracted in our working directory and will contain the entire python source bytecode which we could process further. Also this output contains metadata about python and pyinstaller versions which will come handy during decompilation.

in the app.exe_extracted folder we have the following files(entire python bytecode):

PIL                 _lzma.pyd             cv2                    pyi_rth_inspect.pyc          select.pyd
PYZ.pyz             _multiprocessing.pyd  app.pyc                pyi_rth_multiprocessing.pyc  struct.pyc
PYZ.pyz_extracted   _overlapped.pyd       libcrypto-1_1.dll      pyi_rth_pkgutil.pyc          tcl8
VCRUNTIME140.dll    _queue.pyd            libffi-7.dll           pyiboot01_bootstrap.pyc      tcl86t.dll
VCRUNTIME140_1.dll  _socket.pyd           libssl-1_1.dll         pyimod01_archive.pyc         tk86t.dll
_asyncio.pyd        _ssl.pyd              numpy                  pyimod02_importers.pyc       unicodedata.pyd
_bz2.pyd            _tcl_data             numpy-2.2.6.dist-info  pyimod03_ctypes.pyc          win32
_ctypes.pyd         _tk_data              numpy.libs             pyimod04_pywin32.pyc         yaml
_decimal.pyd        _tkinter.pyd          psutil                 python3.dll
_elementtree.pyd    base_library.zip      pyexpat.pyd            python310.dll
_hashlib.pyd        charset_normalizer    pyi_rth__tkinter.pyc   pywin32_system32

here we can see the entry/main python file named app.pyc which is the file we have to decompile.

decompilation

we can use a tool like pycdc to decompile the python bytecode as it supports python version 3.10.

$ pycdc ./app.exe_extracted/exe.pyc > app.py

voila!, now we have the source code in the app.py file which is:

# Source Generated with Decompyle++
# File: app.pyc (Python 3.10)

import cv2
import threading
import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageTk

class App:
    
    def __init__(self, window):
        self.window = window
        self.window.title('YOLOv8n Person Detection')
        self.window.geometry('800x640')
        self.video_label = tk.Label(window)
        self.video_label.pack()
        button_frame = tk.Frame(window)
        button_frame.pack(10, **('pady',))
        self.start_btn = tk.Button(button_frame, 'Start Streaming', ('Arial', 12), self.start_thread, **('text', 'font', 'command'))
        self.start_btn.pack(tk.LEFT, 10, **('side', 'padx'))
        self.stop_btn = tk.Button(button_frame, 'Stop Streaming', ('Arial', 12), self.stop_streaming, tk.DISABLED, **('text', 'font', 'command', 'state'))
        self.stop_btn.pack(tk.LEFT, 10, **('side', 'padx'))
        self.cap = None
        self.streaming = False

    
    def start_thread(self):
        if not self.streaming:
            self.streaming = True
            self.stop_btn.config(tk.NORMAL, **('state',))
            self.start_btn.config(tk.DISABLED, **('state',))
            threading.Thread(self.stream, **('target',)).start()
            return None

    
    def stream(self):
        self.cap = cv2.VideoCapture(0)
        if not self.cap.isOpened():
            messagebox.showerror('Error', '❌ Cannot open video stream.')
            self.reset_buttons()
            return None
        if None.streaming:
            (ret, frame) = self.cap.read()
            if not ret:
                pass
            else:
                frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                img = Image.fromarray(frame_rgb)
                imgtk = ImageTk.PhotoImage(img, **('image',))
                self.video_label.imgtk = imgtk
                self.video_label.config(imgtk, **('image',))
                if not self.streaming:
                    self.cap.release()
                    self.reset_buttons()
                    return None

    
    def stop_streaming(self):
        self.streaming = False
        if self.cap:
            self.cap.release()
        self.window.after(100, self.window.destroy)

    
    def reset_buttons(self):
        self.start_btn.config(tk.NORMAL, **('state',))
        self.stop_btn.config(tk.DISABLED, **('state',))

    
    def on_close(self):
        self.streaming = False
        if self.cap:
            self.cap.release()
        self.window.destroy()


root = tk.Tk()
app = App(root)
root.protocol('WM_DELETE_WINDOW', app.on_close)
root.mainloop()

this looks like an application that uses your webcam to display a real-time video feed with OpenCV.

📦 lessons to note

  • pyInstaller doesn’t truly compile it’s packages.
  • security through obscurity isn’t real security.
  • tools like pyinstxtractor, pycdc are powerful allies for binary introspection.
  • always check the Python version before decompiling.

📌 final thoughts

reverse engineering can be easy with the help of AI tools like chatgpt and likely tooling but still requires a methodical approach.

always make sure you’re authorized to reverse engineer the application you’re looking into. This stuff gets legally gray real fast.