Module 2: Crystal Palace — The PIC Linker
The spec-driven linker that transforms compiled COFF objects into flat position-independent shellcode — no PE headers, no import tables, just executable machine code.
Module Objective
Understand what Crystal Palace is, how the .spec linker-script language works, and how every directive in the Crystal-Loaders loader.spec contributes to the final PIC shellcode blob. By the end of this module you will be able to read any Crystal Palace spec file and explain exactly what it does at each step.
1. What Is Crystal Palace?
Crystal Palace is a Java-based linker and linker-script language designed by Raphael Mudge specifically for building PIC (position-independent code) DLL loaders. It was released in 2025 as part of the Tradecraft Garden project and is designed for building PIC DLL loaders, including User-Defined Reflective Loaders (UDRLs) for Cobalt Strike.
Crystal Palace takes compiled COFF object files (.o) produced by MinGW and transforms them into flat PIC shellcode with embedded resources. The transformation is driven by .spec files — declarative specifications that describe how to assemble, transform, and link the final payload.
Crystal Palace Is NOT a Compiler
This is a common misunderstanding. Crystal Palace does not compile C code. It processes already-compiled COFF object files. The C compiler (MinGW GCC) runs first, producing .o files. Crystal Palace then takes those object files and performs the linking, relocation resolution, and PIC transformation that would normally be handled by a traditional linker like ld — but instead of producing a PE executable, it produces raw position-independent shellcode.
The key insight is that Crystal Palace replaces the traditional Windows linker entirely. Instead of producing a PE with headers, section tables, and import directories, it produces a single contiguous block of executable bytes that can run from any memory address.
2. The Build Pipeline
The Crystal-Loaders build process follows a two-stage pipeline. First the C compiler produces COFF objects, then Crystal Palace links them into PIC shellcode:
Crystal-Loaders Build Pipeline
loader.c
x86_64-w64-mingw32-gcc
loader.x64.o
loader.spec
flat binary blob
The Makefile uses MinGW to cross-compile the C source into a COFF object file:
MakefileCC_64 = x86_64-w64-mingw32-gcc
$(CC_64) -DWIN_X64 -shared -masm=intel -Wall -Wno-pointer-arith -c src/loader.c -o bin/loader.x64.o
Compiler Flags Explained
| Flag | Purpose |
|---|---|
-DWIN_X64 | Defines the WIN_X64 preprocessor macro, enabling x64-specific code paths in the source |
-shared | Uses DLL calling conventions — important because the loader will eventually load a DLL (Beacon) |
-masm=intel | Uses Intel assembly syntax for any inline ASM blocks (instead of AT&T syntax) |
-Wall | Enables all standard compiler warnings |
-Wno-pointer-arith | Suppresses pointer arithmetic warnings (needed for the raw memory manipulation in PIC code) |
-c | Compile only, do not link — Crystal Palace does the linking |
The -c flag is the critical one. It tells GCC to stop after compilation and produce a .o object file instead of calling the linker. Crystal Palace handles everything from that point forward.
3. The Spec Language
Crystal Palace specs are declarative scripts that describe the complete linking and transformation process. Here is the actual loader.spec from Crystal-Loaders, annotated line by line:
loader.specname "Beacon BUD Loader"
describe "PIC loader to pass memory allocation information via Beacon User Data"
author "Daniel Duggan (@_RastaMouse)"
x64:
load "bin/loader.x64.o"
make pic +gofirst +optimize +disco
dfr "resolve" "ror13"
mergelib "../libgate.x64.zip"
mergelib "../libtcg.x64.zip"
generate $KEY 128
push $DLL
xor $KEY
preplen
link "dll"
push $KEY
preplen
link "key"
export
The spec reads top-down like a recipe. First it loads and transforms the compiled code, then it generates a random XOR key, encrypts the DLL payload, and links everything together into the final output. Let's break down each block:
Block 1: Metadata
The first three lines are purely informational — they name the spec, describe what it does, and credit the author. Crystal Palace uses these for identification and logging.
Block 2: Code Transformation (x64:)
The x64: block targets 64-bit architecture. Inside it, load brings in the compiled COFF object, make pic transforms it into position-independent code, dfr rewrites all API import references for runtime resolution, and mergelib pulls in LibGate and LibTCG library code.
Block 3: Data Preparation
generate $KEY 128 creates 128 bytes of cryptographically random data. The push $DLL block pushes the Beacon DLL onto the data stack, XORs it with the key, prepends a 4-byte length header, and links it into a section named "dll". The key itself gets the same length-prefix treatment and is linked into a section named "key".
Block 4: Export
export emits the final PIC shellcode — the code blob with all linked data sections appended.
4. Directive Reference
Every Crystal Palace directive available in the spec language, with its purpose and usage:
| Directive | Purpose | Example |
|---|---|---|
name | Names the spec for identification | name "Beacon BUD Loader" |
describe | Human-readable description | describe "PIC loader..." |
author | Author attribution | author "Daniel Duggan" |
x64: | Architecture block (x64-only in Crystal-Loaders) | x64: |
load | Load a compiled COFF object file into the linker | load "bin/loader.x64.o" |
make pic | Transform the loaded COFF into position-independent code | make pic +gofirst +optimize +disco |
+gofirst | Place the go() function at byte offset 0 of the PIC blob | Flag on make pic |
+optimize | Enable code size optimization passes | Flag on make pic |
+disco | Randomize function order in the PIC blob (the first function is preserved when used with +gofirst) | Flag on make pic |
dfr | Dynamic Function Resolution — rewrite all __imp_MODULE$Function references to call a resolver | dfr "resolve" "ror13" |
mergelib | Merge an external library's object files into the current build | mergelib "../libgate.x64.zip" |
generate | Generate random data (e.g., an XOR key) and store in a variable | generate $KEY 128 |
push | Push data onto the linker's data stack for processing | push $DLL |
xor | XOR the top-of-stack data with a named key | xor $KEY |
preplen | Prepend a 4-byte little-endian length prefix to the top-of-stack data | preplen |
link | Link data into a named section of the PIC blob | link "dll" |
export | Emit the final PIC shellcode as the build output | export |
patch | Write a value into a named symbol location (used in postex loader) | patch "pGetModuleHandle" $GMH |
The Data Stack Model
Crystal Palace uses a stack-based data model similar to Forth or PostScript. The push directive places data on top of the stack. Subsequent operations like xor and preplen modify the top-of-stack in place. Finally, link pops the data off and attaches it to the PIC blob as a named section. This stack model allows arbitrary data transformations to be chained declaratively.
5. Dynamic Function Resolution (DFR)
DFR is the mechanism that makes PIC code possible. In normal PE executables, the loader resolves API imports at load time using the Import Address Table. PIC shellcode has no IAT, so API addresses must be resolved at runtime by the shellcode itself.
The Problem
In the C source, Windows API functions are declared as DLL imports using the MODULE$Function naming convention:
C// These declarations tell the compiler: "this function comes from an external DLL"
// The '$' separator tells Crystal Palace: module = KERNEL32, function = VirtualAlloc
DECLSPEC_IMPORT LPVOID WINAPI KERNEL32$VirtualAlloc(LPVOID, SIZE_T, DWORD, DWORD);
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$VirtualProtect(LPVOID, SIZE_T, DWORD, PDWORD);
DECLSPEC_IMPORT int WINAPI MSVCRT$strncmp(const char *, const char *, SIZE_T);
After compilation, these become __imp_KERNEL32$VirtualAlloc symbols in the COFF object's symbol table. In a normal linking process, these would be resolved against import libraries (.lib files). Crystal Palace does something fundamentally different.
The Solution: DFR Rewriting
The dfr "resolve" "ror13" directive tells Crystal Palace to:
- Find every instruction in the code that references an
__imp_MODULE$Functionsymbol - Extract the module name and function name from the symbol
- Compute the ROR13 hash of both the module name and function name
- Rewrite each reference to instead call the user-supplied
resolve()function, passing the two hash values as DWORD arguments
DFR Rewriting Process
__imp_KERNEL32$VirtualAlloc
dfr "resolve" "ror13"
0x6A4ABC5B, 0xE553A458)
The resolve() function that Crystal Palace calls into is defined in loader.c:
C (loader.c)char * resolve(DWORD modHash, DWORD funcHash)
{
char * mod = findModuleByHash(modHash);
return findFunctionByHash(mod, funcHash);
}
findModuleByHash walks the PEB InMemoryOrderModuleList (the linked list of loaded DLLs maintained by the Windows loader), hashes each module name using the ROR13 algorithm, and compares against moduleHash. findFunctionByHash then walks the found module's Export Address Table, hashes each exported function name, and returns the matching function pointer.
Two Resolution Methods
ror13 Mode
- Replaces module + function names with 4-byte ROR13 hashes
- Two DWORD arguments passed to
resolve() - Strings never appear in the final shellcode
- Slightly harder to reverse-engineer
- Used by the main UDRL loader
strings Mode
- Embeds actual ASCII module and function name strings
- Two char* pointers passed to
resolve() - Plain-text strings visible in the shellcode
- Easier to debug and develop with
- Used by the postex loader (
dfr "resolve" "strings")
Why Not Always Use ror13?
The postex loader uses "strings" mode because it needs to resolve GetModuleHandle and GetProcAddress — two functions whose addresses are patched in via the patch directive from an Aggressor script that already has access to the running Beacon. In this context, the resolver function expects string pointers, not hashes, because it delegates to the real GetProcAddress rather than walking export tables manually.
6. The PIC Transformation
The make pic directive is where the real magic happens. It converts a standard COFF object file — which assumes fixed memory addresses — into a position-independent blob that can execute from any address.
What make pic Actually Does
- Resolve all internal relocations — Function calls between different
.textsections within the same object are patched to use relative offsets - Flatten all sections — The separate
.text,.data, and.rdatasections are merged into a single contiguous byte stream - Convert absolute addresses to RIP-relative — Any remaining absolute address references are rewritten to use x64 RIP-relative addressing so the code works regardless of where it is loaded
- Place
go()at offset 0 — With the+gofirstflag, the entry point function is positioned at the very start of the blob so the caller can simply jump to byte 0 - Optimize — With
+optimize, Crystal Palace removes dead code and minimizes the output size - Randomize function order — With
+disco, Crystal Palace randomizes the order of functions in the PIC blob (the first function is preserved when used with+gofirst)
COFF to PIC Transformation
.text + .data + .rdata
+ relocations + symbols
resolve, flatten,
RIP-relativize
No PE headers
No section table
No import directory
What the Final Output Looks Like
The result of the entire spec pipeline is a raw byte stream with this layout:
- Offset 0 —
go()entry point (executable code begins immediately) - Code body — All functions from loader.c, LibTCG, and LibGate, flattened and relocated
- "dll" section — XOR-encrypted Beacon DLL, prefixed with a 4-byte length
- "key" section — The 128-byte XOR key, prefixed with a 4-byte length
There are no PE headers, no section tables, no import directories, and no relocations left to process. The shellcode finds its own data sections at runtime by using RIP-relative addressing to locate the linked sections after the code body.
This is why Crystal Palace-based loaders evade static analysis tools that look for PE artifacts. The payload is not a PE file at any point during execution — it is raw machine code with embedded encrypted data.
Module 2 Knowledge Check
Q1: What does the dfr directive do in a Crystal Palace spec?
dfr (Dynamic Function Resolution) directive scans the COFF object for all __imp_MODULE$Function symbols and rewrites each reference to call the user-supplied resolver function instead. In "ror13" mode, the module and function names are replaced with their ROR13 hashes. This eliminates all static import references from the PIC shellcode.Q2: What does the +gofirst flag ensure?
+gofirst flag tells Crystal Palace to place the go() entry-point function at byte offset 0 of the final PIC blob. This means the caller can execute the shellcode by simply jumping to the start of the buffer — no need to parse headers or look up an entry point. The other flags (+optimize and +disco) handle size optimization and function order randomization respectively.Q3: What is the difference between "ror13" and "strings" DFR methods?
"ror13" mode, Crystal Palace computes the ROR13 hash of the module name and function name, then passes those two DWORD hash values to the resolver function. In "strings" mode, Crystal Palace embeds the actual ASCII strings in the shellcode and passes pointers to them instead. The ror13 method avoids having readable API names in the shellcode, while the strings method is used when the resolver delegates to GetProcAddress directly (as in the postex loader).