Evadiendo el antivirus mediante Early Bird y Syscalls

Uno de los problemas que se intentan evitar durante el desarrollo de un proyecto Red Team es generar alertas que puedan hacer sospechar al Blue Team que un activo ha sido comprometido. En este tipo de proyectos la discreción es fundamental.

Es probable que, si se levantan suficientes sospechas, el Blue Team actúe y se pierda un punto de entrada ya conseguido. Puede que sea incluso necesario reconstruir toda la infraestructura de ataque (probablemente el Blue Team obtendrá IOCs y marcará como maliciosos las IPs, dominios, artefactos, etc. que se estén utilizando). Incluso tras reconstruir la infraestructura de ataque, puede que resulte muy difícil encontrar otro punto de entrada.

Por este motivo, uno de los aspectos que es necesario tener en cuenta en el desarrollo de los artefactos que se preparen para el ejercicio es la evasión del antivirus. Las técnicas de detección utilizadas por los servicios de antivirus instalados en los equipos de usuario van mejorando mucho con el tiempo, principalmente con la incorporación del análisis en la nube.

No obstante, las técnicas utilizadas por los profesionales que trabajan en la parte ofensiva y, cómo no, también por los atacantes reales, también han mejorado notablemente, obligando a mejorar continuamente las protecciones y formas de detección.

En este artículo se comentan un par de técnicas que se explican en profundidad en los cursos RED TEAM Operator: Malware Development Essentials y Intermediate Course del Sektor7 Institute, cursos muy recomendables ya que tienen un temario variado y son muy prácticos.

El código utilizado en los ejemplos de este artículo está basado en el que se presenta en estos dos cursos.

Early Bird es una técnica de inyección de código que hace uso del mecanismo implementado en Windows para las llamadas asíncronas a procedimientos, también conocido como APC o Asynchronous Procedure Calls. Esta funcionalidad permite la ejecución asíncrona de código en el contexto de un determinado proceso y resulta interesante porque puede ser utilizada para la ejecución de código malicioso antes de que llegue a ejecutarse el punto de entrada del hilo principal del proceso original.

Básicamente, la técnica consiste en crear un proceso legítimo de Windows (CreateProcessA) en un estado suspendido y después reservar espacio en su región de memoria (VirtualAllocEx) y escribir el shellcode que se pretende ejecutar (WriteProcessMemory). Una vez escrito, se registra y encola en dicho proceso una nueva llamada asíncrona a procedimiento (APC) que apunta al shellcode inyectado (QueueUserAPC) y después se solicita la reanudación del proceso (ResumeThread).

En este procedimiento para reanudar la ejecución, el proceso comprueba si tiene alguna llamada en la cola de llamadas APC y, en caso de que haya alguna, la ejecuta antes de comenzar con el procedimiento de inicialización del proceso original. A continuación se puede ver un diagrama en el que se explica el proceso y una sencilla implementación de la prueba de concepto.

Ilustración 1: Diagrama de la inyección del shellcode mediante la técnica Early Bird
Ilustración 2: Prueba de concepto 1

Al compilar y ejecutar esta prueba de concepto se muestra en la consola la dirección de memoria en la que se ha reservado e inyectado el shellcode. En la siguiente ilustración se observa que se crea un nuevo proceso de notepad.exe.

Al inspeccionar la memoria de dicho proceso, justo en la región indicada en la consola tras la ejecución de la prueba de concepto, se puede encontrar el shellcode inyectado, que en este caso corresponde a la versión de 64 bits de la ejecución del binario de calc.exe.

Ilustración 3: Shellcode inyectado en notepad.exe

Muchas soluciones de seguridad basan parte de sus mecanismos de detección en la intercepción de las llamadas a determinadas funciones de la API de Windows. Esto les permite analizar estas llamadas y tratar de decidir si se trata de comportamientos anómalos y potencialmente maliciosos.

