Blog técnico Entelgy Innotec Security
images/banners/security-garage-banner-04_300.png

¡Hola, lectores!

Hoy os ofrecemos un post que seguro que encontraréis muy interesante y de utilidad.

Y es que, en este artículo vamos a revisar las diferentes estructuras que existen en un fichero PE. En concreto, aquellas que se encargan de especificar qué funciones han de ser importadas una vez este fichero sea ejecutado, y también aquellas que se encargan de especificar qué funciones exporta una DLL.

Pero antes, os ponemos en contexto. El proceso de escribir esta serie de artículos comenzó durante la ejecución de los laboratorios del curso EXP-301. Durante uno de los módulos, se trata en profundidad el desarrollo de shellcodes personalizadas, aunque durante el módulo, se cubre únicamente el uso de la tabla EAT de las diferentes DLLs importadas en un proceso para localizar funciones interesantes que permitirán, más adelante, desarrollar otras acciones, como ejecutar comandos, abrir sockets, etc.

Una aproximación muy común para parsear la tabla EAT de una DLL sería la siguiente:

  1. Localizar la dirección de Kernelbase.dll
  2. Iterar sobre la estructura AddrOfNames de un DLL hasta que localices el string “GetProcAddress”
  3. Localizar el ordinal asociado a “GetProcAddress”
  4. Utilizar dicho ordinal para localizar la dirección real de GetProcAddress
  5. Utilizar GetProcAddress para llamar a otras funciones.

Para realizar estas acciones se debe parsear la siguiente estructura.

typedef struct _IMAGE_EXPORT_DIRECTORY {
ULONG Characteristics;
ULONG TimeDateStamp;
USHORT MajorVersion;
USHORT MinorVersion;
ULONG Name;
ULONG Base;
ULONG NumberOfFunctions;
ULONG NumberOfNames;
ULONG ddressOfFunctions;
ULONG AddressOfNames;
ULONG AddressOfNameOrdinals;}
IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Esta estructura almacena un puntero a tres arrays diferentes e importantes, que son los siguientes:

  1. AddressOfFunctions
  2. AddressOfNames
  3. AddressOfNameOrdinals

Profundizando en lo explicado anteriormente, para localizar funciones mediante la EAT de una DLL se han de seguir los siguientes pasos:

  • Para empezar, se debe iterar el array AddressOfNames, hasta que se localiza la cadena GetProcAddress, momento en el que se detiene la iteración y se guarda el índice en el que se encuentra GetProcAddress en el array.
  • En segundo lugar, se utilizará este índice para ubicar el ordinal de la función. Este ordinal consiste en un índice que indica en qué posición del array AddressOfFunctions se encuentra la dirección real de GetProcAddress
  • Finalmente, utilizando el ordinal, se extrae la dirección real de GetProcAddress del array AddressOfFunctions.

Otros artículos explican en profundidad este proceso, como por ejemplo el siguiente, en el cual el proceso es explicado en profundidad:

https://xen0vas.github.io/Win32-Reverse-Shell-Shellcode-part-2-Locate-the-Export-Directory-Table

Practicando esta técnica, apareció la siguiente idea:

En vez de utilizar la EAT de una DLL importada por el proceso explotado, ¿sería posible utilizar la IAT del propio proceso?

Explicación

Antes de nada, ha de ser aclarado que todos los procesos corriendo en un sistema operativo Windows tienen la DLL Kernel32/Kernelbase cargada en memoria. Es por esto, que no es una fantasía que se pueda utilizar la IAT del proceso explotado para resolver las mismas funciones que se resuelven mediante la EAT.

Durante el post vamos a estar trabajando con las siguientes dos estructuras:

La primera es _IMAGE_IMPORT_DESCRIPTOR y almacena información acerca de los nombres de las DLL importadas, un puntero a la IAT y un puntero a los nombres de las funciones importadas.

A continuación tenemos la definición:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
_ANONYMOUS_UNION union {
ULONG Characteristics;
ULONG OriginalFirstThunk;
} DUMMYUNIONNAME;
ULONG TimeDateStamp;
ULONG ForwarderChain;
ULONG Name;
ULONG FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

La segunda estructura es en sí misma la IAT (_IMAGE_IMPORT_BY_NAME), la cual como se verá durante este articulo, es diferente cuando el archivo ejecutable reside en el disco, que cuando el archivo está cargado en memoria.

En disco tendría la siguiente definición:

