Sophos News

Atacar el Control Flow Flattening de Emotet

Emotet ha sido uno de los servicios de ciberdelincuencia más profesionales y duraderos del panorama de las amenazas. Famoso desde poco después de su debut en 2014, el botnet fue interrumpido en enero de 2021 gracias a un esfuerzo multinacional de las fuerzas del orden que dejó de lado su actividad durante casi un año. Por desgracia, en noviembre de 2021 la red de bots resurgió y volvió a aparecer en el radar de Sophos.

Para proteger a nuestros clientes, SophosLabs siempre busca las técnicas, tácticas y procedimientos más significativos utilizados para distribuir y entregar Emotet. En este artículo, analizaremos el Control Flow Flattening (CFF), una de las diversas tácticas de ofuscación que utilizan los desarrolladores de Emotet para dificultar la detección y la ingeniería inversa de la carga útil del malware. Daremos un breve ejemplo de CFF aplicado a un simple programa de hola-mundo, y luego discutiremos cómo los investigadores de Sophos abordan el CFF en el código de Emotet. Terminaremos resumiendo los retos y problemas que hemos encontrado durante la investigación.

Los aspectos internos de Emotet han sido tratados por muchos investigadores, pero hasta ahora no hemos visto discusiones sobre su uso del Control Flow Flattening.

Emotet: Resurgimiento y tenacidad

La figura 1 muestra el volumen de cargas útiles de Emotet detectadas en nuestros sistemas de sandbox en el primer trimestre de 2022. Como muestra el gráfico, recibimos múltiples envíos de Emotet a diario; creemos que los picos recurrentes más grandes son el resultado de campañas a gran escala puestas en marcha por los distribuidores del malware. Esta es una suposición sensata; Emotet se distribuye principalmente a través de spam de correo electrónico, y más correos electrónicos maliciosos naturalmente conducen a más envíos al sandbox.

Figura 1: Cronología de las detecciones de Emotet en 2022 en los sistemas sandbox de SophosLabs

Además del mecanismo de entrega de Emotet y su prevalencia, también analizamos en profundidad la carga útil final. Así, observamos el Control Flow Flattening en una muestra de Emotet sin empaquetar.  El CFF oculta el flujo del programa poniendo todos los bloques de función uno al lado del otro. Es una conocida técnica de ofuscación utilizada para ocultar el propósito del software. Aunque extraer el código original de un binario aplanado es intrínsecamente difícil, hemos adaptado con éxito algunos conjuntos de herramientas existentes para desofuscar la mayor parte de la funcionalidad de la carga útil de Emotet.

¿Qué es el Control Flow Flattening?

El Control Flow Flattening es una técnica que pretende ofuscar el flujo del programa eliminando las estructuras ordenadas del programa a favor de colocar los bloques del programa dentro de un bucle con una única sentencia switch que controla el flujo del programa.

En primer lugar, el cuerpo de la función se divide en bloques básicos y, a continuación, los bloques se colocan uno al lado del otro en el mismo nivel. En la Figura 2 se puede ver una visualización de esta transformación. El Control Flow Flattening puede combinarse con otras técnicas de ofuscación, como el hashing de la API o el cifrado de cadenas. Algunos de los ofuscadores más destacados para aplanar funciones son OLLVM y Tigress.

Figura 2: Comparación de un gráfico de flujo de control (CFG) aplanado y no aplanado

Echemos un vistazo a un ejemplo simplificado de CFF en acción.

Aplanando Hola Mundo

Para la demostración, hemos compilado un programa sencillo escrito en C. En la parte izquierda de la Figura 3, se muestra un gráfico de flujo de control (CFG) anotado del binario. En el lado derecho se puede ver la salida descompilada generada por el Descompilador Hex-Rays.

En esta figura no se han aplicado técnicas de ofuscación. El Descompilador Hex-Rays no tiene ningún problema en generar una representación de lenguaje de alto nivel fácil de leer del desensamblado. Incluso sin un descompilador, un ingeniero inverso experimentado puede simplemente seguir el gráfico de flujo de control para entender su propósito.

Figura 3: Gráfico de flujo de control y salida descompilada del programa de ejemplo

Ahora vamos a aplanar la función y comparar los resultados. La Figura 4 muestra el CFG y la salida descompilada después de aplicar el Control Flow Flattening. En el lado izquierdo, vemos que el número de bloques básicos se ha duplicado con creces, y la lectura de la salida descompilada ya no es posible sin dedicar una cantidad significativa de tiempo a su análisis.

Figura 4: Ejemplo anotado de una función aplanada

En general, el CFF introduce los siguientes problemas que dificultan nuestro análisis:

Desaplanando Emotet

