Acaba de ser vectorizado: uso de controladores de excepciones vectoriales (VEH) para la evasión de defensa y la inyección de procesos

Un gerente haciendo una presentación para los gestores de proyectos en la oficina

Autor

Joshua Magri

Senior Managing Security Consultant

Adversary Services, IBM X-Force Red

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í.

¿Qué son los controladores de excepciones vectoriales?

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.

Diagrama de una estructura de lista doblemente enlazada. Comienza con un encabezado de lista que apunta al primer nodo, seguido de dos nodos adicionales. Cada nodo contiene campos etiquetados como Flink, Blink, Reserved, Ref y Pointer to VEH. Las flechas indican enlaces hacia adelante y hacia atrás entre nodos, con el último nodo vinculado al encabezado de la lista.

¿Cómo agrego un controlador de excepciones vectorial?

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.

Fragmento de código que muestra la declaración de función para AddVectoredExceptionHandler. Devuelve un PVOID y toma dos parámetros: ULONG First y PVECTORED_EXCEPTION_HANDLER Handler.

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.

¿Cómo utiliza EDR los controladores de excepciones vectoriales?

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.

Manipulación manual de la lista de VEH

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.

Diagrama de una estructura de lista doblemente enlazada. Comienza con un encabezado de lista que apunta al primer nodo, seguido de dos nodos adicionales. Cada nodo contiene campos etiquetados como Flink y Blink, y los dos últimos nodos también incluyen una sección de datos. Las flechas indican enlaces hacia adelante y hacia atrás entre nodos, y el último nodo enlaza con el encabezado de la lista.

Fuente gráfica: https://www.osronline.com/article.cfm%5Earticle=499.HTM

Cada controlador de excepciones vectorial individual se ve así.

Fragmento de código que muestra una definición de estructura C denominada _VECTXCPT_CALLOUT_ENTRY. Incluye los campos: LIST_ENTRY ListEntry, PVOID ref, int reserved y PVECTORED_EXCEPTION_HANDLER VectoredHandler. El typedef crea VECTXCPT_CALLOUT_ENTRY y un tipo de puntero PVECTXCPT_CALLOUT_ENTRY.

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.

Recorrido por la lista de controladores de excepciones vectoriales

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.

Mixture of Experts | 12 de diciembre, episodio 85

Decodificación de la IA: Resumen semanal de noticias

Únase a nuestro panel de ingenieros, investigadores, responsables de producto y otros profesionales de talla mundial que se abren paso entre el revuelo de la IA para ofrecerle las últimas noticias e insights al respecto.

Inserción de elementos en la lista de controladores de excepciones vectoriales

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.

Diagrama de una lista enlazada con un encabezado de lista, tres nodos de control legítimos y un nodo de control malicioso conectado a 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.

Diagrama de una lista enlazada con un encabezado de lista, un nodo controlador malicioso y tres nodos de controlador legítimos conectados en secuencia.

Con este enfoque, podemos manejar los errores que nos interesan y pasar cualquier otra cosa al siguiente controlador de excepciones.

Abuso de los controladores de excepciones vectoriales para la inyección de procesos

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:

  • Asignar memoria en el proceso remoto
  • Escribir su código en la memoria asignada
  • Proteger la memoria en el proceso remoto para que sea ejecutable
  • Ejecutar su código en el proceso remoto

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:

  1. Identifique la lista de VEH en nuestro proceso local, ya que la dirección será la misma en el proceso remoto.
  2. Asigne/escriba/proteja nuestro shellcode en el proceso remoto con las primitivas de su elección.
  3. Asigne espacio para una nueva estructura de controlador de excepciones vectorial en el proceso remoto.
  4. Llame a EncodeRemotePointer para obtener un puntero codificado para la dirección donde escribió su shellcode.
  5. Asigne espacio para un puntero y un int en el proceso remoto (los necesitamos para los dos atributos reservados de la entrada VEH).
  6. Actualice la nueva entrada de VEH con atributos Flink/Blink válidos, actualice el puntero y actualice los dos atributos reservados para que apunten a la memoria que asignó anteriormente.
  7. Compruebe el bit IsUsingVEH en el bloque de entorno de proceso (PEB) del proceso remoto y configúrelo, si es necesario.
  8. Establezca una trampa PAGE_GUARD en una región de memoria que será ejecutada por el proceso.

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:

  1. Elegir un hilo en el proceso.
  2. Suspender el hilo.
  3. Obtener el contexto del hilo.
  4. Establecer una trampa PAGE_GUARD en el RIP del hilo.
  5. Retomar el hilo.

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.

Otras formas de activar excepciones remotas

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:

  • Para un proceso recién generado y suspendido, establezca el bit beingdebugged en el PEB en verdadero. Cuando se reanude el proceso, se llamará a su controlador de excepciones. Deberá usar un stub de shellcode CreateThread para evitar el bloqueo del cargador.
  • Use una primitiva de ejecución que apunte a una dirección inválida en el proceso. Es posible que esto no active el EDR, ya que la primitiva de ejecución no apunta realmente a la memoria maliciosa.
  • Establezca protecciones de página no ejecutables en una sección .text del proceso remoto y lo deshace después de que se active la excepción.
  • Escriba algunas instrucciones no válidas en una sección .text del proceso remoto.

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.

Nota para Windows Server 2012

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.

Consideraciones de detección

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.

Soluciones relacionadas
IBM Guardium

Proteja sus datos más críticos: descubra, monitoree y proteja la información confidencial en todos los entornos, mientras automatiza el cumplimiento y reduce el riesgo.

Explore IBM Guardium
Soluciones de seguridad empresarial

Transforme su programa de seguridad con las soluciones del mayor proveedor de seguridad empresarial.

    Explore las soluciones de seguridad
    Servicios de Ciberseguridad

    Transforme su negocio y gestione el riesgo con servicios de consultoría de ciberseguridad, nube y seguridad gestionada.

    Explore los servicios de ciberseguridad
    Dé el siguiente paso

    Automatice la protección de datos, la detección de amenazas y el cumplimiento para proteger su empresa en entornos on premises y en la nube.

    1. Explore IBM Guardium
    2. Descubra soluciones de ciberseguridad