El malware puede estar hecho de muchos componentes. A menudo, encontramos macros y scripts que funcionan como descargadores maliciosos. Algunas funcionalidades también pueden lograrse mediante el código independiente de posición, denominado código shell. Pero cuando se trata de elementos más complejos o módulos centrales, casi damos por hecho que será un archivo PE que es un formato ejecutable nativo de Windows.

La razón de esto es simple: es mucho más fácil proporcionar una funcionalidad compleja dentro de un archivo PE que dentro de un shellcode. El formato PE tiene una estructura bien definida, lo que permite mucha más flexibilidad. Tenemos ciertos encabezados que definen qué importaciones se deben cargar y dónde, así como también cómo se deben aplicar las reubicaciones. Este es un formato predeterminado que se genera cuando compilamos aplicaciones para Windows, y su estructura es utilizada por Windows Loader para cargar y ejecutar nuestra aplicación. Incluso cuando los autores de malware escriben cargadores personalizados, son principalmente para el formato PE.

Sin embargo, a veces encontramos excepciones. La última vez, cuando analizamos las cargas útiles relacionadas con Hidden Bee (eliminadas por el kit de explotación de Underminer) , notamos algo inusual. Se eliminaron dos cargas útiles que no siguieron el formato PE. Sin embargo, su estructura se veía bien organizada y más compleja de lo que generalmente encontramos al tratar con piezas de Shellcode. Decidimos mirar más de cerca y descubrimos que los autores de este malware realmente crearon su propio formato ejecutable, siguiendo una estructura consistente.

Visión de conjunto

La primera carga útil: b3eb576e02849218867caefaa0412ccd (con extensión .wasm, imitando Web Assembly) es un cargador que descarga y descomprime un archivo de Cabinet:

La segunda carga útil: 11310b509f8bf86daa5577758e9d1eb5 , desempaquetada desde el gabinete:

Podemos ver al principio que, a diferencia de la mayoría de los códigos de shell, no comienza desde un código, sino desde algunos encabezados. Al comparar ambos módulos, podemos ver que el encabezado tiene la misma estructura en ambos casos.

Encabezados

Examinamos más de cerca para descifrar el significado de campos particulares en el encabezado.

El primer DWORD: 0x10000301 es el mismo en ambos. No encontramos este número correspondiente a ninguna de las piezas dentro del módulo. Entonces, asumimos que es un número mágico que hace un identificador de este formato.

A continuación, dos WORDs se compensan con elementos relacionados con la carga de las importaciones. El primero (0x18) apunta a la lista de DLL. El segundo bloque (0x60) se ve más misterioso al principio. Su significado se puede entender cuando cargamos el módulo en IDA. Podemos ver las referencias cruzadas a esos campos:

Vemos que se usan como IAT: se supone que deben llenarse con las direcciones de las funciones importadas:

El siguiente valor es un DWORD (0x2A62). Si lo seguimos en IDA, vemos que conduce al comienzo de una nueva función:

Esta función no está referenciada por ninguna otra función, por lo que podemos sospechar que se trata del punto de entrada del programa.

El significado del siguiente valor (0x509C) es fácil de adivinar porque es el mismo que el tamaño del módulo completo.

Entonces, tenemos los dos últimos DWORD del encabezado. El segundo DWORD (0x4D78) conduce a la estructura que es muy similar a las reubicaciones del PE. Podemos adivinar que debe ser una tabla de reubicación del módulo, y el DWORD anterior especifica su tamaño.

Así fue como pudimos reconstruir el encabezado completo:

typedef struct {
	Magia DWORD;

	WORD dll_list;
	PALABRA iat;
	DWORD ep;
	DWORD mod_size;

	DWORD relocs_size;
	DWORD se traslada;
} t_bee_hdr;

Importaciones

Como sabemos por el encabezado, la lista de las DLL comienza en el desplazamiento 0x18. Podemos ver que cada uno de los nombres de la DLL está precedido de un número:

Los números no se corresponden con un nombre de DLL: en dos módulos diferentes, la misma DLL tenía diferentes números asignados. Pero si sumamos todos los números, encontramos que su suma total es la misma que el número de DWORD en el IAT. Entonces, podemos adivinar que esos números están especificando cuántas funciones se importarán desde una DLL particular.