typedef struct _IMAGE_IMPORT_BY_NAME {
USHORT Hint;
UCHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Depurando esas estructuras con Windbg y con X32DBG

Todo el trabajo se realizará con WinDBG, pero por razones de display, a veces usaremos x32dbg para inspeccionar la memoria.

Para empezar cargamos nuestro programa (asm_iat_parse.exe) en windbg emulando un proceso suspendido.

Veremos cómo calcular la dirección de IAT y usaremos esos pasos para comparar un proceso que aún no se ha inicializado por completo con un proceso en ejecución.

Para ello utilizamos la siguiente línea de comando:

windbg.exe -le:ntdll.dll asm_parse_iat.exe

Esta línea de comando abrirá asm_parse_iat.exe antes de que se inicie RtlUserThreadStart, por lo que el IAT de la imagen aún estará intacta.

Con esto, procedamos a buscar el IAT manualmente usando windbg. Accederemos a la estructura _TEB, para eso usamos el registro fs:[0]. Como podemos ver, _TEB tiene un puntero a _PEB en la posición 0x30

dt _TEB
ntdll!_TEB
+0x000 NtTib            : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId         : _CLIENT_ID
+0x028 ActiveRpcHandle  : Ptr32 Void

+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
+0x034 LastErrorValue   : Uint4B
+0x038 CountOfOwnedCriticalSections : Uint4B
+0x03c CsrClientThread  : Ptr32 Void
+0x040 Win32ThreadInfo  : Ptr32 Void

Visto esto, necesitamos leer el contenido de fs:[0x30] para acceder a la estructura PEB

Teniendo la dirección base de la imagen, analizaremos _IMAGE_DOS_HEADER.

Pero antes de continuar, guardemos la dirección de ImageBase en un registro temporal de WinDBG

r @$t0 = poi(poi(fs:[0x30])+0x008)

Esto se usará más adelante para calcular otras direcciones que son relativas a la dirección de la base del ejecutable.

Haciendo esto no necesitaremos hardcodear dicha dirección y esta técnica será escalable a otros binarios.

Ahora procedamos a calcular el offset a _IMAGE_NT_HEADERS, usando la posición 0x30 de_IMAGE_DOS_HEADER, la cual almacena un offset hacia _IMAGE_NT_HEADERS, y añadiendo la dirección base del ejecutable, que guardamos previamente en el pseudo-registro temporal de WinDBG.

Tras esto, accederemos a _IMAGE_OPTIONAL_HEADER->DirectoryEntry, para obtener la dirección de _IMAGE_OPTIONAL_HEADER. Para esto utilizaremos el siguiente campo_IMAGE_NT_HEADERS->OptionalHeader, el cual se encuentra en _IMAGE_NT_HEADERS+0x030

Y finalmente, solo necesitamos obtener la dirección de DataDirectory, la cual en la posición 1 guarda un puntero hacia la estructura _IMAGE_IMPORT_DESCRIPTOR.

Como aclaración, para acceder a _IMAGE_IMPORT_DESCRITOR hemos de acceder a la posición 1 de  DataDirectory (DataDirectory[1]), que tiene la siguiente definición:

0:000> dt _IMAGE_DATA_DIRECTORY
ntdll!_IMAGE_DATA_DIRECTORY
+0x000 VirtualAddress   : Uint4B
+0x004 Size             : Uint4B

Por lo que para acceder a DataDirectory[1], hemos de acceder a “DataDirectory+0x8”, con lo que podremos obtener la dirección de _IMAGE_IMPORT_DESCRIPTOR, y una vez sumada esta a nuestro registro temporal de WinDBG (base del ejecutable), estaremos en disposición de acceder finalmente a esta estructura.

Como resultado, una vez localizada esta estructura, podemos proceder a resolver el nombre de la primera DLL que será importada por este ejecutable, que se trata de VCRUNTIME140.dll

También podemos obtener la dirección de la IAT, que en este caso es la misma para OriginalFirstThunk y FirstThunk. Esto ocurre porque el proceso no está completamente inicializado, pero una vez que lo esté por completo, FirstThunk pasará a apuntar a las direcciones de las funciones importadas, y OriginalFirstThunk continuará apuntando a la estructura _IMAGE_IMPORT_BY_NAME (IAT)

Una vez el proceso es inicializado, el array OriginalFirstThunk conservará su estructura actual, apuntando a los mismos datos que antes de ser iniciado, pero en el caso de FirstThunk, el cual se encuentra en _IMAGE_IMPORT_DESCRIPTOR+0x10, apuntará a las direcciones importadas, ordenadas exactamente igual que en _IMAGE_IMPORT_BY_NAME (OriginalFirstThunk)

El siguiente diagrama explica gráficamente esta idea.

Podemos ver cómo se modifican esas direcciones cuando se carga el proceso, pero en lugar de usar WinDBG, usaremos x32dbg y estableceremos un breakpoint de hardware en las direcciones de FirstThunk.

Con esto podemos observar cómo el array de la IAT se sobrescribe con las direcciones reales de las funciones.

La imagen previa muestra como la IAT apunta a _IMAGE_IMPORT_BY_NAME antes de ser sobreescrita por el loader. En cambio, la siguiente imagen mostrará cómo esta estructura cambia una vez el proceso es inicializado.

Finalmente, podemos ver cómo esta dirección apunta a la función GetModuleFileName de VCRUNTIME140.dll que, como vimos previamente, es la primera función importada por el proceso.

Conclusión

Hemos visto cómo se pueden analizar las “import structures” (arrays) para obtener el índice (posición) del nombre de una función en dicho array, y usar esto para localizar finalmente la dirección real de dicha función en la IAT.

En comparación con la posibilidad de resolver funciones parseando la estructura EAT de una DLL, este método es mucho más sencillo, porque ahorra pasos y dificultad, lo cual a la hora de realizar una shellcode es siempre interesante, porque nuestro código será más pequeño y eficiente.

En las próximas partes de esta serie veremos cómo implementar esto en C++ y, finalmente, en ensamblador, usándolo para ejecutar código en un entorno real.

El código se encuentra publicado en:

¡Y aquí concluye este análisis sobre revisión de estructuras PE! Esperamos que os haya resultado útil e interesante, y que lo compartáis.

¡Hasta pronto!

Referencias:


Alejandro Pinna Toral

S5 Box