Los controladores de excepciones vectoriales (VEH) han recibido mucha atención de la industria de la seguridad ofensiva en los últimos años, pero los VEH se han utilizado en malware durante más de una década. Los VEH proporcionan a los desarrolladores una manera fácil de detectar excepciones y modificar contextos de registro, por lo que, naturalmente, son un objetivo propicio para los desarrolladores de malware. A pesar de toda la atención que han recibido, nadie había publicitado una forma de agregar manualmente un controlador de excepciones vectorial sin depender de las API de Windows integradas que a veces están enganchadas por los productos de detección y respuesta de endpoints (EDR).
En 2015, un usuario de UnKnoWnCheaTsuser publicó fragmentos de código para manipular la lista VEH y, más recientemente, en 2024, un investigador llamado mannyfreddy publicó un blog que detalla cómo funcionan los controladores de excepciones vectoriales. El blog de Mannyfreddy también se refirió a cómo manipular la lista de VEH e incluso exploró cómo usar los controladores de excepciones vectoriales para la inyección remota de procesos.
En 2022, me interesé por los controladores de excepciones vectoriales después de que rad9800 publicara una prueba de conceptopara recorrer la lista de controladores de excepciones vectoriales y llamar a la API removeVectoreExceptionHandler en cada controlador registrado para borrar la lista. Esto me llevó a desarrollar un método para manipular manualmente la lista de VEH y un método para usar VEH para realizar una inyección de procesos sin subprocesos. Dado que la información sobre estas técnicas está comenzando a compartirse públicamente, pensé que era hora de publicar mi investigación en esta área.
En esta publicación, veremos cómo manipular manualmente la lista de controladores de excepciones vectoriales de Windows y cómo se pueden usar los controladores de excepciones vectoriales para evadir defensas y realizar inyección de procesos. Puede encontrar el código que acompaña a esta entrada en el blog aquí.
Los controladores de excepciones vectoriales son un mecanismo de Windows que amplía el manejo estructurado de excepciones (SEH). En resumen, permiten a los desarrolladores registrar una función que se llamará cuando se genere una excepción en un proceso. Esta función recibirá información sobre la excepción y el estado de los registros cuando se produjo la excepción.
Los controladores de excepciones vectoriales se almacenan en una lista y, cuando se genera una excepción, se llamará al primer controlador de excepciones de la lista. Por lo general, escribiría un VEH para buscar tipos de excepciones específicos que prevé manejar. Si se llama a su controlador y el código de error no le interesa, puede indicarle al proceso que siga recorriendo la lista para encontrar un controlador que pueda manejar el error. Si es un error que le gustaría manejar, luego puede hacer lo necesario y decirle al proceso que se manejó el error, y se reanudará la ejecución. Si se recorre toda la lista de VEH y ningún controlador le indica al proceso que continúe ejecutándose, el proceso finalizará.
El gráfico a continuación muestra cómo se ve el VEH. El controlador de excepciones comenzará en el encabezado de la lista y luego recorrerá cada elemento en busca de un controlador adecuado. Si vuelve al encabezado de la lista, el proceso finaliza.
Puede encontrar algunos códigos de ejemplo de Microsoft aquí. En resumen, puede crear un controlador de excepciones vectorial creando una función que tome un puntero a una estructura _EXCEPTION_POINTERS como argumento y luego llame a la API de Windows AddVectoredExceptionHandler para registrar el controlador de excepciones. Los argumentos de la función AddVectoredExceptionHandler se encuentran a continuación.
El primer argumento le indica a la función si debe insertar su nuevo controlador al principio de la lista de controladores de excepciones. Si no lo inserta como primer controlador, se insertará al final de la lista. El segundo argumento es un puntero al controlador de excepciones que se va a llamar.
Tenga en cuenta que, si bien se supone que su función de controlador debe tomar una estructura _EXCEPTION_POINTERS como argumento, en realidad no necesita cumplir con este prototipo si su controlador no necesita ningún argumento. Esto significa que puede tener direcciones de memoria arbitrarias llamadas controladores de excepciones vectoriales. Veremos las implicaciones de esto más adelante.
Algunos productos de ejecución de la detección y respuesta de endpoints (EDR) registrarán sus propios controladores de excepciones vectoriales. Un caso de uso común para esto es colocar trampas PAGE_GUARD en ciertas regiones de la memoria. Cuando se accede a una región de memoria con la protección PAGE_GUARD, generará una excepción, y el producto de EDR puede inspeccionar qué generó la excepción para decidir si es maliciosa o no.
Por ejemplo, shellcode accederá a la tabla de direcciones de exportación (EAT) para Kernel32.dll para resolver direcciones de funciones. Sin embargo, la función legítima GetProcAddress también hace esto. Al colocar una trampa PAGE_GUARD en Kernel32.dll, una EDR puede analizar si el acceso lo realiza o no un módulo legítimo o desde una región de memoria sin respaldo. Si es lo último, es una indicación de posible malware. Yarden Shafir habló de un escenario similar en esta excelente entrada en el blog.
Dado que los proveedores de EDR utilizan controladores de excepciones vectoriales, es mejor para ellos asegurarse de que la lista VEH no se altere. Si pudiera agregar un controlador de excepciones al principio de la lista, simplemente nunca pasaría la ejecución al controlador de EDR. En al menos un producto popular que hemos probado, una llamada a AddVectoredExceptionHandler siempre dará como resultado que VEH se agregue al final de la lista, independientemente de si le indicó a Windows que lo agregue al principio de la lista.
Manténgase al día sobre las tendencias más importantes e intrigantes de la industria sobre IA, automatización, datos y más con el boletín Think. Consulte la Declaración de privacidad de IBM.
Dado que llamar a la API AddVectoredExceptionHandler (que a su vez llama a RtlAddVectoredExceptionHandler) no es una opción, podemos simplemente (una exageración) volver a implementarla.
Como se muestra en el gráfico anterior, la lista de controladores de excepciones vectoriales se almacena como una lista doblemente enlazada. Una lista doblemente enlazada es una estructura de datos en la que cada entrada tiene un puntero a la siguiente entrada, un puntero a la entrada anterior y luego algunos datos. En este caso, los datos son otra estructura que contiene información para el controlador de excepciones vectorial.
Fuente gráfica: https://www.osronline.com/article.cfm%5Earticle=499.HTM
Cada controlador de excepciones vectorial individual se ve así.
El elemento LIST_ENTRY contiene nuestros punteros Flink/Blink, un contador de referencia, un valor reservado que realmente no importa y, por último, un puntero a la función que debe llamarse. Excepto que este puntero no es realmente un puntero, sino más bien un puntero codificado. Los punteros se pueden codificar/decodificar utilizando las funciones EncodePointer/DecodePointer de la API de Windows.
Hay dos métodos para localizar la lista de controladores de excepciones vectoriales. Uno se basa en el uso de heurísticas, como identificar una función que haga referencia a la variable LdrpVectorHandlerList y leer los bytes para encontrar la dirección. El segundo método es registrar un nuevo controlador de excepciones vectorial y recorrer la lista doblemente vinculada hasta que identifiquemos un puntero a la sección .data de NTDLL, que debería ser el encabezado de la lista enlazada. Este último es el método documentado por rad9800, y el método que prefiero, ya que no tenemos que preocuparnos por las compensaciones o los patrones de bytes que cambian entre las versiones de Windows.
Una vez que hayamos identificado el encabezado de la lista de controladores de excepciones vectoriales, podemos comenzar a manipularlo. Podríamos simplemente secuestrar la lista VEH apuntando las entradas Flink y Blink del List Head hacia nuestro nuevo controlador de excepciones, que se muestra a continuación. Esto dará como resultado que nuestro VEH sea la única entrada en la lista.
El peligro de este enfoque es que si se genera una excepción que su controlador de excepciones no puede manejar, su proceso finalizará. Los procesos legítimos también utilizan controladores de excepciones vectoriales para detectar errores que esperan que se produzcan, por lo que acortar la lista probablemente no sea el mejor enfoque. En su lugar, podemos actualizar correctamente la lista para insertar primero nuestro controlador de excepciones.
Con este enfoque, podemos manejar los errores que nos interesan y pasar cualquier otra cosa al siguiente controlador de excepciones.
Como hemos visto, implementar nuestra propia versión de la API AddVectoredExceptionHandler no es demasiado complicado. Pero lo que es más importante, en realidad no requería que interactuáramos con el kernel, aparte de llamar a NtProtectVirtualMemory para cambiar las protecciones de memoria en la sección .mrdata de NTDLL. Dado que toda la información que utiliza el proceso al llamar a los controladores de excepciones vectoriales se almacena dentro del proceso, presenta un gran objetivo como técnica de inyección de procesos sin subprocesos.
¿Qué es la inyección de proceso threadless? Ceri Coburn lo abordó en su charla de 2023 en Bsides Cymru, “Needles Without the Thread”. Curiosamente, esta charla salió justo antes de que yo estuviera a punto de dar una charla en una conferencia interna de IBM demostrando mi nueva técnica de inyección que no requería una primitiva de ejecución.
En resumen, las técnicas tradicionales de inyección de procesos requieren una forma de:
Podemos mezclar y combinar estas primitivas para obtener diferentes técnicas, y algunas técnicas no necesitan todos los pasos. Por ejemplo, si asigna memoria en el proceso remoto como RWX, no necesita cambiar la protección más adelante. O si llama a NtMapViewOfSection, su memoria se asigna y escribe en el proceso remoto en el mismo paso. Pero lo único que requieren todas las técnicas tradicionales de inyección de procesos es una primitiva para la ejecución. Suele ser CreateRemoteThread/QueueUserAPC/SetThreadContext (o sus equivalentes de función Nt). Como resultado, estas primitivas de ejecución son objeto de un intenso escrutinio por parte de los productos de seguridad para detectar usos maliciosos. Llamar a una primitiva de ejecución dirigida a la memoria sin respaldo en un proceso remoto es una excelente manera de atrapar su baliza.
Entonces, ¿qué tal si nos saltamos la primitiva de ejecución por completo? Con los controladores de excepciones vectoriales, funciona de la siguiente manera:
El último paso es el crítico que nos permite eludir la necesidad de una primitiva de ejecución activando una excepción en el proceso remoto. Hay varias formas de hacerlo, pero, en mi opinión, la mejor es utilizar una trampa PAGE_GUARD. Implementé técnicas de inyección para procesos nuevos y existentes utilizando trampas PAGE_GUARD.
Si está generando un nuevo proceso, puede generarlo en estado suspendido y establecer una trampa en el punto de entrada del proceso. Por lo general, generar un proceso en estado suspendido y manipularlo hará que se le identifique como comportamiento de vaciado de procesos. Sin embargo, dado que no estamos escribiendo en ninguna sección .text o usando cualquier primitiva de ejecución, no deberíamos vernos afectados por esta detección. Pero como siempre, pruebe esto en su laboratorio.
Inyectar en un proceso en ejecución es un poco más complicado, pero he encontrado que la forma más fácil es:
Esta técnica puede ser un poco inestable si está ejecutando shellcode directamente, ya que secuestra el hilo, lo que puede bloquear el proceso. Me ha parecido más confiable agregar algo de shellcode bootstrap que implemente un controlador de excepciones vectorizado adecuado que crea un nuevo hilo para su shellcode y luego devuelve la ejecución del código al hilo como de manera normal. Esta creación de hilos locales no estará sujeta al mismo escrutinio que una creación de hilos remotos.
La última consideración para cualquiera de las dos técnicas es que cada vez que se produzca un error en el proceso, se llamará a su VEH y se ejecutará su shellcode. Esto puede dar lugar a la creación de muchas balizas en un solo proceso y, en última instancia, a bloquearlo. Descubrí que la solución a este problema es el shellcode de arranque mencionado anteriormente, que puede verificar para garantizar que la excepción sea una trampa PAGE_GUARD, o eliminar su controlador de excepciones vectorial de su baliza recién generada. Esto se puede hacer ejecutando un BOF para recorrer la lista de VEH, identificar su controlador (un puntero codificado a la memoria sin respaldo) y eliminarlo mediante manipulación manual, o simplemente llamar a RemoveVectoredExceptionHandler en él.
Creo que las trampas PAGE_GUARD son el mejor método para generar excepciones remotas, ya que es una llamada NtProtectVirtualMemory muy sencilla, la trampa se elimina después de generar la excepción y no requiere una primitiva de escritura o ejecución. Sin embargo, hay otras formas de provocar una excepción remota, por el simple hecho de variar:
No piense que ninguna de estas son ideas particularmente buenas (excepto quizás la primera, que probé con éxito), pero el punto es que no necesariamente necesita usar una trampa PAGE_GUARD.
Como siempre, Windows Server 2012 no funciona bien con las técnicas descritas anteriormente, pero no es demasiado difícil hacerlo funcionar. En Windows Server 2012, a la estructura de VEH le falta una de las dos entradas reservadas que se encuentran en otras versiones de Windows. Además, la lista de VEH no está en la sección .mrdata, pero en la sección .data.
La detección de la manipulación de VEH se puede realizar utilizando las mismas técnicas descritas en esta publicación para recorrer la lista de VEH. Los productos de seguridad que utilizan VEH suelen estar configurados para garantizar que sean la primera entrada en el VEH. Si este no es el caso, es posible que haya ocurrido algo malicioso. Sin embargo, esto puede causar problemas si dos productos se ejecutan en paralelo y ambos esperan ser la primera entrada en la lista.
NCC Group realizó una excelente investigación sobre la enumeración de los controladores de excepciones vectoriales en todos los procesos y la identificación de cualquier controlador que apunte a la memoria sin respaldo. Como siempre, la memoria ejecutable sin respaldo es un indicador bastante bueno de comportamiento malicioso. Event Tracing for Windows Threat Intelligence (ETWTi) también se puede utilizar para identificar la asignación, escritura y protección de shellcode en memoria sin respaldo. Del mismo modo, los eventos ETWTi para escrituras de memoria remota en la sección .mrdata de un proceso deben ser un indicador de señal alta/ruido bajo.