El nuevo cryptominer de Stantinko presenta técnicas únicas de ofuscación


Los investigadores de ESET sacan a la luz técnicas únicas de ofuscación descubiertas en el curso del análisis de un nuevo módulo de criptominería distribuido por la botnet del grupo Stantinko

En el nuevo módulo de criptominería descubrimos y describimos en nuestro Artículo anterior, los cibercriminales detrás del Botnet Stantinko introdujo varias técnicas de ofuscación, algunas de las cuales aún no se han descrito públicamente. En este artículo, diseccionamos estas técnicas y describimos posibles contramedidas contra algunas de ellas.

Para frustrar el análisis y evitar la detección, el nuevo módulo de Stantinko utiliza varias técnicas de ofuscación:

  • Ofuscación de cadenas: las cadenas significativas se construyen y solo están presentes en la memoria cuando se van a usar
  • Ofuscación de flujo de control: transformación del flujo de control a un formulario que es difícil de leer y el orden de ejecución de los bloques básicos es impredecible sin un análisis exhaustivo
  • Código muerto: adición de código que nunca se ejecuta; También contiene exportaciones que nunca se llaman. Su propósito es hacer que los archivos se vean más legítimos para evitar la detección
  • Código de no hacer nada: adición de código que se ejecuta, pero que no tiene ningún efecto material en la funcionalidad general. Está destinado a evitar detecciones de comportamiento
  • Cadenas y recursos muertos: adición de recursos y cadenas sin impacto en la funcionalidad

De estas técnicas, las más notables son la ofuscación de cadenas y la ofuscación de flujo de control; los describiremos en detalle en las siguientes secciones.

Todas las cadenas integradas en el módulo no están relacionadas con la funcionalidad real. Su origen es desconocido y sirven como bloques de construcción para construir las cadenas que realmente se usan o no se usan en absoluto.

Las cadenas reales utilizadas por el malware se generan en la memoria para evitar la detección basada en archivos y frustrar el análisis. Se forman al reorganizar los bytes de las cadenas señuelo, los integrados en el módulo, y al usar funciones estándar para la manipulación de cadenas, como strcpy (), strcat (), strncat (), strncpy (), sprintf (), memmove () y sus versiones Unicode.

Dado que todas las cadenas que se utilizarán en una función en particular siempre se ensamblan secuencialmente al comienzo de la función, se pueden emular los puntos de entrada de las funciones y extraer las secuencias de caracteres imprimibles que surgen para revelar las cadenas.

Figura 1. Ejemplo de ofuscación de cuerdas. Hay 7 cadenas de señuelo resaltadas en la imagen. Por ejemplo, el marcado en rojo genera la cadena "NameService".

Control de aplanamiento de flujo Es una técnica de ofuscación utilizada para frustrar el análisis y evitar la detección.

El aplanamiento común del flujo de control se logra dividiendo una sola función en bloques básicos. Estos bloques se colocan como despachos en una declaración de cambio dentro de un bucle (es decir, cada despacho consta de exactamente un bloque básico). Hay una variable de control para determinar qué bloque básico se debe ejecutar en la instrucción switch; su valor inicial se asigna antes del ciclo.

A todos los bloques básicos se les asigna una ID y la variable de control siempre contiene la ID del bloque básico que se ejecutará.

Todos los bloques básicos establecen el valor de la variable de control en la ID de su sucesor (un bloque básico puede tener múltiples sucesores posibles; en ese caso, el sucesor inmediato se puede elegir en una condición).

Figura 2. Estructura del circuito común de control-flujo-aplanamiento

Existen varios enfoques para resolver esta ofuscación, como el uso de la API de microcódigo de IDA. Rolf Rolles usado este método Para identificar estos bucles heurísticamente, extraiga la variable de control de cada bloque aplanado y reorganícelos de acuerdo con las variables de control.

Esto, y otros enfoques similares, no funcionarían en la ofuscación de Stantinko, porque tiene algunas características únicas en comparación con las ofuscaciones comunes de control-aplanamiento del flujo:

  • El código se aplana en el nivel del código fuente, lo que también significa que el compilador puede introducir algunas anomalías en el binario resultante
  • La variable de control se incrementa en un bloque de control (que se explicará más adelante), no en bloques básicos.
  • Los despachos contienen múltiples bloques básicos (la división puede ser disyuntiva, es decir, cada bloque básico pertenece exactamente a un despacho, pero a veces los despachos se entrelazan, lo que significa que comparten algunos bloques básicos)
  • Los bucles de aplanamiento pueden ser anidados y sucesivos
  • Múltiples funciones se fusionan

