El Formato PE (Portable Executable), es un formato de archivos ejecutables, bibliotecas DLL, archivos de objetos, entre otros, utilizados por los Sistemas Operativos Windows, en sus versiones de 32 y 64 bits. En sí, estos archivos son una estructura de datos que contiene la información necesaria para ser cargados por el PE Loader del Sistema Operativo y hacerle la vida más fácil al mismo.
Consideremos como funciona CreateProcess, este es el proceso para archivos ejecutables con excepción de las DLL, las cuales se cargan vía LoadLibrary. El procedimiento para iniciar un proceso es el siguiente:
- Se convierten y validan todos los parámetros y flags recibidos por CreateProcess
- Se lee el ejecutable y se cargan todas las secciones, el tamaño de las mismas, así como la posición relativa desde la dirección base (Veremos esto más adelante).
- Se crea el Windows Process Object. En este punto se crea el EProcess, el KProcess y el PEB del proceso en cuestión (Para más información Windows Internals.).
- Se crea el Thread Inicial, así como el stack y su contexto (Para más información Windows Internals).
- Se realiza el proceso de inicialización para el timo de Subsistema Windows que vaya a ser utilizado, enviando a este la información necesaria, como el MANIFEST del archivo a ejecutar, el ID del proceso que crea el mismo o el manejador del proceso y los threads.
- Se inicia la ejecución del Thread Inicial, para dar inicio al nuevo proceso. Este es el punto afectado por flag CREATE_SUSPENDED.
- Inicialización del nuevo proceso desde el Entry Point, valor contenido dentro del PE Header.
Esquema de Creacion de Procesos:
Partiendo del listado anterior, podemos ver la importancia de conocer al menos los puntos básicos del PE Header, los cuales nos facilitarían el trabajo a la hora de realizar ciertas técnicas como el Process Hollowing, IAT Hooking o PE Injection.
Bloques del Formato PE
DOS HEADER
Todo ejecutable en Windows, por cuestiones de compatibilidad, inician con un DOS Header, que va desde la posición 0x00 a la posición 0x3c en el archivo, sin importar si es visto en el archivo con un editor hexadecimal o cargado en memoria (Las diferencias entre la ubicación de las secciones y el código, relativos a la base, es casi idéntica, con ciertas diferencias que veremos más adelante). Para el PE Format, lo importante de esta sección es el Magic Number, que define el mismo como ejecutable, y está ubicado en las primeras dos posiciones del DOS Header, tal cual podremos ver en una muestra tomada de notepad.exe y la ubicación del PE Header, que está en la posición 0x3c. Los valores restantes son necesarios para tener una respuesta en casos de ser ejecutados en entornos DOS (16 bits).
PE HEADER
El PE Header es un poco más “complejo” y necesario para manipular o entender archivos ejecutables de 32 o 64 bits, inicia con otro Magic Number que es “PE”, y como pudimos ver en el DOS Header, debería estar en la posición 248 relativo al inicio del archivo.
La estructura del PE Header, es _IMAGE_NT_HEADERS para 32 bits e _IMAGE_NT_HEADERS64 para 64 bits, conteniendo la siguiente estructura (ref. winnt.h). La diferencia de ambas estructuras radica en el OptionalHeader, de ambas.
- Para sistemas de 32 Bits:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
- Para sistemas de 64 Bits:
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
IMAGE_FILE_HEADER
El File Header, contiene información importante como el tipo de arquitectura que se utilizara en el equipo, tamaño del Optional Header, el cual es necesario para saber en que parte inician las secciones contenidas en el ejecutable, así como el número de secciones.
Los puntos importantes de la misma, mencionados más arriba, son el tipo de arquitectura, manejados en Machine (0x0 relativo al inicio de IMAGE_FILE_HEADER); los cuales pueden ser 0x014c para 32 bits (x86), 0x0200 para Intel Itanium y 0x8664 para 64 bits (x64). Como podemos ver en la imagen anterior, el ejecutable con el que estamos realizando la prueba es un ejecutable de 64 bits. Después de Machine, tenemos el número de secciones, en nuestro caso 7. El timestamp de cuando fue creado el ejecutable, representado como el número de segundos pasados desde el 1ro de enero de 1970. Un puntero a la tabla de símbolos (Requerido para las DLL). El número de símbolos contenidos en la tabla (requerido para las DLL). El tamaño del Optional Header, en nuestro caso 0xf0. Las características que en nuestro caso son dos, IMAGE_FILE_EXECUTABLE_IMAGE (0x0002) e IMAGE_FILE_LARGE_ADDRESS_AWARE (0x0020), los cuales son acumulables.
Estructura C++ para IMAGE_FILE_HEADER:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
IMAGE_OPTIONAL_HEADER
El Optional Header, es un tanto más interesante que el File Header, porque contiene información requerida a la hora de inicializar un proceso (Entry Point), así como el tamaño de alineación en el archivo (como estará escrito en disco) o la memoria (como estará escrito en memoria), el tamaño de la sección de código, de datos inicializados, del stack, de la zona reservada para Heap.
Muestra del Optional Header (0x18 bytes relativos al inicio del PE Header)
En este header, también existe un Magic Number para especificar que tipo de ejecutable es el que contiene el Optional Header, IMAGE_NT_OPTIONAL_HDR32_MAGIC (0x10b) para el Optional Header de 32 bits, IMAGE_NT_OPTIONAL_HDR64_MAGIC (0x20b) para el Optional Header de 64 bits, como nuestro caso, IMAGE_ROM_OPTIONAL_HDR_MAGIC para un Optional Header en roms. Podemos encontrar en el mismo, otros campos importantes como SizeOfCode, SizeOfInitializedData y SizeOfUninitializedData, que contienen el tamaño de la información de la sección de código, de datos inicializados y de datos no inicializados, todos estos alineados con respecto a FileAlignment de esta misma sección. Nos encontramos con quizás uno de los valores más importante a la hora de iniciar ejecución que es AddressOfEntryPoint (Modificado en técnicas como PE Injection), el cual nos dice en que dirección, relativa a la base en memoria, se iniciara la ejecución.
En otro orden, podemos encontrar campos como BaseOfCode que nos dice en que dirección relativa a la base en memoria, iniciara la sección de código, el ImageBase, nos dice que dirección de memoria, será la dirección base en memoria del proceso (Solo aplicable cuando no existe ASRL), y las alineaciones de las secciones, tanto en memoria (SectionAlignment), como en archivo (FileAlignment), tanto como el tamaño del ejecutable en memoria (SizeOfImage), el cual debe estar alineado al valor de SectionAlignment, el tamaño de todos los Headers en el archivo (DOS Header, PE Header, Section Headers) en SizeOfHeaders, el cual debe estar alineado a FileAlignment. Los tamaños del Stack Inicial (SizeOfStackCommit), el total reservado para el Stack (SizeOfStackReserve), aumentando el stack, hasta llegar al tamaño máximo reservado, a medida que sea necesario. Los mismos campos existen para el Heap del Sistema Operativo, en SizeOfHeapCommit y SizeOfHeapReserve, respectivamente.
Al final tendriamos la cantidad de Data Directories en el final del Optional Header (NumberOfRvaAndSizes), asi como cada uno de los Data Directories al final del Optional Header, en el cual se encuentra el famoso IAT, a traves del cual se ejecutan tecnicas como IAT Hooking.
Y por ultimo, para finalizar con la misma, en el Optional Header tenemos el campo de DLLCharacteristics, que permite saber que tipo de posibles protecciones tiene el archivo ejecutable (en nuestro caso 0xc160) y el subsistema donde sera cargado nuestro ejecutable, en nuestro caso IMAGE_SUBSYSTEM_WINDOWS_GUI (2).
Estructura en C++ para IMAGE_OPTIONAL_HEADER:
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
IMAGE_SECTION_HEADER
Saliendo del PE Header, vamos a una sección no menos importante, sin la cual, aunque el sistema sabría que tamaño tiene cada sección, la alineación que debería tener y la arquitectura de la misma, no sabría donde estaría ubicada; para localizar la zona de las secciones, utilizaremos el tamaño del Optional Header guardado en el FILE HEADER y lo sumaremos a la posición inicial del Optional Header. Si el cálculo es correcto, deberíamos obtener el nombre de la primera sección, y podríamos avanzar a través de estas, hasta llegar al total de secciones, también contenidas en el File Header (en nuestro caso 7).
Las veces anteriores iniciábamos con una imagen de como se ve el Header en memoria, en esta ocasión, debido a la repetitividad del Header, iniciaremos mostrando la estructura del mismo. ¿Por qué esto? para tener una mayor comprensión de la estructura de las secciones en el Formato PE.
Estructura en C++ para el Section Header:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Mencionaremos debajo las más comunes y tomaremos como ejemplo la sección “.text” de notepad.exe (Siéntanse en la libertad de abrir el Debugger o editor hexadecimal / los valores a la izquierda, son relativos al inicio de la sección especifica):
Iniciaremos por el nombre de las secciones (Name), el primer byte es un punto (“.”) y el nombre de la sección no puede exceder una longitud de 8 bytes, en caso de ser necesario un nombre de sección mayor, el primer byte será un slash, seguido de un offset en la tabla de Strings del ejecutable.
El Physical Address (Misc.PhysicalAddress) y Virtual Size (Misc.VirtualSize), aunque no lo parezca (Vaya nombres le pusieron), almacenan el tamaño real de la información de la sección en el archivo, antes de ser alineados al valor de File Alignment en el Optional Header.
El Virtual Address (VirtualAddress) contienen la posición relativa a la base del ejecutable cargada en memoria; en este punto será cargada toda la información de la sección, cuando esté en memoria.
SizeOfRawData contiene el valor de la información de la sección en el archivo, alineada al valor de File Alignment en el Optional Header (Debe ser un valor múltiplo de File Alignment para estar correcto). Puede ser validado tomando el VirtualSize y completando hasta el próximo valor múltiplo de File Alignment (0x200)
PointerToRawData contiene la dirección física de la información de la sección en el archivo. Este valor también debe estar alineado con respecto al File Alignment en el Optional Header.
Debe ser múltiplo del File Alignment en Optional Header (0x200)
Los cuatro valores previos a las características, son comunes en archivos “.OBJ”, previos a ser enlazados para crear un ejecutable, contienen las redirecciones, la cantidad de redirecciones y el número de líneas, para archivos ejecutables (“EXE”), el valor es 0.
Las características (Characteristics), al igual que en el File Header, son acumulables y dejan entender al sistema operativo que tipo de secciones son. Las más importantes son IMAGE_SCN_CNT_CODE (0x20), que te especifica que la sección es ejecutable, IMAGE_SCN_CNT_INITIALIZED_DATA (0x40) que marca la sección como una sección de data inicializada, IMAGE_SCN_CNT_UNINITIALIZED_DATA (0x80) que marca la sección como una sección de data no inicializada. IMAGE_SCN_MEM_EXECUTE (0x20000000) para secciones ejecutables, IMAGE_SCN_MEM_READ (0x40000000) para secciones con permiso de lectura, IMAGE_SCN_MEM_WRITE (0x80000000) para secciones con permiso de escritura.
La sección es ejecutable, de código y con permisos de lectura.
Referencias
Quien guste profundizar mas (Espero que esten motivados), puede darse un paseo por los capitulos 5 de Windows Internals 6 - Part 1, y capitulo 3 de Windows Internals 7 - Part 1. Microsoft provee como capitulo de prueba, el capitulo 5 de Windows Internals 6, les dejo el link mas abajo. Tampoco esta demas echarle un ojo a la documentacion de las estructuras del PE Format, todas estan contenidas en la libreria winnt.h (Importada por la libreria windows.h); por ultimo, como extra, podrian darle un vistazo a un articulo de 1994 de Matt Pietrek, sobre Windows PE Format (link 3).