Para desofuscar el uso de Emotet del Control Flow Flattenin, comenzamos con una revisión de las herramientas existentes y la investigación sobre la desofuscación de CFG. Algunas de ellas son

Para profundizar en el algoritmo que hay detrás del CFG Unflattening, los artículos citados anteriormente proporcionan una gran cantidad de información.

En la Figura 5 se puede ver la salida descompilada y la CFG de una función en una muestra de Emotet desempaquetada. Excluyendo el Aplanamiento del Flujo de Control aplicado aquí, la salida puede parecer confusa, porque Emotet aplica más de una técnica de ofuscación. (Si no estás familiarizado con esas otras técnicas, un apéndice al final de este artículo explica brevemente las otras técnicas de ofuscación).

En primer lugar, la función llama a OpenSCManagerA para obtener un handle del Service Control Manager. A continuación, llama a OpenServiceW para abrir un servicio existente. Si la apertura del servicio tiene éxito, el servicio abierto se eliminará mediante DeleteService. Por último, se cerrarán los handles abiertos. Si el servicio se ha eliminado con éxito, la función devuelve 1, en caso contrario 0.

Figura 5: Ejemplo anotado de una función aplanada

Si comparamos la salida descompilada de la Figura 3 y la Figura 4, podemos ver múltiples similitudes, y podemos identificar de nuevo el despachador CFG. En la salida descompilada, vemos una variable que anotamos como stateVar. Al igual que la salida de la Figura 3, esta es nuestra variable de estado que se actualiza constantemente y es utilizada por el despachador para determinar qué bloque se ejecuta a continuación.

En un nivel alto, si queremos restaurar el flujo de control, necesitamos:

  1. Identificar el bloque y los estados del despachador
  2. Para cada bloque, identificar la constante correspondiente y encontrar la dirección del siguiente bloque a ejecutar basado en el despachador y el valor de la variable de estado
  3. Parchear los bloques despachadores de salida para saltar a la dirección del siguiente bloque original

En lugar de parchear y operar en el desensamblado directamente, hacemos uso de la API de Microcode de Hex-Rays. Microcode es un lenguaje intermedio utilizado por el descompilador Hex-Rays. Durante la descompilación, el descompilador pasa por diferentes fases de maduración. Las diferentes fases se muestran en la figura 6. La API nos permite enganchar el progreso de la descompilación y operar en el microcódigo en lugar de parchear el desensamblado directamente.

Figura 6: Niveles de madurez de IDA Microcode

Ajustando la herramienta

Utilizamos un fork de IDAPython de la herramienta HexRaysDeob de Rolf Rolles como base. Al igual que el fork, operamos únicamente con el nivel de madurez MMAT_LOCOPT, el tercer nivel de la figura anterior. Como se ve en la figura 6, ese nivel de madurez incluye información sobre los bloques de entrada y salida, que son necesarios para identificar correctamente los bloques de envío. Además, el código original se basaba en la capa MMAT_LOCOPT. Cambiar la capa habría requerido mucha más investigación, verificación y ajustes del código existente que mantener la capa. A continuación se resumen los cambios que aplicamos sobre el código base existente.

Manejo de despachadores múltiples/relacionados

En las funciones múltiples, la ejecución del algoritmo de desobstrucción en un solo despachador no generaba un resultado con el que estuviéramos satisfechos. El análisis demostró que las funciones más complejas podían contener múltiples despachadores anidados en lugar de uno. Hemos añadido una lógica adicional para identificar y ejecutar el algoritmo en múltiples emisores. Esta opción puede activarse o desactivarse ajustando el indicador RUN_MLTPL_DISPATCHERS a True o False. En la Figura 7, a continuación, se puede ver un ejemplo de una función con dos despachadores potenciales.

Figura 7: Ejemplo de una función con dos despachadores potenciales

Búsqueda arriesgada del Cluster Head

Un bloque aplanado puede ser implementado por múltiples bloques de microcódigo. Para encontrar el final de la región, el algoritmo original de Rolf Rolles genera un árbol de dominadores y utiliza la información generada para determinar el final de una región, o el comienzo de un cluster. En algunos casos, el algoritmo no lograba encontrar la cabeza del clúster. Añadimos una función adicional para determinar el cluster head como recurso. Creemos que el algoritmo original de Rolf Rolles es más fiable; sin embargo, la evaluación demostró que el algoritmo de reserva seguía dando buenos resultados y mejoraba la salida descompilada.

Patrones adicionales y pequeñas actualizaciones de código

En algunos casos, la lógica existente fallaba a la hora de parchear todos los bloques aplanados. Tras analizar múltiples funciones, identificamos varios patrones que se repetían en todo el binario. Añadimos a la base de código existente una lógica adicional para identificar y desanclar los bloques que seguían estos patrones. Por último, ajustamos un poco el código general. Algunos de los cambios incluyen:

De 254 funciones, clasificamos 68 funciones como aplanadas. De estas 68 funciones, pudimos desinstalar 38 con éxito. Diecinueve funciones fueron parcialmente aplanadas y 11 fallaron. Por “desinflado con éxito”, nos referimos a los casos en los que nuestro script no logró desinflamar un máximo de 3 estados. “Parcialmente desinflado” significa que la mayor parte de la función permanece aplanada, pero nuestra herramienta fue capaz de desinflamar algunos bloques. Por último, “fallido” significa que no hemos podido desofuscar ni un solo bloque de la función.

La Figura 8 muestra la función de la Figura 5 después de aplicar nuestro script.

Figura 8: Función analizada tras la aplicación de la herramienta de desenmascaramiento de CFG

IoCs

Descripción SHA256
Emotet empaquetado 9a0286ec0a3e7ea346759c9497c8b5c7c212fa2c780a1cabb094134bf492a51b
Emotet desempaquetado 1bbce395c839c737fdc983534b963a1521ab9693a5b585f15b8a4950adea5973

 

Nuestra herramienta de desinstalación ya está disponible en el Github de SophosLabs. (Para quienes estén interesados en estas cosas, también recomendamos una herramienta de desempaquetado de CFF creada por ESET hace varios años para abordar el Control Flow Flattening en la red de bots Stantinko (otro ejemplo de por qué, ya que los atacantes comparten libremente tácticas, técnicas y procedimientos entre ellos, los defensores deben hacer lo mismo).

Conclusión y limitaciones

El Control Flow Flattening es un tema complejo, y el propósito de este artículo es compartir nuestra experiencia y resultados atacando el CFF de Emotet. Aunque hemos realizado múltiples ajustes y hemos tenido cierto éxito, nuestra solución no es capaz de desofuscar todas las funciones por completo.  Entre las cuestiones pendientes:

En general, recomendamos que los investigadores comprueben siempre sus resultados y no confíen ciegamente en la salida. El aplanamiento del flujo de control utilizado junto con otras técnicas de ofuscación ciertamente complica el proceso de ingeniería inversa de Emotet, pero la técnica que hemos descrito ayuda a igualar las probabilidades contra los investigadores que examinan este malware de alto perfil.

Apéndice: Emotet y la ofuscación de código

Al compartir la salida descompilada de las funciones en una muestra de Emotet, es imposible no encontrar otras técnicas de ofuscación de Emotet más allá de CFF. Este apéndice cubre las técnicas de ofuscación más prevalentes que hemos identificado en una muestra de Emotet descompilada. Ten en cuenta que Emotet suele entregarse empaquetado y necesita ser desempaquetado primero.

Cifrado de cadenas

Emotet contiene cadenas cifradas en su forma desempaquetada. Antes de su uso, las cadenas serán descifradas y liberadas de nuevo justo después de que cada una cumpla su propósito.

Figura 9: Referencias cruzadas de la función DecryptString con la correspondiente cadena descifrada

API Hashing

Emotet utiliza el hash de la API para ocultar el uso de las funciones de la API. El malware calcula el hash de los nombres de las funciones exportadas para una DLL determinada. Si el hash calculado coincide con la constante introducida en la pila al invocar el método, se recuperará el puntero a la función exportada.

Figura 10: Desmontaje de la invocación de la función ApiHash

En la mayoría de los casos, las llamadas a la API Hashing y su correspondiente llamada dinámica están envueltas en funciones separadas. Hemos automatizado este análisis, y las funciones con el prefijo DYN_ son funciones determinadas en tiempo de ejecución mediante API Hashing.

Instrucciones basura

Emotet incorpora instrucciones basura para confundir a los ingenieros inversos. Las instrucciones basura son instrucciones que no sirven para nada, excepto para complicar y ralentizar el análisis. La Figura 11 muestra un ejemplo de un bloque de instrucciones basura.

Figura 11: Ejemplo de instrucciones basura en una muestra de Emotet sin empaquetar

Ofuscación de la pila

Otra técnica interesante que confunde al descompilador IDA es la forma en que Emotet pasa los parámetros a las funciones. En la Figura 12, mostramos cómo se invoca DYN_BCryptEncrypt.

DYN_BCryptEncrypt primero resuelve la función de la API BCryptEncrypt y almacena el puntero a esta función en el registro EAX. A continuación, se llama a la función mediante la llamada EAX. En lugar de limitarse a introducir los parámetros necesarios, este método introduce valores en la pila que no son utilizados por la llamada EAX real. Esto lleva a la generación de una firma de función que es mucho más difícil de leer de lo normal.

Figura 12: Múltiples valores introducidos en la pila antes de invocar DYN_BCryptEncrypt
Figura 13: Firma de la función generada correspondiente