Estas características muestran que Stantinko ha introducido nuevos obstáculos a esta técnica que deben superarse para analizar su carga útil final.

Control de flujo de aplanamiento en Stantinko

En la mayoría de las funciones de Stantinko, el código se divide en varios despachos (descritos anteriormente) y dos bloques de control, una cabeza y una cola, que controlan el flujo de la función.

El responsable decide qué despacho debe ejecutarse comprobando la variable de control. La cola aumenta la variable de control en una constante fija y vuelve a la cabeza o sale del bucle de aplanamiento:

Figura 3. Estructura regular del circuito de aplanamiento del flujo de control de Stantinko

Stantinko parece aplanar el código de todas las funciones y cuerpos de construcciones de alto nivel (como un bucle for), pero a veces también tiende a elegir bloques de código aparentemente aleatorios. Dado que aplica los bucles de aplanamiento del flujo de control tanto en las funciones como en las construcciones de alto nivel, pueden anidarse naturalmente y también puede haber múltiples bucles consecutivos.

Cuando se crea un bucle de aplanamiento del flujo de control fusionando el código de múltiples funciones, la variable de control en la función fusionada resultante se inicializa con diferentes valores, en función de cuál de las funciones originales se llama. El valor de la variable de control se pasa a la función resultante como un parámetro.

Superamos esta técnica de ofuscación reorganizando los bloques en el binario; Nuestro enfoque se describe en la siguiente sección.

Es importante tener en cuenta que observamos múltiples anomalías en algunos de los bucles de aplanamiento que dificultan la automatización del proceso de desofuscación. La mayoría de ellos parecen ser generados por el compilador; Esto nos lleva a creer que la ofuscación de aplanamiento del flujo de control se aplica antes de la compilación.

Fuimos testigos de las siguientes anomalías; Pueden aparecer por separado o en combinación:

  1. Algunos despachos pueden ser simplemente código muerto; nunca se ejecutarán. (Ejemplos en la sección "Código muerto dentro del ciclo de control-flujo-aplanamiento" a continuación).
  2. Los bloques básicos dentro de los despachos pueden entrelazarse, esto significa que pueden contener código conjunto.

Figura 4. Estructura de un bucle de aplanamiento con despachos que comparten código conjunto

  1. Hay saltos directos desde los despachos a un bloque fuera del bucle de aplanamiento, justo detrás de la cola, y a los bloques que regresan de la función.

Figura 5. Estructura de un bucle de aplanamiento cuyo envío se rompe directamente del bucle. Solo se produce una de las líneas discontinuas.

  1. Puede haber múltiples colas o ninguna cola: en este último caso, la variable de control aumenta al final de cada envío.

Figura 6. Estructura de un bucle de aplanamiento sin cola (izquierda) y con múltiples colas (derecha)

  1. La cabeza no contiene una mesa de salto de inmediato. En cambio, puede haber varias tablas de salto y hay una secuencia de ramas, antes de las tablas de salto, buscando binariamente el envío correcto.
  2. El valor de la variable de control puede usarse dentro de los despachos; Esto significa que el valor de control debe ser preservado / calculado incluso en el código desobuscado.

Figura 7. El EDI register contiene la variable de control que se pasa a EAX y se usa dentro del despacho. El despacho se resalta en rojo.

  1. A veces, la cola contiene instrucciones que son cruciales para restaurar los valores correctos de los registros y las variables locales. Durante la desofuscación, eliminamos la cola, por lo que debemos asegurarnos de que estas instrucciones se ejecuten después de cada envío, incluso si no forman parte de él.
  2. Hay casos en los que no hay despacho cuya ID es igual a, en ese momento, igual al valor actual de la variable de control.

Desofuscación

Nuestro objetivo es construir una función de desofuscación capaz de reorganizar el código en el nivel binario para que sea fácilmente legible para un ingeniero inverso, mientras se mantiene el código resultante ejecutable. Tiene que poder reconocer todos los bloques básicos que pertenecen a cada despacho y copiarlos y moverlos arbitrariamente.

