En esta publicación, los hackers ofensivos de IBM® Security X-Force Red analizan cómo los atacantes, con privilegios elevados, pueden utilizar su acceso para poner en escena capacidades de posexplotación del kernel de Windows. En los últimos años, las cuentas públicas han demostrado cada vez más que los atacantes menos sofisticados están utilizando esta técnica para lograr sus objetivos. Por lo tanto, es importante que pongamos el foco en esta capacidad y aprendamos más sobre su impacto potencial. En concreto, en esta publicación evaluaremos cómo se puede utilizar la posexplotación del kernel para cegar los sensores ETW y relacionaremos esto con las muestras de malware identificadas en el mundo real el año pasado.
Con el tiempo, las medidas de seguridad y la telemetría de detección en Windows han mejorado sustancialmente. Cuando estas capacidades se combinan con soluciones de detección y respuesta de endpoints (EDR) bien configuradas, pueden representar una barrera nada desdeñable para la posexplotación. Los atacantes se enfrentan a un coste constante para desarrollar e iterar tácticas, técnicas y procedimientos (TTP) para evitar la detección heurística. En el equipo de simulación de adversarios de IBM Security X-Force, nos enfrentamos al mismo problema. Nuestro equipo tiene la tarea de simular capacidades de amenazas avanzadas en algunos de los entornos más grandes y reforzados. La combinación de soluciones de seguridad complejas y bien ajustadas con equipos de centros de operaciones de seguridad (SOC) bien entrenados puede ser muy exigente para las técnicas de espionaje. En algunos casos, el uso de una TTP específica queda completamente obsoleto en un plazo de tres a cuatro meses (normalmente vinculado a pilas tecnológicas específicas).
Los atacantes pueden optar por aprovechar la ejecución de código en el kernel de Windows para manipular algunas de estas protecciones o para evitar por completo una serie de sensores del espacio de usuario. La primera demostración publicada de esta capacidad fue en 1999 en la revista Phrack Magazine. En los años transcurridos desde entonces, se han registrado varios casos en los que los actores de amenazas (TA) han utilizado rootkits del kernel para la posexplotación. Algunos ejemplos antiguos son la familia Derusbi y el Lamberts Toolkit.
Tradicionalmente, este tipo de capacidades se han limitado en su mayoría a los TA avanzados. Sin embargo, en los últimos años, hemos visto cómo más atacantes comunes utilizan primitivas de explotación Bring Your Own Vulnerable Driver (BYOVD) para facilitar las acciones en los endpoints. En algunos casos, estas técnicas han sido bastante primitivas, limitadas a tareas sencillas, pero también ha habido demostraciones más potentes.
A finales de septiembre de 2022, investigadores de ESET publicaron un informe técnico sobre dicha capacidad del kernel utilizada por el TA Lazarus en varios ataques contra entidades en Bélgica y los Países Bajos con fines de exfiltración de datos. Este artículo expone una serie de primitivas de manipulación directa de objetos del kernel (DKOM) que la carga útil utiliza para cegar la telemetría del sistema operativo, el antivirus y el EDR. Las investigaciones públicas disponibles sobre estas técnicas son escasas. Para la defensa, resulta crítico comprender más a fondo las técnicas de posexplotación del kernel. Un argumento clásico y simplista que se oye a menudo es que un atacante con privilegios elevados puede hacer cualquier cosa, así que ¿por qué deberían modelarse las capacidades en ese escenario? Se trata de una postura débil. Los defensores deben comprender qué capacidades tiene un atacante cuando sus privilegios son elevados, qué fuentes de datos siguen siendo fiables (y cuáles no), qué opciones de contención existen y cómo se pueden detectar las técnicas avanzadas (incluso si no existen capacidades para realizar esas detecciones). En esta publicación me centraré específicamente en parchear las estructuras del Kernel Event Tracing for Windows (ETW) para que los proveedores sean ineficaces o inoperables. Proporcionaré algunos antecedentes sobre esta técnica, analizaré cómo un atacante puede manipular las estructuras Kernel ETW y profundizaré en algunos de los mecanismos para encontrar estas estructuras. Por último, revisaré cómo Lazarus implementó esta técnica en su carga útil.
Boletín del sector
Manténgase al día sobre las tendencias más importantes e intrigantes del sector en materia de IA, automatización, datos y mucho más con el boletín Think. Consulte la Declaración de privacidad de IBM.
Su suscripción se enviará en inglés. Encontrará un enlace para darse de baja en cada boletín. Puede gestionar sus suscripciones o darse de baja aquí. Consulte nuestra Declaración de privacidad de IBM para obtener más información.
ETW es una función de seguimiento de alta velocidad integrada en el sistema operativo Windows. Permite el registro de eventos y actividades del sistema por parte de aplicaciones, controladores y el sistema operativo, lo que proporciona una visibilidad detallada del comportamiento del sistema para la depuración, el análisis del rendimiento y el diagnóstico de seguridad.
En esta sección, ofreceré una descripción general de alto nivel de Kernel ETW y su superficie de ataque asociada. Esto será útil para comprender mejor los mecanismos implicados en la manipulación de los proveedores ETW y los efectos asociados a dichas manipulaciones.
En esta publicación, nos centramos en la superficie de ataque del espacio del kernel.
Esta publicación solo tiene en cuenta los ataques de la primera categoría de ataques que se muestra en la “Figura 2”, en la que el seguimiento está desactivado o alterado de alguna manera.
Como precaución, al considerar estructuras opacas en Windows, siempre es importante recordar que estas están sujetas a cambios y, de hecho, cambian con frecuencia entre las distintas versiones de Windows. Esto es especialmente importante al manipular datos del kernel, ya que los errores probablemente provocarán una pantalla azul de la muerte (BSoD). ¡Tenga cuidado!
Los proveedores del kernel se registran utilizando nt!EtwRegister, una función exportada por ntoskrnl. A continuación se muestra una versión descompilada de la función.
La inicialización completa se produce dentro de la función interna EtwpRegisterKMProvider, pero hay dos conclusiones principales que extraer aquí:
Enumeremos brevemente las estructuras que Binarly destacó en su diapositiva de la Figura 2.
A continuación se muestra un listado completo de 64 bits de la estructura _ETW_REG_ENTRY. Se pueden encontrar más detalles en el blog de Geoff Chappell aquí. Esta estructura también se puede explorar más a fondo en el Vergilius Project.
// 0x70 bytes (sizeof)
// Win11 22H2 10.0.22621.382
struct _ETW_REG_ENTRY
{
struct _LIST_ENTRY RegList; //0x0
struct _LIST_ENTRY GroupRegList; //0x10
struct _ETW_GUID_ENTRY* GuidEntry; //0x20
struct _ETW_GUID_ENTRY* GroupEntry; //0x28
union
{
struct _ETW_REPLY_QUEUE* ReplyQueue; //0x30
struct _ETW_QUEUE_ENTRY* ReplySlot[4]; //0x30
struct
{
VOID* Caller; //0x30
ULONG SessionId; //0x38
};
};
union
{
struct _EPROCESS* Process; //0x50
VOID* CallbackContext; //0x50
};
VOID* Callback; //0x58
USHORT Index; //0x60
union
{
USHORT Flags; //0x62
struct
{
USHORT DbgKernelRegistration:1; //0x62
USHORT DbgUserRegistration:1; //0x62
USHORT DbgReplyRegistration:1; //0x62
USHORT DbgClassicRegistration:1; //0x62
USHORT DbgSessionSpaceRegistration:1; //0x62
USHORT DbgModernRegistration:1; //0x62
USHORT DbgClosed:1; //0x62
USHORT DbgInserted:1; //0x62
USHORT DbgWow64:1; //0x62
USHORT DbgUseDescriptorType:1; //0x62
USHORT DbgDropProviderTraits:1; //0x62
};
};
UCHAR EnableMask; //0x64
UCHAR GroupEnableMask; //0x65
UCHAR HostEnableMask; //0x66
UCHAR HostGroupEnableMask; //0x67
struct _ETW_PROVIDER_TRAITS* Traits; //0x68
};
Una de las entradas anidadas dentro de _ETW_REG_ENTRY es GuidEntry, que es una estructura _ETW_GUID_ENTRY. Puede encontrar más información sobre esta estructura no documentada en el blog de Geoff Chappell aquí y en el Vergilius Project.
// 0x1a8 bytes (sizeof)
// Win11 22H2 10.0.22621.382
struct _ETW_GUID_ENTRY
{
struct _LIST_ENTRY GuidList; //0x0
struct _LIST_ENTRY SiloGuidList; //0x10
volatile LONGLONG RefCount; //0x20
struct _GUID Guid; //0x28
struct _LIST_ENTRY RegListHead; //0x38
VOID* SecurityDescriptor; //0x48
union
{
struct _ETW_LAST_ENABLE_INFO LastEnable; //0x50
ULONGLONG MatchId; //0x50
};
struct _TRACE_ENABLE_INFO ProviderEnableInfo; //0x60
struct _TRACE_ENABLE_INFO EnableInfo[8]; //0x80
struct _ETW_FILTER_HEADER* FilterData; //0x180
struct _ETW_SILODRIVERSTATE* SiloState; //0x188
struct _ETW_GUID_ENTRY* HostEntry; //0x190
struct _EX_PUSH_LOCK Lock; //0x198
struct _ETHREAD* LockOwner; //0x1a0
};
Por último, una de las entradas anidadas dentro de _ETW_GUID_ENTRY es ProviderEnableInfo, que es una estructura _TRACE_ENABLE_INFO. Para más información sobre los elementos de esta estructura de datos, puedes consultar la documentación oficial de Microsoft y el Vergilius Project. La configuración de esta estructura afecta directamente al funcionamiento y las capacidades del proveedor.
// 0x20 bytes (sizeof)
// Win11 22H2 10.0.22621.382
struct _TRACE_ENABLE_INFO
{
ULONG IsEnabled; //0x0
UCHAR Level; //0x4
UCHAR Reserved1; //0x5
USHORT LoggerId; //0x6
ULONG EnableProperty; //0x8
ULONG Reserved2; //0xc
ULONGLONG MatchAnyKeyword; //0x10
ULONGLONG MatchAllKeyword; //0x18
};
Aunque es bueno tener algunos conocimientos teóricos, siempre es mejor ver ejemplos concretos de uso para comprender mejor un tema. Veamos brevemente un ejemplo. La mayoría de los proveedores ETW del kernel críticos se inicializan dentro de nt!EtwpInitialize, que no se exporta. Al examinar esta función, se observan unos quince proveedores.
Si tomamos como ejemplo la entrada Microsoft-Windows-Threat-Intelligence (EtwTi), podemos comprobar el parámetro global ThreatIntProviderGuid para recuperar el GUID de este proveedor.
Al buscar este GUID en línea, se verá inmediatamente que hemos podido recuperar el valor correcto (f4e1897c-bb5d-5668-f1d8-040f4d8dd344).
Veamos un caso en el que se utiliza el parámetro de identificador de registro, EtwThreatIntProvRegHandle, y analicemos cómo se utiliza. Un lugar en el que se hace referencia al identificador es nt!EtwTiLogDriverObjectUnLoad. Por el nombre de esta función, podemos intuir que su finalidad es generar eventos cuando el núcleo descarga un objeto controlador.
Las funciones nt!EtwEventEnabled y nt!EtwProviderEnabled se invocan aquí pasando el identificador de registro como uno de los argumentos. Veamos una de estas subfunciones para comprender mejor lo que está sucediendo.
Es cierto que esto es un poco difícil de seguir. Sin embargo, la aritmética de punteros no es especialmente importante. En su lugar, centrémonos en cómo esta función procesa el identificador de registro. Parece que la función valida una serie de propiedades de la estructura _ETW_REG_ENTRY y sus subestructuras, como la propiedad GuidEntry.
struct _ETW_REG_ENTRY
{
…
struct _ETW_GUID_ENTRY* GuidEntry; //0x20
…
}
Y la propiedad GuidEntry->ProviderEnableInfo .
struct _ETW_GUID_ENTRY
{
…
struct _TRACE_ENABLE_INFO ProviderEnableInfo; //0x60
…
}
A continuación, la función realiza comprobaciones similares basadas en niveles. Por último, la función devuelve verdadero o falso para indicar si un proveedor está habilitado para el registro de eventos en un nivel y una palabra clave específicos. Puede encontrar más detalles en la documentación oficial de Microsoft.
Podemos ver que, cuando se accede a un proveedor a través de su identificador de registro, la integridad de esas estructuras se vuelve muy importante para el funcionamiento del proveedor. Por el contrario, si un atacante pudiera manipular esas estructuras, podría influir en el flujo de control de la persona que realiza la llamada para eliminar o impedir que se registren los eventos.
Echando un vistazo a la superficie de ataque declarada por Binarly y basándonos en nuestro ligero análisis, podemos plantear algunas estrategias para interrumpir la recopilación de eventos.
Ahora tenemos una buena idea de cómo sería un ataque de DKOM a ETW. Supongamos que el atacante tiene una vulnerabilidad que otorga una primitiva de lectura/escritura del kernel, como lo hace el malware Lazarus en este caso al cargar un controlador vulnerable. Lo que falta es una manera de encontrar estos identificadores de registro.
Voy a exponer dos técnicas principales para encontrar estos identificadores y mostrar la variante de uno que utiliza Lazarus en su carga útil del Kernel.
En primer lugar, puede ser prudente explicar que, si bien existe Kernel ASLR, este no es un límite de seguridad para los atacantes locales si pueden ejecutar código en MedIL o superior, ya que este no es un límite de seguridad. Hay muchas formas de filtrar los punteros del kernel que solo están restringidas en escenarios de entorno aislado o LowIL. Para añadir un poco de contexto, puede echar un vistazo a I Got 99 Problems But a Kernel Pointer Ain't One de Alex Ionescu; muchas de estas técnicas siguen siendo aplicables hoy en día.
La herramienta elegida aquí es ntdll!NtQuerySystemInformation con la clase SystemModuleInformation:
internal static UInt32 SystemModuleInformation = 0xB;
[DllImport(“ntdll.dll”)]
internal static extern UInt32 NtQuerySystemInformation(
UInt32 SystemInformationClass,
IntPtr SystemInformation,
UInt32 SystemInformationLength,
ref UInt32 ReturnLength);
Esta función devuelve la dirección base activa de todos los módulos cargados en el espacio del kernel. En ese momento, es posible analizar esos módulos en el disco y convertir los offsets brutos de los archivos en direcciones virtuales relativas y viceversa.
public static UInt64 RvaToFileOffset(UInt64 rva, List<SearchTypeData.IMAGE_SECTION_HEADER> sections)
{
foreach (SearchTypeData.IMAGE_SECTION_HEADER section in sections)
{
if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize)
{
return (rva – section.VirtualAddress + section.PtrToRawData);
}
}
return 0;
}
public static UInt64 FileOffsetToRVA(UInt64 fileOffset, List<SearchTypeData.IMAGE_SECTION_HEADER> sections)
{
foreach (SearchTypeData.IMAGE_SECTION_HEADER section in sections)
{
if (fileOffset >= section.PtrToRawData && fileOffset < (section.PtrToRawData + section.SizeOfRawData))
{
return (fileOffset – section.PtrToRawData) + section.VirtualAddress;
}
}
return 0;
}
Un atacante también puede cargar estos módulos en su proceso de usuario mediante llamadas API de biblioteca de carga estándar (por ejemplo, ntdll!LdrLoadDll). Hacerlo evitaría las complicaciones de convertir las compensaciones de archivos a RVA y viceversa. Sin embargo, desde el punto de vista de la seguridad operativa (OpSec), esto no es lo ideal, ya que puede generar más telemetría de detección.
Siempre que sea posible, esta es la técnica que prefiero porque hace que las filtraciones sean más portátiles entre las versiones del módulo porque se ven menos afectadas por los cambios en los parches. La desventaja es que depende de que existan cadenas de gadgets para el objeto que quiere filtrar.
Teniendo en cuenta los identificadores de registro ETW, tomemos Microsoft-Windows-Threat-Intelligence como ejemplo. A continuación puede ver la llamada completa a nt!EtwRegister.
Aquí queremos filtrar el puntero al identificador de registro, EtwThreatIntProvRegHandle. Como se ve cargado en param_4 en la primera línea de la Figura 8. Este puntero se resuelve en un global dentro de la sección .data del módulo Kernel. Dado que esta llamada ocurre en una función no exportada, no podemos filtrar su dirección directamente. En vez de eso, tenemos que ver dónde se hace referencia a esta global y ver si se usa en una función cuya dirección pueda filtrarse.
Explorar algunas de estas entradas revela rápidamente un candidato en nt!KeInsertQueueApc.
Este es un gran candidato por varias razones:
Al observar el ensamblado, se muestra la siguiente disposición.
La filtración de este identificador de registro se vuelve entonces sencilla. Leemos una serie de bytes utilizando nuestra vulnerabilidad y buscamos la primera instrucción mov R10 para calcular el desfase virtual relativo de la variable global. El cálculo sería algo así:
Int32 pOffset = Marshal.ReadInt32((IntPtr)(pBuff.ToInt64() + i + 3));
hEtwTi = (IntPtr)(pOffset + i + 7 + oKeInsertQueueApc.pAddress.ToInt64());
Con el identificador de registro, es posible acceder a la estructura de datos _ETW_REG_ENTRY.
En general, estas cadenas de gadgets pueden utilizarse para filtrar diversas estructuras de datos del kernel. Sin embargo, vale la pena señalar que no siempre es posible encontrar tales cadenas de gadgets y, a veces, las cadenas de gadgets pueden tener múltiples etapas complejas. Por ejemplo, una posible cadena de gadgets para filtrar constantes de entrada de directorio de páginas (PDE) podría tener este aspecto.
MmUnloadSystemImage -> MiUnloadSystemImage -> MiGetPdeAddress
De hecho, un análisis superficial de los identificadores de registro de ETW reveló que la mayoría no tiene cadenas de gadgets adecuadas que puedan utilizarse como se describe anteriormente.
La otra opción principal para filtrar estos identificadores de registro ETW es usar escaneo de memoria, ya sea desde memoria activa del kernel o desde un módulo en disco. Recuerde que al escanear módulos en disco es posible convertir desplazamientos de archivo a RVAs.
Este enfoque consiste en identificar patrones únicos de bytes, escanear esos patrones y, finalmente, realizar algunas operaciones en los desplazamientos de la coincidencia del patrón. Echemos otro vistazo a nt!EtwpInitialize para entenderlo mejor:
Las quince llamadas a nt!EtwRegister se agrupan en su mayoría en esta función. La estrategia principal aquí es encontrar un patrón único que aparezca antes de la primera llamada a nt!EtwRegister y un segundo patrón que aparezca después de la última llamada a nt!EtwRegister. Esto no es demasiado complejo. Un truco que se puede utilizar para mejorar la portabilidad es crear un escáner de patrones que pueda gestionar cadenas de bytes comodín. Esta es una tarea que se deja al lector.
Una vez identificado un índice de inicio y de parada, es posible consultar todas las instrucciones intermedias.
Una vez que se han encontrado todas las instrucciones CALL , es posible buscar hacia atrás y extraer los argumentos de la función, primero el GUID que identifica al proveedor ETW y segundo, la dirección del identificador de registro. Con esta información en la mano, podemos realizar ataques de DKOM informados contra las cuentas de registro para afectar la operación de los proveedores identificados.
Obtuve una muestra del DLL del FudModle mencionado en el informe técnico de ESET y lo analicé. Este DLL carga un controlador Dell vulnerable firmado (a partir de un recurso codificado XOR en línea) y, a continuación, dirige el controlador para parchear muchas estructuras del kernel con el fin de limitar la telemetría en el host.
Como parte final de esta publicación, quiero revisar la estrategia que utiliza Lazarus para encontrar los identificadores de registro de Kernel ETW. Se trata de una variación del método de escaneado que comentamos anteriormente.
Al inicio de la función de búsqueda, Lazarus resuelve nt!EtwRegister y utiliza esta dirección para iniciar el escaneo
Esta decisión es un poco extraña porque depende de dónde existe esa función en relación con dónde se llama a la función. La posición relativa de una función en un módulo puede variar de una versión a otra, ya que puede introducirse, eliminarse o modificarse código nuevo. Sin embargo, debido a la forma en que se compilan los módulos, se espera que las funciones mantengan un orden relativamente estable. Se supone que se trata de una optimización de la velocidad de búsqueda.
Al buscar referencias a nt!EtwRegister en ntoskrnl parece que no se pasan por alto muchas entradas utilizando esta técnica. Puede que Lazarus también haya realizado análisis adicionales para determinar que las entradas omitidas no son importantes o no es necesario parchearlas. Las entradas perdidas se destacan a continuación. El empleo de esta estrategia permite a Lazarus omitir 0x7b1de0 bytes mientras realiza el escaneo, lo que puede ser una cantidad no trivial si el escáner es lento.
Además, al iniciar el escaneo, se saltan las cinco primeras coincidencias antes de empezar a registrar los identificadores de registro. A continuación se muestra parte de la función de búsqueda.
El código es un poco obtuso, pero obtenemos los aspectos más destacados de la trama. El código busca llamadas a nt!EtwRegister, extrae el identificador de registro, convierte este identificador en la dirección activa mediante una omisión de KASLR y almacena el puntero en una matriz reservada para este fin dentro de una estructura de configuración de malware (asignada en la inicialización).
Por último, veamos qué hace Lazarus para desactivar estos proveedores.
En general, esto tiene sentido, lo que Lazarus hace aquí es filtrar la variable global que vimos antes y luego sobrescribir el puntero en esa dirección con NULL. Esto borra de forma efectiva la referencia a la estructura de datos _ETW_REG_ENTRY, si existe.
No estoy del todo satisfecho con la técnica mostrada por varias razones:
Volví a implementar esta técnica para la ciencia; sin embargo, hice algunos ajustes en la técnica.
En general, después de los ajustes, la técnica anterior es claramente la mejor manera de realizar este tipo de enumeración. Dado que el tiempo de búsqueda es insignificante con algoritmos optimizados, tiene sentido escanear todo el módulo en el disco y luego usar alguna lógica adicional posterior al escaneo para filtrar los resultados.
Es prudente evaluar brevemente el impacto que podría tener un ataque de este tipo. Cuando los datos de los proveedores se reducen o eliminan por completo, se produce una pérdida de información, pero al mismo tiempo no todos los proveedores señalan eventos sensibles a la seguridad.
Sin embargo, algunos subgrupos de estos proveedores son sensibles a la seguridad. El ejemplo más obvio de esto es Microsoft-Windows-Threat-Intelligence (EtwTi), que es una fuente de datos central para Microsoft Defender Advanced Threat Protection (MDATP), que ahora se llama Defender for Endpoint (todo es muy confuso). Cabe señalar que el acceso a este proveedor está muy restringido, solo los controladores Early Launch Anti Malware (ELAM) pueden registrarse en este proveedor. Del mismo modo, los procesos de espacio de usuario que reciban estos eventos deben tener un estado protegido (ProtectedLight / Antimalware) y estar firmados con el mismo certificado que el controlador ELAM.
Con EtwExplorer es posible hacerse una mejor idea de qué tipo de información puede enviar este proveedor.
El manifiesto XML es demasiado grande para incluirlo aquí en su totalidad, pero a continuación se muestra un evento para dar una idea de los tipos de datos que se pueden suprimir mediante DKOM.
El kernel ha sido y sigue siendo un área importante y controvertida en la que Microsoft y los proveedores externos deben esforzarse por salvaguardar la integridad del sistema operativo. La corrupción de datos en el kernel no solo es una característica de la posexplotación, sino también un componente central en el desarrollo de exploits del kernel. Microsoft ya ha hecho muchos avances en esta área con la introducción de seguridad basada en virtualización (VBS) y uno de sus componentes como Kernel Data Protection (KDP).
Los consumidores del sistema operativo Windows, a su vez, deben asegurarse de que se benefician de estos avances para imponer el mayor coste posible a los posibles atacantes. El controlde aplicaciones de Windows Defender (WDAC) se puede utilizar para garantizar que las protecciones de VBS estén implementadas y que existan políticas que prohíban la carga de controladores potencialmente peligrosos.
Estos esfuerzos son aún más importantes a medida que vemos cada vez más cómo los TA de productos básicos aprovechan los ataques BYOVD para realizar DKOM en el espacio del kernel.
Obtenga más información sobre X-Force Red aquí. Programe una consulta sin coste con X-Force aquí.