Podemos describirlo como la siguiente estructura (donde no se especifica la longitud del nombre):

typedef struct {
	WORD func_count;
	nombre del personaje;
} t_dll_name;

Entonces, el IAT viene como una lista de DWORDs:

Es común en el malware que cuando los nombres de la función no se dan como una cadena explícita, se importen mediante suma de comprobación. Lo mismo se hace en este caso. Adivinar la función apropiada que se usó para calcular la suma de comprobación puede ser más difícil. Afortunadamente, lo encontramos en el componente cargador:

Suma de comprobación DWORD (char * func_name)
{
  Resultado DWORD = 0x1505;
  while (* func_name)
    result = * func_name ++ + 33 * resultado;
  resultado de devolución;
}

Sabiendo que emparejamos las sumas de comprobación apropiadas con los nombres de las funciones:

Una vez que se recupera la dirección de la función, se almacena en el IAT en lugar de la suma de comprobación.

Reubicaciones

Crear una tabla de reubicación es simple. Consiste en la lista de DWORD que identifican los desplazamientos de los lugares en el código a los que debemos agregar la base donde se ha cargado el módulo. Sin las reubicaciones aplicadas, el módulo se bloqueará (por lo tanto, no es independiente de la posición como un shellcode típico).

Comparación con el formato PE

Si bien el formato PE es complejo, con una variedad de encabezados, este contiene solo elementos esenciales. La mayor parte de la información que generalmente se almacena en un encabezado PE se omite por completo aquí.

Puede ver un formato PE visualizado por Ange Albertini aquí .

Compárelo con la visualización del formato analizado actualmente:

Análisis estático

Podemos cargar este código en IDA como una burbuja de código sin formato. Sin embargo, estaremos perdiendo información importante. Debido al hecho de que el archivo no sigue una estructura PE, y su tabla de importación no es estándar, tendremos dificultades para entender qué llamadas API se están realizando con cada desplazamiento. Para resolver este problema, hice una herramienta que resuelve los hashes en nombres de funciones y genera un archivo TAG para marcar los desplazamientos donde se llenará la dirección de cada función.

Esas etiquetas se pueden cargar en IDA usando un  plugin IFL :

Teniendo todas las funciones API etiquetadas, es mucho más fácil entender qué acciones realiza el módulo. Aquí, por ejemplo, podemos ver que establecerá la conexión con el servidor C2:

Análisis dinámico

Este formato es personalizado, por lo que no es compatible con las herramientas típicas para el análisis. Sin embargo, después de entenderlo, podemos escribir nuestras propias herramientas, como el analizador sintáctico para los encabezados y el cargador, que ayudarán a ejecutar este formato y analizarlo dinámicamente.

A diferencia de PE, el módulo no tiene secciones. Por lo tanto, tenemos que cargarlo en una región de memoria continua con acceso RWX (lectura-escritura-ejecución). Al recorrer la lista de reubicaciones, agregaremos el valor de la base en la que se cargó el módulo a las direcciones enumeradas. Luego, tenemos que resolver las funciones importadas por sus valores hash y completar las direcciones en los procesos. Después de preparar la etapa, solo necesita saltar en el punto de entrada del módulo. Cargaremos el cargador preparado debajo del depurador y lo seguiremos hasta el punto de entrada del módulo cargado.

Simple pero raro

Los elementos descritos aquí son bastante simples: sirven como una primera etapa del paquete completo de malware, descargando otras piezas e inyectándolas en los procesos. Sin embargo, lo que los hace interesantes es el hecho de que sus autores han demostrado cierta creatividad y han decidido inventar un formato personalizado que es menos complejo que un PE completo, pero va un paso más allá de una pieza típica de shellcode.

Dicho módulo, a diferencia de Shellcode independiente, no es autosuficiente y no puede cargarse de manera trivial, sino que debe analizarse primero. Dado el hecho de que el formato es personalizado, no es compatible con las herramientas existentes. Aquí es donde las habilidades de programación son útiles para un analista de malware.

Afortunadamente, los formatos completamente personalizados son bastante poco comunes en el mundo del malware; por lo general, los autores dependen en gran medida de los formatos existentes, de vez en cuando corrompen o personalizan partes seleccionadas de los encabezados PE.