Early Bird permite la evasión de la detección de amenazas en el nuevo proceso donde se inyecta el shellcode debido a la ejecución tan temprana de este, ya que se ejecuta antes del procedimiento de inicialización del propio proceso, por lo que estos mecanismos de intercepción que utiliza el antivirus no han podido ser inicializados aún y, por tanto, no están operativos.

No obstante, esto no afecta al binario que lo inyecta, puesto que se trata de un proceso completamente funcional, por lo que el ejecutable puede ser marcado como una amenaza si se interceptan sus llamadas a las funciones sospechosas. Además, los antivirus y otras soluciones de seguridad no se limitan únicamente a monitorizar el uso de determinadas funciones en el momento en el que son llamadas, si no que cuando se carga en memoria el binario preparado para su ejecución, los antivirus también se encargan de consultar su tabla de funciones externas importadas. De esta forma pueden realizar una detección precoz de posibles amenazas incluso antes de que el binario llegue a ser ejecutado.

En el caso de la prueba de concepto, es inmediatamente identificada como una amenaza por parte de Windows Defender. Tal y como se observa en la siguiente ilustración, su tabla de importaciones muestra el uso de bastantes funciones de la API de Windows que son sospechosas y comúnmente utilizadas por binarios maliciosos, lo cual puede influir en una catalogación como potencial amenaza por parte del antivirus.

Ilustración 4: Funciones importadas por la primera prueba de concepto
Ilustración 5: Detección como amenaza de la prueba de concepto

Evitar que el nombre de estas funciones sospechosas aparezca en la tabla de importaciones del ejecutable elimina algunos indicadores que los antivirus utilizan para clasificar un ejecutable como sospechoso. Esto puede conseguirse mediante el uso de la carga dinámica de dichas funciones y de sus respectivas DLLs en tiempo de ejecución.

Además, la carga dinámica de las funciones y sus DLL asociadas también permite utilizar una copia nueva sin alterar de las funciones, por lo que si un antivirus había modificado las DLL cargadas en el proceso para poder interceptar las llamadas a sus funciones, de esta forma se podría evitar también.

Para utilizar las funciones cargadas de forma dinámica, basta con realizar unos cuantos cambios en la prueba de concepto anterior:

  1. Definir nuevos tipos como punteros a funciones de la API de Windows para cada una de las 5 funciones indicadas. Para ello, es necesario hacer uso de la documentación de Microsoft, ya que al realizar la definición de los punteros a cada una de las funciones hay que declarar los mismos parámetros que para la función original.
  2. Instanciar punteros de cada uno de los nuevos tipos definidos para cada una de sus respectivas funciones asociadas. El valor asociado a estos punteros se obtiene mediante las funciones GetProcAddress y GetModuleHandle, indicando el nombre de la función original y el nombre de la DLL en la que se encuentra dicha función. Esta información también puede ser obtenida en la documentación de Microsoft.
  3. Por último, sustituir las llamadas a las funciones originales por los punteros instanciados en el paso 2. De esta forma, el compilador no necesita declarar que necesita importar estas funciones, ya que no se hace referencia a ninguna de ellas, si no que se realiza una carga dinámica “manual” en tiempo de ejecución.

Tras las modificaciones propuestas, el código de la prueba de concepto quedaría de la siguiente forma:

Ilustración 6: Prueba de concepto 2

Se puede observar el resultado del primer paso entre las líneas 4 y 41. Entre las líneas 49 y 53 se encuentran las modificaciones asociadas al segundo paso. Y en las líneas 63, 65, 66, 68 y 72 se puede ver como se utilizan los punteros a las funciones cargadas dinámicamente en vez de las funciones originales.

El resultado se puede ver en la siguiente ilustración. Se observa que en la tabla de importaciones ya no aparecen las funciones indicadas anteriormente y ahora aparecen las dos nuevas utilizadas: GetProcAddress y GetModuleHandle.

