El Formato PE (Portable Executable) Parte I

El Formato PE fue diseñado con la intención de poder generar archivos ejecutables compatibles con cualquier versión del sistema Windows que trabajen sobre procesadores de 32 y 64 bits. En este post mostraremos métodos que permitan comprender su estructura para que sirva en otras metodologías de análisis como ingeniería reversa de amenazas por ejemplo aunque no está limitado sólo a eso. Para un mejor aprendizaje abarcaremos los siguientes puntos:

  1. Herramientas de visualización.
  2. Encabezado MZ
  3. Encabezado MS-DOS Stub
  4. Encabezado PE
  5. Encabezado Opcional
  6. Encabezados de Sección
  7. Secciones

 

1.       Herramientas de visualización

El primer paso es contar con al menos una herramienta que nos permita interpretar el formato binario en que se encuentran escritos los archivos ejecutables. Para Windows podemos utilizar un editor hexadecimal llamado HexEdit, lo puedes descargar desde http://www.hexedit.com. Si ocupas Linux  puedes usar el comando “hexdump”,” hd” ú ”od” disponible en varias distribuciones como CentOS y Debian.

Ya que contamos con alguna de estas herramientas para visualizar un archivo ejecutable, el siguiente paso es abrirlo. Para ejemplificar esto utilizaremos como ejecutable la calculadora de Windows que se encuentra en C:\WINDOWS\System32\calc.exe.  Con HexEdit sólo basta el arrastrar el archivo a la interfaz gráfica o abrirlo desde la aplicación. En Linux tenemos la opción de visualizar el archivo por bloques utilizando el siguiente comando, lo que nos mostrará únicamente los primeros 32 bytes del archivo:

# hexdump –C –n 32 calc.exe

En el comando, los parámetros indican lo siguiente:

-C Permite observar los caracteres hexadecimales, al mismo tiempo que su representación legible en ASCII.

-n 32 Aquí podemos indicar el número de bytes que se deben mostrar del archivo, de no ponerlo, el comando despliega el archivo completo.

 

2.       Encabezado MZ

pe1Llegados a este punto es cuando podemos comenzar a interpretar las diferentes secciones del formato PE.

Cualquier archivo ejecutable, incluidos los archivos DLL, comienzan con la combinación 0x4D5A cuyo equivalente en ASCII es MZ por las siglas de Mark Zbikowski, uno de los desarrolladores de MS-DOS. Esta combinación es la que permite identificar a este archivo como ejecutable ya sea un EXE o una DLL. Si abrimos otros ejecutables podremos confirmar que siempre los primeros dos Bytes coinciden con esta nomenclatura e incluso si abrimos archivos DLL como C:\Windows\System32\User32.dll ó c:\Windows\System32\kernel32.dll veremos que los primeros 2 Bytes son siempre 0x4D5A. Si modificamos este valor por cualquier otro Windows comenzará por dejar de reconocer a este archivo como un PE y a pesar de que no hemos cambiado su contenido, Windows no podrá ejecutarlo.

Observe también que esta combinación se compone de 2 Bytes. Siendo cada Byte equivalente a 8 bits, tenemos que esta sección corresponde a 16 bits y además esta información se encuentra en el “Offset” 0. Estos conceptos aunque simples, son importantes ya que su buen entendimiento permitirá identificar de forma exacta las diferentes secciones del formato. Las diferentes secciones del formato generalmente harán referencia al contenido de los campos por su tamaño en bits y por su ubicación con respecto del inicio del archivo o con respecto del inicio de una sección, por eso es importante poder ubicar una sección con respecto del offset indicado.

 

3.       Encabezado MS-DOS Stub

Los Bytes que corresponden a las direcciones 0x18 a 0x1B representan el Offset en el cuál encontraremos la información del encabezado MS-DOS ó “MS-DOS Stub”. Tome en cuenta que los procesadores Intel escriben los datos en orden inverso, a este tipo de codificación se le llama “little-endian” por lo que el valor actual de estas direcciones es 0x00000040. Esto significa que debemos desplazarnos hasta esta dirección para encontrar la siguiente sección del encabezado. En Linux podemos mostrar únicamente esta sección con el siguiente comando que muestra el contenido a partir del offset 0x40 e incluye los 56 Bytes que componen al MS-DOS Stub.

# hexdump -C -s 0x40 -n 64 calc.exe

pe2Este valor es una leyenda heredada que sólo indica que el archivo no puede ser ejecutado desde la línea de comandos de MS-DOS y existe en todos los archivos ejecutables PE.