Durante la manipulación básica de bloques, uno debe asegurarse de recalcular las direcciones relativas de los objetivos de sucursal y las direcciones que forman correctamente tablas de salto legítimas.

Nuestra solución no tiene en cuenta las reubicaciones, por lo que siempre es necesario asegurarse de que la muestra se cargue en la misma dirección base.

Utilizamos un marco de ingeniería inversa que nos proporciona algunas características útiles, como la manipulación de ensamblajes y un ejecución simbólica motor.

Los parámetros centrales de la función son las direcciones de los bloques de control (cabeza y cola), el rango y el paso de la variable de control, los nombres de los registros y las ubicaciones de memoria que contienen la variable de control, las ubicaciones de control y, por último, la dirección de el primer bloque básico que sigue al ciclo, que definimos como bloque_siguiente. Obviamente, también requiere que la dirección de la función esté desofuscada y la dirección donde se debe colocar la función desofuscada.

Esperamos múltiples colas debido a la anomalía 4 anterior.

La función de desofuscación recorre el rango de la variable de control por su valor de paso para simular el bucle real de control-flujo-aplanamiento; En cada iteración, la función comienza generando un contexto para tratar las anomalías 6 y 7. El contexto debe colocarse antes del despacho respectivo.

El contexto es un bloque básico que contiene instrucciones que asignan registros y direcciones de memoria y mantienen las ubicaciones de control actualizadas. El contexto de la primera iteración simplemente conserva el valor de la variable de control. (Nota: no se requiere contexto para tratar la anomalía número 4.)

Los últimos bloques básicos del despacho anterior (o, en el caso del primer despacho, los bloques básicos justo antes del encabezado) se redirigen al contexto creado.

El bloque básico inicial de un despacho que se ejecutará (en cada una de las iteraciones) está determinado por el valor actual de la variable de control (ID de despacho).

El bloque básico real se encuentra ejecutando simbólicamente el algoritmo de búsqueda binaria, que busca un bloque básico con la ID actual. El estado inicial de la ejecución simbólica contiene las ubicaciones de control asignadas al valor actual de la variable de control.

Paramos la ejecución simbólica en el primer bloque básico que (i) contiene una rama incondicional, o (ii) tiene un destino que no puede ser determinado por la variable de control.

También se podría emular esta parte o usar un marco que podría simplificar el algoritmo de búsqueda binaria en una tabla de salto y luego convertirlo en una declaración de cambio. Estos métodos abordan la anomalía 5.

En caso de que no haya un envío para una ID en particular, el ciclo simplemente continúa y aumenta la variable de control debido a la anomalía 8.

El envío completo (es decir, cada bloque básico al que se puede acceder desde su bloque básico inicial hasta su cabeza, cola o cola). bloque_siguiente) se copia después del bloque de contexto anterior (como se describió anteriormente). No se puede mover solo debido a la anomalía 2.

Actualmente hay dos casos poco comunes que pueden ocurrir debido a la anomalía 3; ambos resultan en la terminación prematura de la iteración. Los casos ocurren cuando un despacho:

  • Devoluciones de la función
  • Puntos a bloque_siguiente

Finalmente, cuando finaliza la iteración, los últimos bloques básicos del despacho anterior (o bloques básicos justo antes del encabezado, en el caso del primer despacho), se redirigen al primer bloque básico fuera del bucle de aplanamiento.

Este método resuelve la anomalía 1 automáticamente, ya que los despachos muertos no se copiarán en el código resultante.

Figura 8. Ejemplo de una función ofuscada (izquierda) y su contraparte desofuscada (derecha). Los despachos se ejecutan en este orden: dispatch1 → dispatch2 → dispatch3.

Estos cambios se escriben en la dirección virtual donde se debe colocar la función desofuscada.

En caso de que se trate de aplanar funciones fusionadas, señalamos referencias a la función de destino que tiene el valor inicial idéntico de la variable de control en el parámetro, a la dirección de la nueva función desofuscada.

Figura 9. Ejemplo de diagrama de flujo de control ofuscado (derecha) y desobuscado (izquierda)

Posibles mejoras.

El enfoque descrito anteriormente opera exclusivamente en el nivel de ensamblaje, que no es suficiente para automatizar completamente la desofuscación.

