Skip to content
λmaldev wiki/
pagesEncrypted Shellcode Loader
T1027.009WindowsLinuxC / C++RustXORAES

Encrypted Shellcode Loader

updated 2026-08-047 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

A shellcode loader’s job is simple: get encrypted bytes from somewhere safe, decrypt them, make them executable, and transfer control. The encryption step means nothing recognisable sits in the binary’s sections — no PE headers, no common shellcode preambles, nothing a static scanner can fingerprint.

The two variables that define a loader’s detection surface are where it gets the key (baked in, derived, fetched over the network) and how it calls into the payload (direct function pointer, CreateThread, an indirect callback). Neither dimension is a magic bypass; every path eventually produces executable private memory, and that is what modern sensors watch.

Note.

W^X discipline (allocate RW, decrypt, then flip to RX) is now table stakes. A loader that asks for PAGE_EXECUTE_READWRITE in a single call produces a finding in almost every EDR on the market before the first instruction of the payload runs.

The call chain

  1. 1
    embed ciphertext blob
    Shellcode is XOR- or AES-encrypted at build time and baked into the loader's .data or .rdata section.
  2. 2
    VirtualAlloc(MEM_COMMIT, PAGE_READWRITE)
    Allocate a RW region — never RWX at this point.
  3. 3
    decrypt in place
    XOR key or AES key+IV is derived at runtime (CPUID, env hash, etc.) to resist static extraction.
  4. 4
    VirtualProtect(PAGE_EXECUTE_READ)
    Flip the allocation to RX. W^X discipline removes the loudest static indicator.
  5. 5
    CreateThread / callback trampoline
    Execute via CreateThread, EnumSystemLocalesA, or another API that accepts a function pointer.

Reference implementation

The XOR variant is the simplest and the easiest to reason about.

loader.cC
// shellcode baked in as ciphertext at build time
extern const unsigned char sc_enc[];
extern const size_t        sc_enc_len;
static const unsigned char KEY[] = { 0xDE, 0xAD, 0xBE, 0xEF };

void run(void) {
  // 1. allocate RW — no execute flag yet
  unsigned char *buf = VirtualAlloc(NULL, sc_enc_len,
                                    MEM_COMMIT | MEM_RESERVE,
                                    PAGE_READWRITE);

  // 2. decrypt in place
  for (size_t i = 0; i < sc_enc_len; i++)
      buf[i] = sc_enc[i] ^ KEY[i % sizeof KEY];

  // 3. flip to RX — W gone before X arrives
  DWORD old;
  VirtualProtect(buf, sc_enc_len, PAGE_EXECUTE_READ, &old);

  // 4. execute via indirect callback (less conspicuous than CreateThread)
  EnumSystemLocalesA((LOCALE_ENUMPROCA)buf, 0);
}
Caution.

A static XOR key is recoverable from the binary in seconds. Key derivation — hashing the hostname, reading a beacon response, requiring a specific argument — meaningfully raises the analysis cost.

Key derivation patterns

Approach Analysis cost Ops cost
Hardcoded key in binary Trivial — strings or xorbrute Zero
Key derived from hostname/domain Forces domain-joined execution Breaks sandbox
Key fetched from C2 first stage Requires live C2 to decrypt High
Key split: half binary, half env var Breaks automated detonation Medium

Sandboxes typically run without a domain or specific environment variables. A loader that refuses to proceed unless it sees the right hostname or registry value is broken for the sandbox but functional in the target environment.

Execution primitives

Passing the buffer to CreateThread is the obvious path and the most monitored. Alternative APIs that accept a function pointer:

  • EnumSystemLocalesA / EnumSystemLanguageGroupsA — legitimate Windows enumeration callbacks
  • EnumChildWindows — takes an LPARAM you can put anything in
  • QueueUserAPC — queues to any alertable thread; see APC Injection
  • RtlCreateUserThread — avoids the kernel32 import but is equally visible at the kernel layer

None of these avoid ETW-TI. The kernel records every thread start regardless of which API created the thread.

Detection

ETW-TI
VirtualAlloc followed immediately by VirtualProtect on the same region is a strong loader pattern.
MEMORY SCAN
Executable private memory whose entropy is below 7.5 bits/byte after decryption — structured PE or shellcode.
YARA
XOR key schedules or AES S-box constants in the loader's .text section before the allocation call.
BEHAVIOURAL
A thread whose start address points into an anonymous (non-image) RX allocation.

The VirtualAlloc → VirtualProtect sequence on the same private region is the most reliable pattern. Production loaders that split the allocate-decrypt-protect steps across multiple frames or use indirect calls do not defeat this; they just add noise between the same two events.

Was this page useful?edit this page ↗