Con la información que tenemos hasta ahora podemos comenzar a armar el formato PE:

pe3Como se puede observar, esta información es estática, lo que significa que sus valores siempre son los mismos sin importar el ejecutable que se esté analizando.

 

4.       Encabezado PE

La siguiente sección del formato la encontraremos siempre en la dirección 0x3C y corresponde a lo que ya es formalmente el encabezado PE.

pe4El contenido de las direcciones 0X3C a 0x3F nos indica que el encabezado PE se encuentra en el offset 0xF0 por lo que debemos brincar ahora a esta dirección para continuar la lectura.

pe5La información de este encabezado nos indica varios atributos referentes a la composición del archivo. Para que queden claras las describiremos una por una:

Signature (Firma PE)

pe6Esta sección comienza siempre con una secuencia de 4 Bytes (32 bits) con valor 0x50450000 (P E NULL NULL), recuerde que en “little-endian” los valores se leen de derecha a izquierda. Este valor sólo sirve para identificar el inicio de la sección y para confirmarle al sistema que está tratando con una imagen ejecutable.

Machine (Tipo de Máquina)

pe7Los siguientes dos Bytes nos indican el tipo de CPU sobre el que puede ejecutarse el archivo. En nuestro caso el valor es 0x014C lo que hace referencia a un procesador x86. La siguiente lista muestra los diferentes valores que puede tener este campo:

Constant                                                              Value           Description

IMAGE_FILE_MACHINE_UNKNOWN                0x0               Cualquier tipo de máquina

IMAGE_FILE_MACHINE_AM33                         0x1d3          Matsushita AM33

IMAGE_FILE_MACHINE_AMD64                      0x8664        x64

IMAGE_FILE_MACHINE_ARM                           0x1c0          ARM little endian

IMAGE_FILE_MACHINE_ARMNT                      0x1c4          ARMv7 (or higher) Thumb mode only

IMAGE_FILE_MACHINE_ARM64                      0xaa64        ARMv8 in 64-bit mode

IMAGE_FILE_MACHINE_EBC                             0xebc          EFI byte code

IMAGE_FILE_MACHINE_I386                          0x14c           Intel 386 or later and compatible processors

IMAGE_FILE_MACHINE_IA64                           0x200          Intel Itanium processor family

IMAGE_FILE_MACHINE_M32R                         0x9041        Mitsubishi M32R little endian

IMAGE_FILE_MACHINE_MIPS16                      0x266          MIPS16

IMAGE_FILE_MACHINE_MIPSFPU                    0x366          MIPS with FPU

IMAGE_FILE_MACHINE_MIPSFPU16               0x466          MIPS16 with FPU

IMAGE_FILE_MACHINE_POWERPC                 0x1f0           Power PC little endian

IMAGE_FILE_MACHINE_POWERPCFP             0x1f1           Power PC with floating point support

IMAGE_FILE_MACHINE_R4000                        0x166          MIPS little endian

IMAGE_FILE_MACHINE_SH3                             0x1a2          Hitachi SH3

IMAGE_FILE_MACHINE_SH3DSP                      0x1a3          Hitachi SH3 DSP

IMAGE_FILE_MACHINE_SH4                             0x1a6          Hitachi SH4

IMAGE_FILE_MACHINE_SH5                             0x1a8          Hitachi SH5

IMAGE_FILE_MACHINE_THUMB                      0x1c2          ARM or Thumb (“interworking”)

IMAGE_FILE_MACHINE_WCEMIPSV2              0x169          MIPS little-endian WCE v2

 

NumberOfSections (Número de secciones)

pe8Los siguientes dos Bytes indican el número de secciones que en nuestro caso es de 0x0003 que como veremos más adelante corresponden a .text, .data y .rsrc.

 

TimeDateStamp (Etiqueta de Fecha y Hora)

pe9Los siguientes cuatro Bytes indican la fecha de creación del archivo, en nuestro caso el valor es de 0x3B7D8410. El equivalente decimal a este valor nos indica el número de segundos que han transcurrido desde las 00:00:00 del 1º de Enero de 1970. En Linux se pueden ejecutar los siguientes comandos para traducir este número a la fecha correspondiente:

[root ~]# echo $((0x3b7d8410))
998081552
[root ~]# date -d @998081552
Fri Aug 17 15:52:32 CDT 2001

PointerToSymbolTable y NumberOfSymbols (Puntero a la tabla de símbolos y número de símbolos)

