Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PyDoublespeak

Proof of concept: a PyInstaller with customized bootloader and a native Rust payload. The payload provides two extension points:

  • before_interpreter() runs after loading python312.dll, but before CPython initialization;
  • install_hooks() runs after PyInstaller initializes its PYZ importer and before the application entry point.

Python source is never evaluated through PyRun_SimpleStringFlags. Python hooks are native callables created through the CPython C API.

Concept

Standard reverse engineering of PyInstaller binaries follows a well-established pipeline: extract .pyc archives, decompile bytecode, read Python source, make a verdict. The bootloader is treated as trusted infrastructure and is never audited.

PyDoublespeak exploits this assumption. The Python bytecode is real, functional, and clean — it is not a decoy. However, the bootloader installs native hooks on Python-level functions (e.g. builtins.open), altering their semantics at runtime. The bytecode executes correctly, but the meaning of its operations is silently redefined by the native layer.

Layout

fake_pyinstaller/
├── pyinstaller/   # vendored PyInstaller source and local build directories
├── payload/       # Rust hook library and user extension points
├── py_code/       # Python application
├── build/         # generated executables (ignored by Git)
├── build.bat
└── build.ps1

User-editable code lives in:

  • payload/src/user_code/pre_interpreter.rs
  • payload/src/user_code/hooks.rs
  • py_code

Requirements

  • Windows x64;
  • CPython 3.12 (the build script can locate a 3.12 installation managed by uv);
  • nightly Rust with the rust-src component;
  • MSVC Build Tools with the x64 C/C++ toolchain.

Example toolchain setup:

uv python install 3.12
rustup toolchain install nightly --component rust-src
rustup default nightly

Build

.\build.bat

Release is the default. Select the complete Rust/bootloader profile explicitly with:

.\build.bat -Release
.\build.bat -Debug

Debug keeps diagnostic and library strings, unwind metadata, and unwind panics. Release uses immediate abort and removes diagnostic/location strings.

The result is written to build\main.exe. The bootloader is rebuilt only when the Rust payload or bootloader sources change; changing only Python code repacks the application without rebuilding the bootloader.

Useful options:

.\build.bat -EntryPoint "py_code\app.py" -Name "app"
.\build.bat -Windowed
.\build.bat -RebuildBootloader
.\build.bat -PythonExe "C:\path\to\python.exe"

The demo hook replaces py_code.check.check: never believe me returns True, while other values return False. The unhooked Python implementation uses the separate value believe me to make interception visible.

Release behavior

The release payload uses panic = "immediate-abort" and rebuilds Rust std with panic location details disabled. Consequently, a Rust panic terminates the process immediately and cannot be converted into a Python exception. Ordinary HookError values are still reported through the Python C API.

Third-party code

This repository vendors PyInstaller. Their original license files and attribution are preserved in their respective source directories.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages