- From my AutoNuitkaDecompiler to https://github.com/DimaReverse/hexpyrion the Nuitka himself is fully defeated.
- And someone requested to alternative to solve this problem. This the peak of Python converting C without any problem.
- The solution is converting your python code via Python 3.14 (for example) with getting all his dependecies and conveting into onefile.
- This forces to run .exe to decompile because currently no static decompiler available!
- https://github.com/HydraDragonAntivirus/HydraDragonAntivirus/tree/development-version/hydradragon/python_hook_backend/bin You can try decompile via forking this project.
- Cython 3.0.11 transpiles your
.pyinto real C (/* Generated by Cython */, ~270+ KB of C per demo). - Local Modules / Self Imports (e.g.
import utils,from core.math import multiply): All internal.pymodules and packages in the project are automatically discovered, transpiled with Cython, and compiled with MSVC into native.pydextension modules. Zero Python bytecode is exposed. - A genuine Visual Studio 2026 project (
.vcxproj+.sln, toolset v145, x64 Release) wraps the application and all local.pydextension modules. Visual Studio 2022 (v143) is used as-is when 2026 is not installed. - MSBuild compiles the entire solution — Visual Studio auto-generates the debug binaries (
.pdb) via/Zi+/DEBUGunless--releaseis passed. - The stdlib is staged as
python314.zipplus the required.pyd/.dllfiles next to the EXE, so it boots on a PC with no Python installed. No Debug CRT (*_d.pyd) is ever shipped.
python -m cpythonizer vs-build examples/single_hello.py --name Hello --dist dist --onefile
Options:
--onefile: Folds the program, runtime (python314.dll), stdlib (python314.zip), extension modules, and third-party packages into a single self-contained.exe(see below).--zombie: Zombie loader (implies--onefile): the runtime is dropped to%TEMP%as real files, but the program itself is mapped in RAM and never written to disk - the dropped EXE is an empty shell (see below).--encrypt/--no-encrypt: Encrypts the onefile payload with a unique random AES-256 key per build (default: enabled).--no-antidump: Zombie mode: keepSizeOfImageintact instead of blanking it (anti-dump off, default: on).--guard <off|basic|full>: Zombie mode: obfuscate the loader itself — compile-time string encryption, anti-debug probes and a flattened role dispatch (fulladds hardware-breakpoint detection and fake protector sections). Default:off.--lzma2/--compress lzma2: Compresses the onefile payload using LZMA2 compression.--lzma-preset <0..9>: Configurable LZMA2 compression level (default:9).--lzma-extreme/--no-lzma-extreme: Enable/disable LZMA2 extreme preset for extra compression (default: enabled).--obfuscate: Strips inline and line comments from all Python sources (entry & local modules) before Cython transpilation without touching strings or regexes.--release: Strips debug symbols / PDB, enables max compiler optimizations, and applies Cython speedup directives.--keep-pdb: Retains debug symbols (.pdb) even when building with--release.--noconsole/--windowed: Windows GUI subsystem — hides the black console/CMD window at launch (for Tkinter, PyQt, etc.).--embed/--no-embed: Generate standalonemain()entrypoint viacython --embed(default: enabled).--icon <path>: Executable icon (.icoor.png; PNG files are converted automatically).--include-package <name>: Explicitly bundle a third-party package (third-party imports likerequestsare also detected and bundled automatically).--vs 2026/--vs 2022: Pin a specific Visual Studio version (default:auto= newest installed).
The built-in obfuscator safely transforms Python sources before or during Cython compilation:
- Comment Stripper: Tokenize-based stripping of inline (
# ...) and standalone comments without altering strings, raw strings, docstrings or regexes. - AST Function Name Randomization: Automatically renames function definitions and internal callers to randomized identifiers (
_cpx_fn_<hex>) via Python AST. - Random Decoy Step Comments: Injects randomized decoy step comment lines (
# [cpx-step: <token>]) before statements. - AES-256-CBC Self-Decrypting Wrapper (
Encryptclass): Automatically generates fresh 256-bit AES keys & 128-bit IVs, encrypts source codes, and outputs self-executingexec(decrypt_text(...))scripts. - Base64 Exec Wrapper: Compresses/encodes sources into self-executing
import base64; exec(base64.b64decode(...))scripts.
# Strip comments
python -m cpythonizer obfuscate myfile.py --in-place
# Full obfuscation (strip comments + rename functions via AST + insert random step comments)
python -m cpythonizer obfuscate myfile.py --all --in-place
# AES-256-CBC self-decrypting wrapper (auto key & IV generation)
python -m cpythonizer obfuscate myfile.py --aes
# Base64 exec wrapper
python -m cpythonizer obfuscate myfile.py --b64
# Interactive Encrypt CLI
python -m cpythonizer.obfuscator
# Integrated with vs-build:
python -m cpythonizer vs-build main.py --onefile --obfuscate --releasePython API:
from cpythonizer.obfuscator import remove_inline_comments, obfuscate_source, Encrypt
# Strip comments:
remove_inline_comments("myfile.py", in_place=True)
# Full AST + step comment obfuscation:
cleaned_code = obfuscate_source(source_text, rename_funcs=True, add_comments=True)
# Base64 encrypt:
Encrypt().encrypt("myfile.py")The onefile EXE is [ stub ][ AES-256 encrypted payload ZIP / XZ ][ 24-byte trailer ], where the trailer is
CPYONE1 (stored) or CPYLZM2 (LZMA2 max) + payload size + uncompressed size.
- Per-Build Random Encryption (Polymorphic): Every build generates a fresh 256-bit AES key (
os.urandom(32)), initialization vector (os.urandom(16)), and key mask array (os.urandom(32)). The payload is encrypted with native Windows BCrypt (AES-256-CBC with PKCS#7 padding). The stub stores the key masked (KEY_MASK ^ MASKED_KEY), derives the key on startup, decrypts the payload in RAM via hardware-accelerated AES-NI, and wipes the key memory (SecureZeroMemory). No static binary signature or static file offsets are identical across builds. - LZMA2 Max Compression (
--lzma2): Compresses the payload with LZMA2 extreme preset 9, shrinking the onefile binary by ~55% (from ~35 MB down to ~15 MB). The embedded 0BSDxz-embeddeddecoder decompresses the stream entirely in RAM. - Dependency-Free Launcher: The stub is pure Win32 C (
build/…/<app>_stub.c,/MT, static CRT, no Python dependency at all).
On startup the stub creates %TEMP%\<app>-<8 random hex>\, decrypts the payload in RAM, unpacks it there,
runs the program from it under its original name, waits for it, deletes the folder and
exits with the program's own exit code:
tasklist -> Hello.exe (never the temp path)
sys.argv[0] / sys.executable -> C:\...\Hello.exe
The command line is forwarded verbatim, argv[0] is rewritten to the shipped EXE, and the
working directory is left alone. Unpacking ~29 MB costs about 0.25 s per launch.
Debug switches: CPYTHONIZER_ONEFILE_VERBOSE=1 prints the temp folder, and
--cpythonizer-keep (or CPYTHONIZER_ONEFILE_KEEP=1) keeps it for inspection.
--onefile drops the whole staged folder to %TEMP%, the program EXE included.
--zombie drops only what genuinely must exist as a file and keeps the program in RAM:
python -m cpythonizer vs-build main.py --name Hello --zombie --lzma2 --releaseThe shipped EXE is [ stub ][ runtime blob ][ program blob ][ 56 byte trailer ], and the
stub is one binary with two roles, picked by the trailer magic (CPYZMB1 / CPYZMB2):
- Droper (the shipped EXE) - decrypts + unpacks the runtime blob
(
python314.dll, every.pyd,python314.zip,vcruntime140.dll,libcrypto-3.dll, …) into%TEMP%\<app>-<random>\, then writes the zombie next to it: its own stub bytes- the untouched program blob + a
CPYZMB2trailer, namedHello.exe. It runs the zombie, waits, deletes the folder and forwards the exit code.
- the untouched program blob + a
- Zombie (
%TEMP%\...\<app>.exe) - decrypts the program PE, maps it in memory (headers, sections, relocations, imports, section protections,.pdatafor x64 unwinding) and calls itswmainCRTStartup.
Because the zombie is produced by copying the droper's own prefix, no second payload is embedded anywhere, and the dropped EXE holds zero instructions of the real program — only the loader plus the still-encrypted blob:
build/onefile_ProbeZ/ProbeZ.exe 125 KB real program: "Py_Initialize", the .text, the imports
%TEMP%\ProbeZ-XXXX\ProbeZ.exe 228 KB zombie: no program code, no program imports
dist/ProbeZ/ProbeZ.exe 15 MB shipped: no program code either
Why the DLLs are dropped but the EXE is not. A manually mapped module is invisible to
the loader, so everything that expects a real on-disk module breaks: the import graph,
extension module loads, sys.prefix discovery (CPython looks for python314.zip relative to
the process image, which is why the zombie must run from %TEMP%), TLS and CRT
bookkeeping. Do that to every DLL and the program cannot start at all. The EXE is the one
image nothing else loads, so it is the one that can live purely in RAM - and once its DLLs
sit next to the zombie, the mapped program boots exactly like the ordinary staged build.
tasklist still shows Hello.exe and sys.argv[0] / sys.executable still point at the
distributed file.
Anti-dump — SizeOfImage only. After mapping, the loader blanks just
OptionalHeader.SizeOfImage. That is the field every memory dumper and
GetModuleInformation uses to size an image, so the dump comes out empty. Nothing else is
touched: the DOS/NT headers, the section table and .pdata must stay valid for the CRT, the
x64 unwinder and CPython's own module bookkeeping - wiping the header (as most loaders do)
breaks the program instead of protecting it. --no-antidump disables it at build time,
CPYTHONIZER_ONEFILE_NOANTIDUMP=1 at runtime.
Both blobs are AES-256-CBC encrypted with a fresh random key/IV/key-mask per build
(two independent sets, one per blob) and CRC32-checked after decryption; --lzma2 compresses
them with LZMA2 preset 9. The debug switches of --onefile work unchanged
(CPYTHONIZER_ONEFILE_VERBOSE=1, --cpythonizer-keep), and in verbose mode the loader prints
what it maps, which imports it resolved, and — if the mapped program ever faults — the faulting
address and the module it belongs to.
python -m cpythonizer vs-build shadowfox.py --name shadowfox_onefile --dist dist `
--onefile --icon shadowfox.ico --release --noconsole `
--add-data "shadowfox_logo.png;." --add-data "shadowfox.ico;." --add-data "assets;images"The flag is repeatable and takes PyInstaller's SOURCE;DEST syntax (a SOURCE:DEST
form is accepted too, and a Windows drive letter does not confuse the parser).
DEST is always a folder: a file keeps its own name inside it, a directory becomes a
subfolder of it. .. and absolute destinations are rejected, and a missing source is
reported before the build starts rather than after a two-minute MSBuild run.
Where the files end up, and how your program finds them:
| mode | location | lookup |
|---|---|---|
| plain | next to the EXE in dist/<app>/ |
os.path.dirname(sys.executable) |
--onefile / --zombie |
inside the payload, extracted to the %TEMP% folder |
sys._MEIPASS |
import os, sys
BASE = getattr(sys, "_MEIPASS", os.path.dirname(sys.executable))
logo = os.path.join(BASE, "shadowfox_logo.png")For the frozen modes sys._MEIPASS is set for you: the loaders export
_MEIPASS/CPYTHONIZER_TEMP pointing at the extraction folder, and the staged
python314.zip carries a sitecustomize.py that copies it onto sys during start-up.
Without it, sys._MEIPASS — the one attribute PyInstaller-frozen code usually asks for
directly — would be missing, and bundled files would be unreachable. Under --zombie this
needed a fix of its own: the loader set the variables to the path of the zombie file
rather than to its folder, so every bundle lookup failed there while working under
--onefile.
Security note. With --zombie the program itself is still never written to disk, but
anything passed to --add-data is, in the clear, in %TEMP% for as long as the app runs.
Do not ship secrets that way.
An --icon that the resource compiler rejects (for example RC2176: old DIB) no longer
kills the build: the project is regenerated without the icon, the build continues, and a
warning is printed. A missing icon beats no binary.
A GUI-subsystem build (--noconsole) has no console at all, so Windows gives every child
process that is itself a console application a brand new console window. That is why a Tkinter
app whose screens each shell out once (git, pip, where, ...) flashes a black rectangle on
every navigation. Measured inside a packaged build, launched the way Explorer launches it:
| flags | PE subsystem | own console | console a child cmd.exe gets |
|---|---|---|---|
--noconsole |
GUI | none | new window, visible |
--hide-console |
Console | present, hidden | inherits it, hidden |
--noconsole --hide-console |
GUI | hidden, self-allocated | inherits it, hidden |
| neither | Console | visible | inherits it, visible |
--hide-console keeps the console and hides its window (GetConsoleWindow + ShowWindow), so
children inherit something invisible to draw into instead of being handed a window of their own.
Combined with --noconsole — the mode to use for a GUI app — the PE stays GUI, so Windows creates
nothing at start-up and sys.stdout stays None exactly as with --noconsole, and the program
allocates one hidden console (AllocConsole + hide) for children to inherit. The policy is applied
to the loader stub and to the program, so neither of them can show a window the other hid.
What it costs, honestly:
- Windows creates the console before
main()runs, so--hide-consolealone can flash once at start-up. Combined with--noconsolethere is no such flash, since nothing is created for us. - A console object survives, so another process could
AttachConsoleand read what the program printed. Matters only if the app prints secrets; drop--hide-consoleif that is a concern. - AV/EDR heuristics classify the binary as a console application rather than a GUI one.
The code-level fix is still the better one where you own the source:
subprocess.Popen(cmd, creationflags=subprocess.CREATE_NO_WINDOW).
The loader is C, so it is the softest target in the package. --guard compiles the stub with
cpythonizer/obfuscate/obfus_msvc.h — an MSVC port of the usable parts of
DosX/obfus.h:
| level | what it adds |
|---|---|
off (default) |
plain loader, every literal visible |
basic |
compile-time string encryption, hidden imports, anti-debug at both decrypt sites, flattened role dispatch |
full |
basic + hardware-breakpoint probe (DR7) and fake protector sections (.aspack, .adata, __wibu00) |
Why a port instead of the upstream header. obfus.h does not support MSVC by design: it
rewrites every if/while/for/switch with GNU statement expressions and builds its
control-flow and junk layers on GNU inline AT&T asm, __builtin_choose_expr and __typeof__.
None of those exist for x64 cl.exe (#warning obfus.h doesn't support Visual C/C++), and
there is no flag that changes that. The port keeps what translates — the RND() mixing, stack
and encrypted strings, opaque predicates, the math VM, the anti-debug probe set with its
response behind an obfuscated sink, fake signatures — and replaces what does not: __rdtsc
and __readgsqword intrinsics instead of inline asm, and an explicit OBF_DISPATCH_*
dispatcher instead of the automatic if/while rewriting. The result in the binary is the
same shape OLLVM's flattening produces: one switch inside one loop with an opaque state value.
String encryption is real, not cosmetic. obfus.h's HIDE_STRING only keeps a literal out of
the data sections — MSVC still emits the bytes as .text immediates, so a raw grep over the
file finds them anyway. The port therefore encrypts at build time: CPY_HSTR("x") is rewritten
into a per-site XOR array decoded at the call site, so the plaintext never reaches the binary.
Hidden imports. basic/full resolve all 39 Win32 entry points the loader uses at run time
instead of importing them: PEB->ImageBaseAddress gives the loader's own base, its own import
directory yields GetModuleHandleW/LoadLibraryExW (forced references — the linker drops an
import nobody uses), and every other function is found by walking kernel32's and bcrypt.dll's
export directories and matching a keyed hash of "<module>!<function>". No function name exists
anywhere in the file, and the expected-hash table is itself stored encrypted. Import table of the
packed stub, counted by parsing the import directory (not by grepping strings):
bare /MT hello-world 1 DLL, 75 functions <- the static CRT, nothing else
guard basic / full 1 DLL, 76 functions <- 75 + GetFileSizeEx, no bcrypt at all
guard off 2 DLLs, 103 functions <- 96 KERNEL32 + 7 bcrypt
So basic/full leave the loader with no Win32 imports of its own: the set difference against
a bare /MT binary is one name, GetFileSizeEx, and it is not something the code calls directly -
all three call sites go through the hash table (checked in the preprocessed output). Where it comes
from is not explained, and it is harmless: a file-size API that appears in most CRT programs anyway.
None of the interesting ones are there: no CreateProcessW, no VirtualAlloc, no BCryptDecrypt,
no LoadLibraryW, no GetThreadContext, no AddVectoredExceptionHandler, no VirtualProtect.
What remains is the static CRT's own list (heap, TLS, stdio), which no work on the loader can remove -
only dropping the CRT entirely could, and the stub needs malloc/memcpy/fwprintf.
Fingerprints found in the first 200 KB of the packed EXE:
off CPYZMB1 CPYZMB2 RtlAddFunctionTable IsDebuggerPresent
full IsDebuggerPresent
The survivor is the static CRT's own KERNEL32!IsDebuggerPresent import, not loader code.
Forwarded exports. On Windows 11, 211 of kernel32's 1697 exports are forwarders: the
export's RVA points at an ASCII "NTDLL.RtlAddVectoredExceptionHandler" string inside the export
directory instead of at code. The resolver follows them (and bounds the export directory by
SizeOfImage rather than a fixed cap, because ntdll's alone exceeds 64 KiB). Handling this is not
cosmetic — AddVectoredExceptionHandler is one of the forwarded ones, so before this was fixed the
loader stored a string address in that slot and jumped into kernel32's string table when the
verbose crash locator was installed. The bug was invisible to a matrix that only checked exit codes,
because the call is reached only when CPYTHONIZER_ONEFILE_VERBOSE=1; see tests/ below.
Never writing an EXE to %TEMP% (--no-temp-exe). By default the loader drops a copy of its
own stub into the extraction folder, and that copy is what runs the program: the stub is a real file,
so CPython finds python314.zip next to its own executable, and when the program exits the parent
can delete the whole folder. --no-temp-exe removes that file entirely - the zombie role runs as a
second instance of the shipped EXE instead, told apart by an environment variable rather than by
its name, and the parent relocates the interpreter by hand (PYTHONHOME for sys.prefix,
PYTHONPATH for the flat embeddable layout, which has no Lib/ directory). Measured, both modes:
default temp folder contains: python314.zip, python314.dll, *.pyd, *.dll, <app>.exe
--no-temp-exe temp folder contains: python314.zip, python314.dll, *.pyd, *.dll
sys._MEIPASS / sys.prefix / __file__ : identical in both
temp folder removed on exit : yes, both
Without the relocation the child silently fell back to whatever Python was installed on the
machine, and on a clean target it would have died with "Could not find platform independent
libraries" - the test asserts the temp folder holds no *.exe and that the program still runs.
Anti-debug probes PEB.BeingDebugged, ProcessDebugPort / ProcessDebugObjectHandle,
CheckRemoteDebuggerPresent and the DR7 hardware-breakpoint bits, hides the thread with
NtSetInformationThread(ThreadHideFromDebugger) first, and reacts through a computed spin sink
rather than ExitProcess — a kill is a one-byte patch target, a sink is not. It is armed at the
two points worth protecting: before the runtime blob is decrypted and before the program blob is
decrypted and mapped. CPYTHONIZER_ONEFILE_NOAD=1 disables it at runtime so you can debug your
own build under a debugger.
python -m cpythonizer vs-build main.py --name Hello --zombie --lzma2 --release --guard fullpython tests\test_resolver.py # ~10 s, no build needed
CPXP_ZOMBIE_TESTS=1 python tests\test_zombie_switches.py # ~3 min, four MSBuild runstest_resolver.py compiles obfuscate/resolve.h on its own, resolves every hidden import through
the real table, and compares each pointer against GetProcAddress — an oracle that shares no code
with the resolver. It skips cleanly when no MSVC toolchain is present.
test_zombie_switches.py builds four variants (plain, guard full, guard full --no-antidump,
guard off) and asserts the behaviour, not just exit codes:
- the program's output is byte-identical to the unprotected build
CPYTHONIZER_ONEFILE_VERBOSE=1really prints loader diagnostics and still exits 0CPYTHONIZER_ONEFILE_KEEP=1leaves the dropped runtime behind; a normal run removes it--no-temp-exeleaves no*.exein that folder while the default mode does, and the program still lands on the samesys._MEIPASS/__file__(with nothing dropped next to the child, the interpreter has to be relocated by hand)- the default build reports blanking
SizeOfImage,--no-antidumpdoes not, and the dropped stub on disk keeps a valid one (Windows has to be able to load it) guard offimportsbcrypt.dll,guard fulldoes not
The verbose assertion is the one that matters: a loader whose verbose path crashes still runs the program perfectly in every other configuration, so any test that only looks at exit codes reports it as healthy.
A memory-dump / hook-based Python decompiler
(HydraDragonAntivirus python_hook_backend)
was run against a CPythonizer build (Hello.exe, built from examples/single_hello.py).
Result — no Python bytecode recoverable, only stubs:
Module: single_hello
Type: <class 'module'>
File: built-in
def factorial(n: 'int') -> 'int':
# COMPILED CODE - Metadata extraction:
# Local variables:
# n
# result
# i
# Argument count: 1
# Keyword-only args: 0
# Local variables count: 3
# AG: STUB_IDENTIFIED - Compiled code - no Python bytecode available
def main() -> 'int':
# COMPILED CODE - Metadata extraction:
# Local variables:
# cmd
# Argument count: 0
# Keyword-only args: 0
# Local variables count: 1
# AG: STUB_IDENTIFIED - Compiled code - no Python bytecode available
Function names and metadata remain visible (Cython keeps them as module attributes),
but no bytecode, no source logic, no constants can be reconstructed — the actual
code is native machine code produced by MSVC. Static decompilers (Hexpyrion-style)
get nothing at all, since there is no frozen .pyc to extract — unlike Nuitka
onefiles, which ship recoverable bytecode.
Even this library-free minimal build is already hard to reverse: there is no
embedded .py, no bytecode header, no co_code to carve — only MSVC-compiled
machine code plus metadata stubs. A reverser is reduced to plain native
reverse engineering (IDA/x64dbg level), with zero Python semantics to latch onto.
Add any real library and it becomes a nightmare: each dependency (e.g. requests,
numpy, cryptography) is itself transpiled into thousands of additional Cython
functions inside the same binary. The module list explodes, control flow is buried
under generated __pyx_ helpers, error paths and type dispatchers — while still
yielding zero bytecode. What was one small stub table becomes tens of thousands
of native functions with inlined, optimized logic. Automated Python tooling stays
blind; manual analysis must fight through MSVC-optimized (/O2 + LTCG) machine
code with no source-level landmarks.