pe10Los siguientes ocho bytes corresponden a funciones de debug que generalmente están deshabilitados. Los primeros 4 equivalen al puntero para la tabla de símbolos y los 4 restantes al número de símbolos.

SizeOfOptionalHeader (Tamaño del Encabezado Opcional)

pe11Los siguientes 2 Bytes corresponden al tamaño del encabezado opcional que en nuestro ejemplo corresponde a 0x00E0 (224 Bytes). Este valor es utilizado para validar que la estructura actual del archivo permanecerá dentro de los límites definidos al subirlo en memoria. El término opcional es relativo porque de hecho es obligatorio para todos los ejecutables, en archivos de objetos el valor debe ser cero. Su tamaño también, como veremos más adelante, para determinar el inicio de los encabezados de sección, ya que estos no cuentan con un puntero específico.

Characteristics (Características)

pe12Los siguientes 2 Bytes corresponden a la sección de características. Cada bit equivale a un flag que indica una característica específica del archivo. Para poder interpretar este valor es necesario hacer la traducción binaria del valor y comparar el resultado contra la definición de cada flag. Tome en cuenta que la posición del bit es importante porque esta sección debe interpretarse en un orden específico.

El valor de nuestro archivo calc.exe es 0x010F por lo que su representación es como sigue:

pe13Los 16 valores a los que corresponde cada flag se describen a continuación.

0   –   IMAGE_FILE_RELOCS_STRIPPED. Un valor de “1” indica que el archivo no contiene información de relocalización por lo que debería ser colocado en la dirección base preferida (más sobre esto adelante).

1   –   IMAGE_FILE_EXECUTABLE_IMAGE. Indica si el archivo es válido y se puede ejecutar.

2   –   IMAGE_FILE_LINE_NUMS_STRIPPED. Indica si se han eliminado los números de línea.

3   –   IMAGE_FILE_LOCAL_SYMS_STRIPPED. Indica si se han removido las tablas que hacen referencia a símbolos locales.

4   –   IMAGE_FILE_AGGRESSIVE_WS_TRIM. Indica si se debe limitar de forma agresiva la cantidad de RAM utilizada por el proceso.

5   –   IMAGE_FILE_LARGE_ADDRESS_AWARE. Indica si la aplicación puede manejar más de 2 GB de direcciones.

6   –   NO UTILIZADO

7   –   IMAGE_FILE_BYTES_REVERSED_LO. Indica si la codificación es “little-endian” donde el bit menos significativo precede al más significativo en memoria.

8   –   IMAGE_FILE_32BIT_MACHINE. Indica que la ejecución del archivo es para máquinas de 32 bits.

9   –   IMAGE_FILE_DEBUG_STRIPPED. Indica si se ha eliminado la información de debug.

10   – IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP. Si el archivo se encuentra en un medio extraible como un CD ó USB, indica si se debe copiar primero a un archivo temporal (swapfile) y ejecutarlo desde esa copia.

11   – IMAGE_FILE_NET_RUN_FROM_SWAP. Si el archivo no puede ser ejecutado desde la red, indica que se debe copiar primero a un archivo temporal (swapfile) y ejecutarlo desde esa copia.

12   – IMAGE_FILE_SYSTEM. Indica que se trata de un archivo de sistema y no un programa de usuario.

13   – IMAGE_FILE_DLL. El archivo es una DLL.

14   – IMAGE_FILE_UP_SYSTEM_ONLY. Indica si el archivo debe ejecutarse sólo en máquinas con un procesador.

15   – IMAGE_FILE_BYTES_REVERSED_HI. “Big-Endian”. Indica si el bit más significativo debe preceder al menos significativo.

Hasta este punto ya contamos con información para descifrar el Encabezado de Archivo.

pe14

5.       Optional Header (Encabezado Opcional)

Ahora continuamos con la sección de Encabezado Opcional cuyos Bytes comienzan inmediatamente después del Encabezado de Archivo.

Este encabezado también se compone de diversas secciones que analizaremos una por una a continuación:

Magic number (Número mágico)

pe15Los primeros dos Bytes sirven para saber la arquitectura bajo la cual es compatible el archivo. Un valor de 0x010B indica una arquitectura de 32 bits, 0x020B indica plataformas de 64 bits o superiores y 0x0107 indica que el archivo es una imagen ROM.

MajorLinkerVersion y MinorLinkerVersion (Versiones menor y mayor de Linker)

