Article Image

Protecting Python Executables: Why PyArmor and PyInstaller Aren't Enough

26th March 2026

The Python Protection Problem

Python is interpreted. Your source code runs directly, which means shipping a Python application means shipping your source code. Every tool in the Python ecosystem that creates .exe files is working around this fundamental limitation.

Some do it badly. Some do it well. None do it well enough on their own.

If you distribute a Python desktop app, a CLI tool, a trading bot, or any on-premise software, your intellectual property is at risk the moment the .exe leaves your build server.

PyInstaller: Not Protection, Just Packaging

PyInstaller bundles your Python script, its dependencies, and a Python interpreter into a single .exe file. It's the most popular deployment tool for Python applications. It is not a protection tool.

PyInstaller creates a self-extracting archive. At runtime, it unpacks everything to a temp directory and runs your Python code normally. The .pyc bytecode files are sitting right there in the archive.

pyinstxtractor extracts everything from a PyInstaller bundle in one command:

python pyinstxtractor.py myapp.exe

You get the complete .pyc files. Run them through uncompyle6 or decompyle3 and you have readable Python source code. Variable names, comments (in docstrings), class structures, everything.

This takes about 30 seconds. There is no skill required. A tutorial with 50,000 views on YouTube walks through it step by step.

PyInstaller with --key flag adds AES encryption to the .pyc files. The decryption key is embedded in the binary's bootloader. Extract the key, decrypt the files. pyinstxtractor handles this automatically.

PyArmor: Cracked

PyArmor is the most well-known commercial Python obfuscation tool. It encrypts Python bytecode and uses a custom runtime to decrypt and execute it. PyArmor v8 rewrote the protection engine from scratch, claiming stronger encryption.

In February 2025, security researchers published a full bypass of PyArmor v8's protection. The vulnerability: PyArmor used AES-GCM encryption but didn't validate the authentication tag. This meant an attacker could decrypt the bytecode without the correct key by manipulating the ciphertext. The protection was cryptographically broken, not just reverse-engineered.

The bypass was automated. Scripts appeared on GitHub within days. Any PyArmor v8 protected application could be deobfuscated with a single command.

PyArmor has released patches, but the fundamental architecture remains the same: encrypted Python bytecode decrypted by a runtime you ship alongside it. The decryption key and logic are in the binary. It's a question of when, not if, the next bypass appears.

The Windows Defender Problem