Ilustración 7: Funciones importadas por la segunda prueba de concepto

Sin embargo, a pesar de haber revisado y eliminado ciertos indicadores de la tabla de importaciones del ejecutable, y de haber cargado manualmente nuevas versiones de las DLL para evitar utilizar DLL manipuladas por el antivirus, éste sigue “sospechando” del comportamiento del ejecutable y marcándolo como una amenaza. Y es que los mecanismos de detección de los antivirus no se limitan solo a inspeccionar los binarios o a monitorizar la información que se pasa a determinadas funciones de Windows, si no que trabajan a niveles mucho más profundos también.

Para conocer a más bajo nivel cómo funciona la prueba de concepto e identificar otros puntos que el antivirus pueda estar monitorizando, se puede abrir el binario en un debugger, como por ejemplo x64dbg, y seguir la traza de la ejecución. Para simplificar la tarea y que sea más visual, es mejor utilizar la primera versión de la prueba de concepto, sin carga dinámica de funciones.

Una vez cargado el binario y con la ejecución pausada en su EntryPoint, que es la primera instrucción del programa una vez termina su carga por parte del sistema operativo, en primer lugar, estableceremos puntos de interrupción para cada una de las funciones mencionadas anteriormente en la DLL con nombre kernelbase.dll. La siguiente ilustración muestra este proceso para una de las funciones.

Ilustración 8: Interrupciones en las llamadas a las funciones de kernelbase.dll

Una vez establecidos los puntos de interrupción, se puede continuar la ejecución hasta el siguiente punto de interrupción, que será el punto de entrada a cualquiera de las funciones que se han marcado. En ese punto se puede continuar la ejecución paso a paso por instrucción y siguiendo las llamadas a otras funciones, y se podrá comprobar el mismo resultado para todas ellas: la implementación de estas funciones no hace mucho más que preparar los argumentos y llamar a una función de la librería ntdll.dll, que termina haciendo una llamada al kernel.

En la siguiente ilustración se observa la ejecución pausada en el punto de entrada de la función QueueUserAPC, dentro del módulo kernelbase.dll. Esta función termina haciendo una llamada a otra función similar que se encuentra en el módulo ntdll.dll y que, tal y como se puede ver, únicamente selecciona el tipo de llamada al sistema que se requiere y pasa la ejecución al kernel.

Ilustración 9: Implementación de la función QueueUserAPC en kernelbase.dll

¿Qué implicaciones tiene esto? Pues principalmente que los antivirus pueden interceptar las llamadas a las funciones entre los módulos kernelbase.dll y ntdll.dll. Por lo tanto, aunque se puede evitar la intercepción entre el ejecutable que se está desarrollando y las funciones de kernelbase.dll, por ejemplo mediante la carga dinámica de las DLL, estos mecanismos pueden seguir estando presentes a nivel de ntdll.dll antes de pasar la ejecución al kernel.

Dada esta situación, se plantea la posibilidad de realizar las llamadas al sistema directamente desde el código de la prueba de concepto. De esta forma, se evitan los mecanismos de intercepción que puedan haber sido implantados tanto en las funciones del módulo kernelbase.dll como del módulo ntdll.dll. Esto no es una tarea fácil debido a que hay que implementar las diferentes funciones en ensamblador y, además, para cada versión de Windows, el código de llamada al sistema puede ser diferente. Los códigos de las llamadas al sistema para cada versión de Windows se pueden consultar en este recurso.

No obstante, esta solución no es fácil de implementar debido a que se debería tener un conocimiento muy avanzado del entorno objetivo en el que se va a ejecutar el binario. Por suerte, existen proyectos como SysWhispers2 que facilitan esta tarea, ya que proporcionan una implementación en ensamblador que se encarga de identificar la versión de Windows en la que se ejecuta el binario y de elegir el código de llamada al sistema adecuado. Además de la implementación en ensamblador, ofrece también un archivo de cabeceras en C para que las funciones se puedan importar directamente en el código y realizar las llamadas al sistema de forma simplificada.

