Proof of concept: a PyInstaller with customized bootloader and a native Rust payload. The payload provides two extension points:
before_interpreter()runs after loadingpython312.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.
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.
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.rspayload/src/user_code/hooks.rspy_code
- Windows x64;
- CPython 3.12 (the build script can locate a 3.12 installation managed by
uv); - nightly Rust with the
rust-srccomponent; - 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.batRelease is the default. Select the complete Rust/bootloader profile explicitly with:
.\build.bat -Release
.\build.bat -DebugDebug 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.
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.
This repository vendors PyInstaller. Their original license files and attribution are preserved in their respective source directories.