pe16Los siguientes dos Bytes indican cada uno la versión mayor y menor del linker utilizado. En nuestro caso 0x00 para la versión menor y 0x07 para la mayor.

SizeOfCode (Tamaño de código)

pe17

Los siguientes 4 Bytes indican cuál es el tamaño de la sección de código (.text) ó de la suma de todas las secciones de código en caso de haber varias. En nuestro ejemplo el valor es 0x00012800.

SizeOfInitializedData (Tamaño de los datos de inicialización)

pe18Los siguientes 4 Bytes indican el tamaño de la sección de datos de inicio. En nuestro ejemplo es de 0x00009600.

SizeOfUninitializedData (Tamaño de los datos no inicializados)

pe19Los siguientes 4 Bytes indican el tamaño de la sección de datos no iniciados. En nuestro ejemplo es de 0x00000000.

AddressOfEntryPoint (Dirección de Punto de Entrada)

pe20aLos siguientes 4 Bytes indican la dirección de entrada o EntryPoint (EP). Esta dirección es un RVA (Relative Virtual Address), significa que es una dirección relativa con respecto de la dirección base. Para encontrar en memoria esta dirección debemos sumar su valor al valor de la dirección base. En nuestro ejemplo el EP es 0x00012475. Esta es una de las secciones más importantes ya que en archivos ejecutables indica cómo encontrar la primera instrucción de ejecución en memoria. Para controladores, esta dirección apunta a la función de inicialización. En archivos DLL este parámetro es opcional y de no usarse su valor debe ser 0.

BaseOfCode (Base del código)

pe21Los siguientes 4 Bytes indican la dirección relativa de la base del código. Su valor es 0x00001000.

BaseOfData (Base de datos)

pe22Los siguientes 4 Bytes indican la dirección relativa al inicio de los datos. Su valor es 0x00014000. Es usada sólo para archivos PE32.

ImageBase (Base de imagen)

pe23Los siguientes 4 Bytes indican la dirección base de la imagen. Si dicha dirección está disponible en memoria al momento de ejecución, el código se cargará a partir de ahí, de lo contrario se tendrá que relocalizar a otra dirección disponible. Su valor en nuestro ejemplo es 0x01000000.

SectionAlignment (Alineación de sección)

pe24Los siguientes 4 Bytes indican la alineación de las secciones cuando son cargadas en memoria. Su valor debe ser mayor o igual a FileAlignment y generalmente toma el tamaño de página de la arquitectura de CPU. Su valor es 0x00001000 (4096 Bytes en Decimal que corresponde a una página en CPU’s de 32 bits).

FileAlignment (Alineación de archivo)

pe25Los siguientes 4 Bytes indican el factor de alineación utilizado para alinear los datos crudos de las secciones de datos en el archivo. Los valores deben ser potencias de 2 entre 512 y hasta 64K. El valor por defecto es de 512 Bytes. En nuestro ejemplo el valor es 0x00000200 (512 Bytes en Decimal).

MajorOperatingSystemVersion y MinorOperatingSystemVersion (Versión menor y mayor del SO)

pe26Los siguientes 4 Bytes indican cada par, la versión menor y mayor del sistema operativo esperado para la ejecución del archivo. El valor de la versión mayor en nuestro ejemplo ex 0x0005 y la versión menor es 0x0001.

MajorImageVersion y MinorImageVersion (Versión menor y mayor del archivo)

pe27Los siguientes 4 Bytes, indican cada par, la versión mayor y menor del archivo. En nuestro ejemplo, la versión mayor es 0x0005 y la menor es 0x0001.

MajorSubsystemVersion y MinorSubsystemVersion (Versión menor y mayor de subsistema)

pe28Los siguientes 4 Bytes, indican cada par, la versión mayor y menor del subsistema esperado para la ejecución del archivo. En nuestro ejemplo la versión mayor es de 0x0004 y la menor 0x0000.

Win32VersionValue (Valor de versión Win32)

pe29Los siguientes 4 Bytes son un valor reservado y es siempre 0x00000000.

SizeOfImage (Tamaño de la imagen)

pe30Los siguientes 4 Bytes indican el tamaño de la imagen incluyendo todos los encabezados cuando la imagen es cargada en memoria. Su valor debe ser un múltiplo de SectionAlignment. Su valor en nuestro ejemplo es 0x0001f000.

SizeOfHeaders (Tamaño de encabezados)

pe31Los siguientes 4 Bytes indican el tamaño de todos los encabezados redondeado a un múltiplo de FileAlignment. Su valor es 0x00000400 (1024 Bytes).

