Skip to content
ScannerServer.
Journal / Article
Cyber Security

The Art of Custom Shellcode: Evading Modern Mitigations

ScannerServer3 min read
A collection of physical RAM memory modules
A collection of physical RAM memory modules. Photo: Blake Patterson / Wikimedia Commons · CC BY 2.0. Resized; may be cropped for display.

The Demise of Off-The-Shelf Exploits

In the nascent days of cybersecurity, exploit development was straightforward. A researcher would identify a stack-based buffer overflow, generate a generic msfvenom payload, append a massive NOP sled, overwrite the instruction pointer, and effortlessly receive an interactive root shell. Those days are permanently behind us.

Modern operating systems and compilers have erected formidable defensive barriers: Data Execution Prevention (DEP / NX), Address Space Layout Randomization (ASLR), Stack Canaries, and Control Flow Guard (CFG). In contemporary hardened environments, off-the-shelf payloads generated by public frameworks are immediately flagged, isolated, and terminated by modern security controls.

To survive modern defensive architectures, an engineer or red-team operator must understand the meticulous craft of custom position-independent shellcode development. Here is the technical breakdown of modern shellcode architecture, memory evasion, and evasion primitives.

Fundamental Primitives of Position-Independent Code (PIC)

Shellcode must be capable of executing seamlessly regardless of where it is injected in virtual memory. It cannot rely on hardcoded absolute memory addresses, because ASLR randomizes base addresses of the stack, heap, and loaded libraries on every process execution.

1. Dynamic Delta Offset Calculation

To access its own embedded data (such as strings or configuration blocks), shellcode must determine its current instruction pointer (EIP or RIP) dynamically. In 64-bit x86-64 architecture, this is trivial thanks to RIP-relative addressing (lea rax, [rip + string_offset]). However, in 32-bit x86, reading EIP directly is forbidden by the instruction set.

Analysts utilize the classic call/pop technique or the Floating-Point Unit (FPU) trick:

; Classic Call/Pop Delta Offset Primitive
    call get_eip
get_eip:
    pop ebx             ; EBX now holds the exact runtime memory address of get_eip
    add ebx, payload_offset

2. Absolute Elimination of Null Bytes (\x00)

Many classic memory vulnerabilities occur in string manipulation functions (such as strcpy, strcat, or sprintf). These functions treat the null byte (0x00) as an end-of-string terminator. If your shellcode contains a single null byte, the memory copy operation terminates prematurely, corrupting the payload.

Eliminating null bytes requires disciplined assembly patterns:

  • Avoid mov eax, 0 (which compiles to B8 00 00 00 00). Use xor eax, eax (compiles to 31 C0).
  • Avoid loading small immediate values into 32-bit registers (e.g., mov eax, 1 contains three trailing null bytes). Instead, zero the register and utilize 8-bit sub-registers (xor eax, eax; mov al, 1) or stack operations (push 1; pop rax).

Dynamic API Resolution via PEB and TEB Parsing

On Windows, a raw shellcode payload injected into a compromised process has no linked imports. It cannot simply invoke WinExec or VirtualProtect directly. It must dynamically locate these functions in memory by manually navigating the operating system’s internal structures.

The Resolution Chain:

  1. Locate the PEB: In 64-bit Windows, the Thread Environment Block (TEB) is accessible via the GS segment register. The shellcode reads offset 0x60 to obtain a pointer to the Process Environment Block: mov rax, gs:[0x60].
  2. Navigate the LDR: At offset 0x18 of the PEB resides the PEB_LDR_DATA structure, which maintains doubly-linked lists of all dynamic libraries loaded into the process.
  3. Traverse InLoadOrderModuleList: The shellcode iterates through the modules until it discovers kernel32.dll or ntdll.dll.
  4. Parse the Export Directory: Upon locating kernel32.dll, the shellcode parses the Portable Executable (PE) headers, finds the Export Address Table (EAT), Export Name Table (ENT), and Ordinal Table.

API Hashing to Evade Static String Inspection

Storing ASCII strings like "VirtualAlloc" or "CreateProcessA" inside shellcode is an amateur mistake that immediately triggers static YARA rules and memory scanners. Instead, professional shellcode implements API Hashing.

The developer computes a 32-bit hash (such as ROR13) of the function name ahead of time. During execution, the shellcode iterates through the export table, hashes each exported function name on the fly, and compares the resulting hash against its precomputed target:

// Conceptual ROR13 Hashing Algorithm
DWORD Ror13Hash(const char *functionName) {
    DWORD hash = 0;
    while (*functionName) {
        hash = (hash >> 13) | (hash << (32 - 13));
        hash += *functionName++;
    }
    return hash;
}

When the computed hash matches the target constant, the shellcode extracts the matching function pointer from the Address Table. Zero human-readable strings ever appear in the binary.

Evading Modern Mitigations: Bypassing DEP and CFG

1. ROP Stagers to Defeat DEP/NX

Data Execution Prevention marks non-code memory pages (such as the stack and heap) as non-executable. If execution redirects directly to shellcode stored on the stack, the CPU generates an immediate hardware access violation.

To defeat DEP, custom payloads utilize a preliminary Return-Oriented Programming (ROP) chain. The ROP chain chains together short instruction snippets ("gadgets") ending in ret already residing in executable library memory. The sole objective of this initial ROP chain is to invoke VirtualProtect (on Windows) or mprotect (on Linux), flipping the permissions of the shellcode memory page from PAGE_READWRITE to PAGE_EXECUTE_READWRITE before transferring execution control.

2. Direct System Calls to Bypass EDR Hooks

Enterprise EDRs place inline hooks inside user-mode libraries (ntdll.dll). If shellcode invokes NtAllocateVirtualMemory through the standard library interface, it lands directly into the EDR inspection hook.

Modern evasive shellcode bypasses user-mode hooks entirely by issuing Direct System Calls. The shellcode reads the system call number (SSN) dynamically (using techniques like Hell's Gate or Halo's Gate to account for different OS build versions) and invokes the syscall CPU instruction directly from raw assembly, completely evading all user-space defensive hooks.

Conclusion

Custom shellcode development is the purest expression of computer architecture and assembly engineering. It requires intimate comprehension of CPU instruction encoding, OS internals, calling conventions, and memory virtualization. Relying on pre-packaged, automated generators is an obsolete paradigm; true security research and adversarial testing demand bespoke, meticulously crafted payloads engineered to survive modern defensive scrutiny.

Related Technical Research

Scroll to Top