Análisis de malware
Manual del investigador de malware (desmitificando el archivo PE, parte 2)
diciembre 23, por Revers3r
Desmitificando el archivo PE – Continuación
Según nuestro artículo anterior, continuaremos con este artículo aquí con el resto de la sección del archivo PE. Aquellos que no conozcan la sección anterior, por favor miren aquí .
Porque esta será una sección más avanzada, así como algún tipo de material de automatización, donde implementaré la API de Windows y la programación en Python.
Como dijimos en el artículo anterior, analizaremos cómo la función de devolución de llamada TLS es muy útil para el atacante y también mostraremos una demostración mediante una aplicación. Pero antes de eso deberíamos terminar el resto de la sección de Educación Física.
La Sección de Exportación:
Las exportaciones son funciones y valores en un módulo que se han declarado para compartir con otros módulos. Esto se hace mediante el uso del «Directorio de exportación», que se utiliza para traducir entre el nombre de una exportación (u «Ordinal», se explicará más adelante) y una ubicación en la memoria donde se pueden almacenar el código o los datos. encontró. El inicio del directorio de exportación se identifica mediante la entrada IMAGE_DIRECTORY_ENTRY_EXPORT del directorio de recursos como se muestra a continuación:
estructura IMAGE_EXPORT_DIRECTORY {
características largas;
largo TimeDateStamp;
versión principal corta;
versión menor corta;
nombre largo;
base larga;
número largo de funciones;
número largo de nombres;
largo *DirecciónDeFunciones;
long *DirecciónDeNombres;
long *DirecciónDeNombreOrdinales;
}
Esta sección hace especial referencia al archivo DLL y su estructura.
En Microsoft Windows, una DLL son los módulos que contienen funciones y datos. Una DLL se carga en tiempo de ejecución mediante su módulo de llamada, que puede ser exe o una DLL. Cuando se carga una DLL, se asigna a la dirección del proceso de llamada de la función.
Una DLL puede tener dos secciones : Exportada e Interna.
Las funciones exportadas pueden ser invocadas por otros módulos. Las funciones internas se pueden llamar dentro del módulo/DLL donde se definieron.
Las exportaciones reales se describen a través de AddressOfFunctions, que es un RVA para una matriz de RVA, cada uno de los cuales apunta a una función o valor diferente que se exportará. El tamaño de esta matriz está en el valor NumberOfFunctions. Cada una de estas funciones tiene un ordinal. El valor «Base» se utiliza como ordinal de la primera exportación y el siguiente RVA en la matriz es Base+1, y así sucesivamente.
Cada entrada en la matriz AddressOfFunctions se identifica mediante un nombre, que se encuentra a través de RVA AddressOfNames. Los datos a los que apunta AddressOfNames son una matriz de RVA, del tamaño NumberOfNames. Cada RVA apunta a una cadena ASCII terminada en cero, siendo cada uno el nombre de una exportación. También hay una segunda matriz, a la que apunta el RVA en AddressOfNameOrdinals. Esto también tiene el tamaño NumberOfNames, pero cada valor es una palabra de 16 bits y cada valor es un ordinal. Estas dos matrices son paralelas y se utilizan para obtener un valor de exportación de AddressOfFunctions. Para buscar una exportación por nombre, busque en la matriz AddressOfNames la cadena correcta y luego tome el ordinal correspondiente de la matriz AddressOfNameOrdinals. Este ordinal luego se usa para obtener un índice de un valor en AddressOfFunctions.
Si analizamos los miembros de 11 secciones de Image_Import_Directory, solo discutiremos las secciones importantes como se muestra a continuación:
nName: el nombre interno del módulo. Este campo es necesario porque el usuario puede cambiar el nombre del archivo. Si eso sucede, el cargador PE utilizará este nombre interno.
nBase: número ordinal inicial (necesario para obtener los índices en la matriz de dirección de función; ver más abajo).
NumberOfFunctions: número total de funciones (también denominadas símbolos) que exporta este módulo.
NumberOfNames: número de símbolos que se exportan por nombre. Este valor no es el número de todas las funciones/símbolos del módulo. Para ese número, debe verificar NumberOfFunctions. Puede ser 0. En ese caso, el módulo puede exportar únicamente por ordinal. Si no hay ninguna función/símbolo para exportar en el primer caso, el RVA de la tabla de exportación en el directorio de datos será 0.
AddressOfFunctions: un RVA que apunta a una serie de punteros a (RVA de) las funciones del módulo: la tabla de direcciones de exportación (EAT). Para decirlo de otra manera, los RVA de todas las funciones del módulo se mantienen en una matriz y este campo apunta al encabezado de esa matriz.
AddressOfNames: un RVA que apunta a una matriz de RVA de los nombres de las funciones en el módulo: la tabla de nombres de exportación (ENT).
AddressOfNameOrdinals: un RVA que apunta a una matriz de 16 bits que contiene los ordinales de las funciones nombradas: la tabla ordinal de exportación (EOT).
Discutiremos más de forma gráfica a continuación:
Entonces Image_Export_Directory apunta a tres matrices y una tabla de cadenas ASCII. Lo importante es Exportar tabla de direcciones, que es una matriz de puntero de función, que contiene la dirección de la función exportada. Las otras 2 matrices (EAT y EOT) se ejecutan en paralelo en orden ascendente según el nombre de la función, de modo que se puede realizar una búsqueda binaria del nombre de una función y su resultado será que se encuentre su ordinal en la otra matriz. El ordinal es simplemente un índice del EAT para esa función.
Entonces podemos decir que si la función se exporta por nombre, debemos recorrer las matrices AddressOfNames y AddressOfNameOrdinals para obtener el índice en la matriz AddressOfFunctions.
Si ya tenemos el ordinal de una función, podemos encontrar su dirección yendo directamente al EAT. Aunque obtener la dirección de una función a partir de un ordinal es mucho más fácil y rápido que utilizar el nombre de la función, la desventaja es la dificultad en el mantenimiento del módulo. Podemos ver que cuando usamos los ordinales obtener la dirección de la función es mucho más rápido porque solo tenemos que calcular una operación de resta.
Exportar solo por ordinal: el número de funciones debe ser igual al número de nombres. A veces, el número de nombres es menor que el número de funciones. Entonces, las funciones que no tienen nombre se exportan solo por ordinal.
Reenvío de exportaciones: a veces, funciones que parecen exportadas por una DLL en particular, en realidad residen en una DLL completamente diferente. Esto se denomina reenvío de exportaciones.
Por ejemplo, en WinNT, Win2k y XP, la función HeapAlloc de kernel32.dll se reenvía a la función RtlAllocHeap exportada por ntdll.dll. NTDLL.DLL también contiene el conjunto de API nativo que es la interfaz directa con el kernel de Windows.
Ejemplo/demostración:
Imprimamos el encabezado de exportación para Kernel32.dll y su subsección. Primero usaré PE Explorer y luego volcaré todo EAT e IAT de notepad.exe de Windbg. Después de eso conectaré kernel32.dll
Entonces, el desplazamiento (A915) es el directorio de exportación de la base de imágenes (77510000). Por lo tanto, la dirección real es 77510000+A915. Veremos esta dirección en dd 77510000+B4DA8.
Concéntrate en las primeras 3 filas. La primera columna tiene la dirección de memoria. Necesitamos centrarnos en los valores de las siguientes 4 columnas para las primeras 3 filas. Al analizar estos valores y compararlos con la definición de estructura IMAGE_EXPORT_DIRECTORY obtenemos:
Características = 00000000
Marca de fecha y hora = 4a5bc04c
Versión principal = 0000
Versión menor = 000b82e6
lpNombre = 00000001
Base = 0000054f
Número de funciones = 0000054f
NúmeroDeNombres = 000b4dd0
lpDirecciónDeFunciones = 000b630c
lpDirecciónDeNombres = 000b7848
lpDirecciónDeNombreOrdinales = 00051162
Obteniendo RVA: ahora conoceremos el RVA de una dirección de función diferente.
El RVA del puntero a la matriz AddressOfNames es: 000b7848
Para volcar el contenido de esta matriz, agreguémoslo a la dirección base y mostremos:
dd base de imágenes +DirecciónDeNombre
Ahora tenemos la lista de RVA. Cada uno de estos RVA, cuando se agrega a la dirección base de gdi32.dll, apuntará a la cadena de nombre de función. Comprobemos tomando el primer RVA de esta lista: 00030002
La sección IMPORTAR
La sección Importar contiene información sobre todas las funciones importadas por ejecutable desde archivos DLL. Esta información se almacena en varias estructuras de datos. La estructura de datos más importante es el directorio de importación y la tabla de direcciones de importación. En algunos ejecutables, también hay directoriosbound_import y delay_Import, en los cuales el importante esbound_import.
El cargador de Windows es responsable de cargar todas las DLL que utiliza la aplicación y asignarlas al espacio de direcciones del proceso. Tiene que encontrar todas las funciones importadas en las distintas DLL y ponerlas a disposición del ejecutable que se está cargando.
DIRECTORIO DE IMPORTACIÓN:
La estructura del directorio IMPORT es el desplazamiento 80 del encabezado PE. Consulte la siguiente pantalla de ollydbg.
010000E0+80 = 01000160
El directorio de importación es en realidad una matriz de estructuras IMAGE_IMPORT_DESCRIPTOR .
estructura typedef _IMAGE_IMPORT_DESCRIPTOR {
Unión {
Características DWORD; // 0 para terminar el descriptor de importación nulo
DWORD OriginalFirstThunk; // RVA al IAT original independiente (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
Marca de fecha y hora DWORD; // 0 si no está vinculado,
// -1 si está vinculado y marca de fecha y hora real
// en IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (nuevo BIND)
// Marca de fecha/hora OW de la DLL vinculada a (BIND antiguo)
Cadena de reenvío DWORD; // -1 si no hay reenviadores
Nombre DWORD;
DWORD Primer pensamiento; // RVA a IAT (si está vinculado, este IAT tiene direcciones reales)
}
Cada estructura tieneytes y contiene información sobre una DLL desde la cual nuestro archivo PE importa funciones. Iniciemos windbg y extraigamos el símbolo detallado.
Aquí 1000 es el RVA para la base de la imagen (06a0000). Por lo tanto, la dirección base de la imagen +RVA apuntará a la tabla de direcciones de importación. Usaremos el comando «dps» para volcar la dirección en ese desplazamiento e intentaremos resolverla en símbolos. Probablemente necesitará ejecutar dps varias veces para recorrer toda la tabla de importación. Ya sabíamos que el tamaño es 400, por lo que forzaremos a windbg a usar esta dirección para mostrar todas las direcciones relevantes. También podemos ver la dirección.
Es bueno saber cómo recuperar un IAT de una imagen PE, ya que podemos usar este resultado para detectar cualquier tipo de ganchos IAT. Más adelante veremos los ganchos IAT, que es una técnica utilizada por los rootkits para tomar el control de las funciones en una DLL sobrescribiendo los punteros de función en el IAT.
Ahora analicemos las diferentes estructuras de IAT. El primer miembro OriginalFirstThunk , que es una unión DWORD, puede haber sido en algún momento un conjunto de indicadores. Sin embargo, Microsoft cambió su significado y nunca se molestó en actualizar WINNT.H. Este campo realmente contiene el RVA de una matriz de estructuras IMAGE_THUNK_DATA .
El miembro TimeDateStamp se establece en cero a menos que el ejecutable esté vinculado cuando contenga -1 (ver más abajo). El miembro ForwarderChain se utilizó para encuadernación al estilo antiguo y no se considerará aquí. El último miembro, FirstThunk , también contiene el RVA de una matriz de estructuras IMAGE_THUNK_DATA de tamaño DWORD, un duplicado de la primera matriz.
estructura typedef _IMAGE_THUNK_DATA32 {
Unión {
Cadena de reenvío DWORD; //PBYTE
Función DWORD; // PDWORD
DWORD Ordinal;
DWORD Dirección de datos; // PIMAGE_IMPORT_BY_NAME
}u1;
Cada IMAGE_THUNK_DATA es una unión DWORD que efectivamente solo tiene uno de 2 valores. En el archivo en el disco, contiene el ordinal de la función importada o un RVA para una estructura IMAGE_IMPORT_BY_NAME . Una vez cargados, los señalados por FirstThunk se sobrescriben con las direcciones de las funciones importadas; esto se convierte en la tabla de direcciones de importación.
Cada estructura IMAGE_IMPORT_BY_NAME se define de la siguiente manera:
estructura typedef _IMAGE_IMPORT_BY_NAME {
PALABRA Pista;
BYTE Nombre[1];
}
Sugerencia : contiene el índice en la tabla de direcciones de exportación de la DLL en la que reside la función. Este campo es para uso del cargador PE para que pueda buscar la función en la tabla de direcciones de exportación de la DLL rápidamente.
Nombre1 : contiene el nombre de la función importada. El nombre es una cadena ASCII terminada en nulo.
Funciones exportadas solo por ordinal
Como comentamos en la sección de exportación, algunas funciones se exportan únicamente por ordinal. En este caso, no habrá una estructura IMAGE_IMPORT_BY_NAME para esa función en el módulo de la persona que llama. En cambio, IMAGE_THUNK_DATA para esa función contiene el ordinal de la función.
Importaciones consolidadas
Cuando el cargador carga un archivo PE en la memoria, examina la tabla de importación y carga las DLL requeridas en el espacio de direcciones del proceso. Luego recorre la matriz señalada por FirstThunk y reemplaza IMAGE_THUNK_DATA con las direcciones reales de las funciones de importación. Este paso lleva tiempo. Si de alguna manera el programador puede predecir las direcciones de las funciones correctamente, el cargador de PE no tiene que corregir los IMAGE_THUNK_DATA cada vez que se ejecuta el archivo PE, ya que la dirección correcta ya está ahí. La vinculación es producto de esa idea.
El directorio Bound_Import
La información que utiliza el cargador para determinar si las direcciones vinculadas son válidas se mantiene en una estructura IMAGE_BOUND_IMPORT_DESCRIPTOR.
El cargador
Cuando se ejecuta un ejecutable, el cargador de Windows primero crea un espacio de direcciones virtual y real para el proceso y asigna el ejecutable desde el disco al espacio de direcciones del proceso. Intenta cargar la imagen en la dirección base preferida pero la reubica si esa dirección ya está ocupada. El cargador revisa la tabla de secciones y asigna cada sección en la dirección calculada agregando el RVA de la sección a su dirección base.
Luego se verifica la tabla de importación y cualquier otra DLL requerida se asigna al espacio de direcciones del proceso. Después de ubicar y asignar todas las DLL, verificará la sección de exportación de cada DLL y se fijará IAT para que apunte a la dirección de función importada real. Una vez que se cargan todos los módulos cargados, la ejecución pasa al punto de entrada de la aplicación.
Automatización
Primero, desarrollaremos un analizador IAT utilizando la API de WINDOWS a través de c/c++. Extraeremos información relevante sobre un archivo.
ImportParser.exe abc.exe
Nombre DLL: KERNEL32.DLL
Función: cargar biblioteca A
Función: GetProcAddress
Función: VirtualProtect
Función: VirtualAlloc
Función: VirtualGratis
Función: Proceso de salida
Nombre DLL: GDI32.dll
Función: BitBlt
Encuentre el código aquí.
Recuerde: si está compilando el código en 32 bits, entonces debe ejecutarlo en un sistema de 32 bits solo con DEP y ASLR deshabilitados.
También podemos imprimir la misma información usando Python ( http://stackoverflow.com/questions/19325402/getting-iat-and-eat-from-pe )
¡Conviértete en un ingeniero inverso certificado!
Obtenga capacitación práctica en vivo sobre análisis de malware desde cualquier lugar y conviértase en un analista certificado de ingeniería inversa. Comienza a aprender
Así que este es el final de esta parte. Discutiremos temas más interesantes como el virus .tls, el desarrollo del empaquetador PE en la próxima serie……….