CheckSum

pe32Los siguientes 4 Bytes indican el valor del checksum del archivo. El algoritmo utilizado se toma de la librería IMAGEHELP.DLL. Este valor debería ser validado únicamente al momento de cargar alguno de los siguientes archivos: todos los controladores, cualquier DLL cargada en el inicio de sistema (boot) y cualquier DLL cargada en un proceso crítico de Windows. Su valor es 0x000264e9.

Subsystem (Subsistema)

pe33Los siguientes 2 Bytes indican el tipo de subsistema necesario. Su valor es de  0x0002.

DllCharacteristics (Características de DLL)

pe34Los siguientes 2 Bytes indican el punto de entrada para las DLL. Su valor es de 0x8000.

SizeOfStackReserved (Tamaño del Stack Reservado)

pe35Los siguientes 4 Bytes indican el tamaño que se debe reservar en la pila. Su valor es de 0x00040000.

SizeOfStackCommit (Tamaño utilizado por la pila)

pe36Los siguientes 4 Bytes indican el tamaño usado en memoria. Su valor es 0x00001000.

SizeOfHeapReserved (Tamaño reservado para el Heap)

pe37Los siguientes 4 Bytes indican el tamaño reservado para el heap. Su valor es 0x00100000.

SizeOfHeapCommit (Tamaño usado por el Heap)

pe38Los siguientes 4 Bytes indican el tamaño usado en memoria por el heap. Su valor es 0x00001000.

LoaderFlags (Banderas del Loader)

pe39Los siguientes 4 Bytes indican el tamaño de los loaderFlags. Su valor es reservado y siempre es cero.

NumberOfRvaAndSizes (Número de direcciones RVA y sus tamaños)

pe40Los siguientes 4 Bytes indican el número y tamaño de entradas en el resto del encabezado opcional. Cada uno describe una ubicación y un tamaño. Su valor es 0x00000010, lo que significa que hacen falta aún 16 entradas más para que el encabezado termine, dichas entradas corresponden a los 16 directorios de datos que veremos a continuación.

Data Directories (Directorios de Datos)

pe41Ya que contamos con el valor de NumberOfRvaAndSizes debemos contar 16 secciones de 8 Bytes cada una para completar el encabezado opcional. Cada una de las 16 entradas corresponde a un directorio de datos específico que se presentan en la siguiente lista. De los 8 Bytes, los cuatro primeros indican la posición relativa en memoria (RVA) de dicha tabla y los 4 restantes indican el tamaño de la tabla. El contenido de estas tablas se refiere a datos que Windows carga en memoria al momento de ejecución del archivo. Observe que en el caso de calc.exe sólo se usan 5 de los 16 directorios disponibles (con letra roja).

Export Table                             Tabla de datos exportados (.edata)

Import Table                             Tabla de datos importados (.idata)

Resource Table                        Tabla de recursos (.rsrc)

Exception Table                       Tabla de excepciones (.pdata)

Certificate Table                      Tabla de certificados de atributos

Base Relocation Table            Tabla de relocalización de base (.reloc)

Debug                                        Tabla de datos de debug (.debug)

Architecture                            Reservado, debe ser 0

Global Ptr                                  El RVA del valor a grabar en el Global Pointer, su tamaño debe ser cero.

TLS Table                                   Tabla de almacenamiento local de threads (.tls)

Load Config Table                    Tabla de carga de configuración

Bound Import                           Tabla Bound Import

IAT                                              Tabla de importación de direcciones

Delay Import Descriptor        Tabla de descripción de importaciones con retraso

CLR Runtime Header              Encabezado CLR Runtime (.cormeta)

NULO                                         Tabla reservada, debe estar en ceros

 

Si agregamos esta información a nuestro modelo del formato PE, podemos agregar esta sección como se muestra en la siguiente imagen:

pe42

En la segunda parte de este Post El Formato PE (Portable Executable) Parte II mostraremos los campos restantes y su interpretación.

Recuerda que puedes mandarnos tus preguntas y comentarios a nuestra cuenta de Twitter: @redinskala donde encontrarás más información y tips de seguridad.

Gracias por tu visita!

 

2 thoughts on “El Formato PE (Portable Executable) Parte I

  1. Pingback: El Formato PE (Portable Executable) Parte II | RedinSkala

  2. Pingback: Cómo encontrar el Entry Point en Memoria y Disco | RedinSkala

Comments are closed.