La razón es que el reconocimiento preciso de todos los patrones es bastante difícil, principalmente debido a varias optimizaciones del compilador presentes en las ofuscaciones a nivel del código fuente. El reconocimiento de patrones es necesario en nuestro caso, por ejemplo, para completar automáticamente los parámetros de la función de desofuscación del núcleo.

La ventaja de este enfoque es que el código resultante puede ejecutarse de inmediato y uno puede usar herramientas arbitrarias de ingeniería inversa para un análisis más detallado.

Este enfoque podría mejorarse aún más mediante el uso de un método progresivo. representación intermedia (IR), que proporciona técnicas de optimización que, entre otras cosas, eliminarían la mayoría de las anomalías generadas por los compiladores y, por lo tanto, permitirían el reconocimiento automático de los parámetros requeridos por la función de desofuscación.

También se podría usar el IR seleccionado tanto para el reconocimiento como para la desofuscación, de los cuales este último, en nuestro caso, consiste en reorganizar los bloques básicos.

El inconveniente de esta opción es que el código resultante también estaría en el IR, lo que significa que el análisis consecutivo tendría que hacerse también con el IR. El número de herramientas que trabajan con el IR y su funcionalidad podría ser bastante limitado, especialmente cuando se trata de visualización. Debido a esto, sería difícil analizar una muestra más compleja, especialmente cuando hay capas adicionales de ofuscación. Tampoco podríamos ejecutar el código resultante.

Por "código muerto" nos referimos al código que nunca se ejecuta o que no tiene un impacto general en la funcionalidad. El malware contiene código muerto principalmente en los bucles aplanados (efectivamente eliminado por nuestra función de desofuscación explicada anteriormente), pero también hay, por ejemplo, exportaciones no utilizadas y no hay forma de distinguir las exportaciones no utilizadas de las legítimas.

En cuanto al código muerto en el bucle plano: para Stantinko, siempre está dentro de los despachos que nunca se ejecutan. Puede contener partes modificadas de software legítimo como WinSpy ++ (vea el ejemplo a continuación) que se ofuscó de la misma manera.

Figura 10. Parte desofuscada del código muerto dentro de un despacho que contiene código legítimo de WinSpy ++

Figura 11. La parte equivalente del código (como en la Figura 10) en el lanzamiento oficial de WinSpy ++

Incluso después de la operación de aplanamiento, hay partes del código que no tienen ningún propósito, entremezcladas con las líneas del "código real". Probablemente, esto esté destinado a oscurecer aún más el análisis o evitar la detección de comportamiento.

Figura 12. Las partes marcadas son código redundante que itera a través de los dos primeros nombres de volumen de disco y luego no hace nada con los valores devueltos

Como el código no es mucho más difícil de leer, decidimos no tomar ninguna medida y analizamos el código en este momento.

Para optimizar este código de no hacer nada en general: deberíamos, por ejemplo, generar disyunción rebanadas que contiene todas las llamadas a la API de Windows que están presentes. El criterio de corte consistiría en todos los parámetros de las llamadas en cada corte disyuntivo.

Posteriormente, ejecutaríamos los segmentos con una pila de llamadas preparada en un entorno controlado y consideraríamos que un segmento es funcional si hace al menos uno de los siguientes:

  • hacer algunos cambios al SO subyacente
  • requieren que se conozca un valor inicial de un parámetro de función o una variable global
  • asignar un valor de un parámetro de función o una variable global
  • afectar directamente el flujo de control general de la función

Los delincuentes detrás de la botnet Stantinko están constantemente mejorando y desarrollando nuevos módulos que a menudo contienen técnicas no estándar e interesantes.

Hemos descrito su nuevo módulo de criptominería anteriormente; para el análisis funcional del módulo, consulte nuestro noviembre de 2019 entrada en el blog. Este módulo muestra varias técnicas de ofuscación destinadas a proteger contra la detección y frustrar el análisis. Analizamos las técnicas y describimos un posible enfoque para desofuscar algunas de estas técnicas.

Nota: para IoC y la lista de técnicas asignadas a la taxonomía MITER ATT & CK, consulte nuestro artículo anterior describiendo la funcionalidad de este cryptominer.








Enlace a la noticia original