PyArmor-protected binaries frequently trigger Windows Defender and other antivirus engines. The PyArmor runtime uses code injection techniques (injecting into the Python interpreter's memory) that look identical to malware behavior.

Users download your application, Windows Defender quarantines it, and you spend your week dealing with support tickets. You can submit false positive reports to Microsoft, but the detections keep coming back with each Defender update.

This isn't a minor inconvenience. For commercial software, antivirus false positives destroy user trust.

Cython: Partial Solution

Cython compiles Python to C extension modules (.pyd files). The resulting binaries are native code, harder to reverse engineer than .pyc bytecode. Some developers use Cython as their protection strategy.

The problems: Cython doesn't compile all Python code. Async/await, complex generators, and many dynamic Python patterns require fallback to standard Python. You end up with a mix of compiled .pyd files and unprotected .pyc files. An attacker focuses on the .pyc files.

String literals in Cython-compiled code are still readable. "Invalid license", "API endpoint", and "SELECT * FROM users" appear in the .pyd binary in plain text. The control flow is harder to follow than Python bytecode, but experienced reverse engineers handle C-compiled code daily.

Nuitka: The Right Foundation

Nuitka is a Python-to-C compiler that handles the full Python language. Unlike Cython, it compiles your entire application, including all dependencies. The output is a native executable with no .pyc files anywhere.

python -m nuitka --standalone --onefile myapp.py

This produces a single .exe that contains compiled C code, not Python bytecode. pyinstxtractor doesn't work on it. uncompyle6 has nothing to decompile. The Python source code is genuinely gone.

Nuitka is the best first step for Python protection. But it's not the last step.

What Nuitka Doesn't Hide

Nuitka compiles Python to C, then compiles C to native code. The native code is standard x86-64 that IDA Pro and Ghidra handle perfectly well.

String literals remain readable. Every "password", "license_key", and "https://api.example.com" in your Python code becomes a string constant in the compiled binary. Running strings on a Nuitka binary reveals all of them.

Function structure is visible. Nuitka preserves Python's function structure as C functions. The names are mangled, but the call graph, branching logic, and data flow are all there. A reverse engineer can follow your license validation step by step.

Python C API calls reveal intent. Nuitka-compiled code calls CPython API functions like PyDict_GetItem, PyList_Append, PyLong_AsLong. These calls tell the analyst what Python types you're using and what operations you're performing. It's not source code, but it's far from opaque.

The Two-Step Solution

The approach that actually works: compile with Nuitka, then protect with ChaosProtector.

python -m nuitka --standalone --onefile myapp.py
ChaosProtector.exe --input myapp.exe --output myapp_protected.exe --flags 309

Step one eliminates Python bytecode entirely. Step two protects the resulting native binary.

What ChaosProtector Adds

String encryption. Every string literal in the Nuitka binary gets encrypted in place. The strings command returns nothing useful. API endpoints, error messages, license keys, all encrypted until runtime.

Code virtualization. Selected functions are converted from x86-64 machine code into custom bytecode executed by an embedded VM. IDA Pro sees a dispatcher loop, not your logic. This is the same technique used by VMProtect on C/C++ applications. No automated tool can reverse it.

Import protection. The import table (which lists every DLL and function your binary calls) is replaced with a runtime resolver. Static analysis can't determine your system call surface.

Anti-debug. Detects debuggers like x64dbg and WinDbg through TLS callbacks. If someone attaches a debugger, the process terminates immediately.

Integrity checking. CRC32 hash over the code section, verified at startup. Any byte patch (even a single NOP to skip a license check) triggers a crash.

Zero AV False Positives

ChaosProtector doesn't inject code into other processes. It doesn't use hooking techniques. It doesn't modify the PE header in ways that trigger heuristic detections. The protected binary is a standard Windows executable with additional code sections.

This is a direct contrast to PyArmor, whose runtime injection triggers constant false positives. ChaosProtector-protected binaries pass Windows Defender, Norton, Kaspersky, and other major AV engines cleanly.

Practical Walkthrough

Here's the complete pipeline for a Python application:

1. Compile with Nuitka

pip install nuitka
python -m nuitka --standalone --onefile --windows-console-mode=disable myapp.py

Test the Nuitka output thoroughly. Make sure every feature works. Nuitka compilation occasionally changes behavior for highly dynamic code (eval, exec, dynamic imports).

2. Protect with ChaosProtector

Upload the binary to the ChaosProtector dashboard or use the CLI:

ChaosProtector.exe --input myapp.exe --output myapp_protected.exe --flags 309

Flag 309 enables all protection layers: code virtualization, string encryption, import protection, anti-debug, and integrity checking.

3. Test the protected binary

Run it on a clean machine. Verify all features work. Pay attention to startup time (virtualized functions add a few milliseconds) and any network calls (import protection resolves DLLs at runtime, which is transparent but worth testing).

4. Ship it

The protected binary is self-contained. No runtime dependencies, no external DLLs, no license files to bundle. One .exe file.

What About py2exe and cx_Freeze?

Same story as PyInstaller. They bundle .pyc files into an archive. The extraction tools work on all of them. py2exe bundles are extracted with unpy2exe. cx_Freeze bundles are standard zip archives.

None of these packaging tools provide protection. They're deployment tools. Use them if you need them for packaging, but don't confuse packaging with protection.

Choosing the Right Protection Level

Not every Python application needs full virtualization. Here's a guide:

Free tier (string encryption + import protection): Good for applications where you want to hide configuration, API keys, and endpoint URLs. Blocks casual inspection. Available on the free plan.

Pro tier (all protections): Required if your application contains proprietary algorithms, license validation, or code that competitors would benefit from reading. Code virtualization makes reverse engineering impractical, not just difficult. See pricing for details.

Start with Nuitka compilation alone and see what strings reveals. If the output concerns you, add ChaosProtector.


Compile with Nuitka. Protect with ChaosProtector. Ship with confidence. Try it free.

Ready to protect your software?

Download ChaosProtector for free and start protecting your binaries in minutes.

Download Free