Utilizando SysWhispers2 se generará el archivo de cabeceras y la implementación en ensamblador para las siguientes funciones: NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, NtQueueApcThread, NtResumeThread. Además, es necesario realizar algunas modificaciones en la prueba de concepto. En este caso, partiendo de la primera versión, hay que importar el nuevo archivo de cabeceras generado con SysWhispers2 y sustituir las llamadas a las funciones originales por las llamadas a las funciones NT que se definen en el archivo de cabeceras. Hay que hacer algunas modificaciones a estas llamadas, ya que los argumentos que se requieren en unas y otras no son exactamente iguales. No obstante, aunque no hay una documentación oficial para las funciones NT, en este recurso se puede encontrar información al respecto.

Así pues, el código de la tercera versión de la prueba de concepto quedaría de la siguiente manera:

Ilustración 10: Prueba de concepto 3

Al ejecutar esta última versión de la prueba de concepto en el debugger, se puede observar que, tras crear un nuevo proceso mediante la llamada a la función CreateProcessA original, se realizan llamadas a 5 funciones contenidas en el propio ejecutable. Estas llamadas, marcadas como puntos de interrupción para poder analizarlas, corresponden a las llamadas a las implementaciones de SysWhispers2 de las funciones NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, NtQueueApcThread y NtResumeThread, respectivamente.

Ilustración 11: Llamada a las funciones de la ntdll.dll imlementadas por SysWhispers en el propio ejecutable

Si analizamos la llamada del primer punto de interrupción, referente a la llamada a la implementación de SysWhispers2 de NtAllocateVirtualMemory, por ejemplo, se pueden observar un gran número de comparaciones y saltos que terminan en la carga de un cierto valor en el registro EAX y la llamada al sistema. La siguiente imagen muestra parte de esta función y se puede comprobar que encaja con el código en ensamblador generado por dicha herramienta. Además, es posible encontrar el identificador de la llamada al sistema en el registro RAX justo antes de la ejecución de la orden syscall.

Ilustración 12: Implementación de SysWhispers de la función NtAllocateVirtualMemory

En este caso, el identificador de la syscall que termina cargado en el registro RAX tiene valor 0x18. Utilizando el recurso indicado anteriormente, en el que se recopilan los diferentes códigos de las llamadas al sistema de Windows, se puede comprobar que el valor presente en el debugger corresponde a la llamada al sistema asociada a la función NtAllocateVirtualMemory.

Ilustración 13: Códigos de syscall de la función NtAllocateVirtualMemory para diferentes versiones de Windows

Esto mismo se puede verificar si analizamos la primera versión de la prueba de concepto. Profundizando en la ejecución de la función VirtualAlloc de la librería kernelbase.dll, se observa que esta llama a la función NtAllocateVirtualMemory de ntdll.dll, la cual termina cargando el valor 0x18 en el registro RAX y llamando al sistema. En la siguiente imagen se muestra la función NtAllocateVirtualMemory original de la ntdll.dll, y se aprecia el código de la llamada al sistema en el registro RAX.

Ilustración 14: Implementación original de la función NtAllocateVirtualMemory en ntdll.dll

Así pues, se ha mostrado como llamar directamente a funciones susceptibles de ser analizadas, evadiendo posibles mecanismos de detección que los antivirus hayan podido registrar para interceptar las llamadas a las funciones de la API de Windows, ya sea al nivel de librerías como kernelbase.dll o kernel32.dll, o incluso un escalón por debajo, como ntdll.dll.

Aunque los antivirus modernos disponen de muchos más mecanismos de detección, estas dos técnicas comentadas en el artículo permiten evadir algunos de ellos a nivel del proceso que inyecta el código malicioso y a nivel del proceso que se crea y ejecutará dicho código.

Referencias