ARCHIVOS EN GENERAL Y EL FORMATO PE
======================================
Un archivo no es más que un conjunto de datos organizados en
varias entidades llamadas "registros". Un registro es lo que conocemos
en programación como "estructura", es decir, un espacio en memoria
que se emplea para guardar datos de manera ordenada.
Una estructura se divide en entidades de tamaño definido destinadas
a almacenar datos de justo ese tamaño. A cada una de estas "celdas"
de la estructura la llamamos campos.
Las estructuras o registros permiten almacenar información,
en sus campos, sobre entidades particulares. En este sentido, la estructura
es una colección de campos que pueden ser tratados como una unidad por algún
programa. Es un bloque de datos organizado en campos sobre una entidad específica.Toda
estructura posee un nombre que la identifica y permite localizarla o manejarla;
este nombre es una varable del tipo de la estrcuctura que nombra y permite
el tratamiento de las entidades de las cuales informa la estrcutura.
Ahora bien, a la información contenida en un archivo se puede
acceder de manera secuencial o directa. El modo secuencial es lento en muchos
casos ya que se necesita revisar cada dato desde el comienzo del archivo con
orden secuencial, hasta dar con el buscado. Por esto, la mayoría de las veces
lo mejor es ir directamente al dato buscado, sin tener que revisar todos los
campos del archivo.
Para facilitar el acceso directo a los datos, se pueden colocar
unos directorios al comienzo del archivo que indiquen dónde se encuentran
ciertos tipos de datos. Los directorios son estructuras cuyos campos, llamados
entradas del directorio, son tablas que tienen la misma estrcutura. Funcionan
como un "directorio" telefónico. Si busco un dato de cierto
tipo, reviso el directorio y veo dónde están agrupados esos datos. Luego voy
a la sección donde se encuentran los datos de este tipo. Seguramente en esta
sección habrá una tabla que me informe sobre lo que hay en ella. Reviso
entonces esta tabla y con lo que me dice, busco el dato que me interesa: no
tengo que ir dato por dato para encontrar lo que busco.
Este es el principio a partir del cual se ideó el formato de
los archivos PE, y facilitar el montaje del programa en la RAM: se coloca
al principio del archivo una serie de estructuras que informan sobre el contenido
del archivo. El contenido del archivo se divide en secciones cada una con
datos de cierto tipo, formando el cuerpo del archivo. Las estructuras al comienzo
del archivo forman el encabezado del archivo y nos dicen la dirección donde
se ubica cada sección, su tamaño y sus atributos. A la vez, cada sección implementa
tablas con información particular sobre el contenido de su cuerpo.
Entonces, en los archivos con formato PE, tenemos un encabezado
y un cuerpo. El encabezado de los archivos PE se subdivide en, podríamos decir,
cuatro subencabezados:
- El encabezado DOS MZ
- El encabezado PE
- El encabezado NT opcional
- El conjunto de tablas de secciones
Veámos la organización en la siguiente tabla:
PE EXE (Windows
32Bit EXE, DLL, OCX, etc)
Encabezado MZ EXE |
Contiene información necesaria para ejecutar
el DOS STUB. Conservado por compatibilidad |
Encabezado DOS |
Encabezado MZ extendido |
El desplazamiento (OFFSET) 3Ch apunta al encabezado
PE |
DOS STUB |
Usualmente despliega 'Requires windows
to run' o un mensaje similar |
Agregado para avisar que el programa rueda
en Windows |
Encabezado PE |
Contiene info necesaria para correr el
programa en Win32 |
Encabezados agregados
por W32 |
Encabezado opcional NT |
Contiene info adicional necesaria para correr el programa
en Win32 |
Tabla de Objetos o Secciones |
Información sobre objetos o secciones en el archivo |
Objetos o secciones |
Datos de las secciones |
Cuerpo del archivo |
El cuerpo del archivo con formato PE se subdivide en un número
no fijo de secciones, cada una de las cuales dividida también en una tabla
de sección, que nos informa sobre el contenido de la sección, y el cuerpo
de sus datos.
Veamos ahora con detenimiento el encabezado.
RASTROS ARCÁICOS: ENCABEZADO DOS MZ
=======================================
Los archivos ejecutables con formato PE inician con el encabezado
DOS MZ, que no es más que el antiguo encabezado de los archivos EXE más algunos
campos adicionales que se agregaron para posibilitar la transición.
El encabezado DOS, conservado por compatibilidad, es el mismo
que empleaban los antiguos programas DOS de 16 bits, más unos campos
adicionales. Su estructura (en ensamblador) es:
_IMAGE_DOS_HEADER STRUC
; /////////////////////////////////////////////////////
; CAMPOS TRADICIONALES
; ////////////////////////////////////////////////////
e_magic DW ?
e_cblp DW ?
e_cp DW ?
e_crlc DW ?
e_cparhdr DW ?
e_minalloc DW ?
e_maxalloc DW ?
e_ss DW ?
e_sp DW ?
e_csum DW ?
e_ip DW ?
e_cs DW ?
e_lfarlc DW ?
e_ovno DW ?
; ////////////////////////////////////////////
; CAMPOS ADICIONALES
; ////////////////////////////////////////////
e_res DW 4 DUP ( ? )
e_oemid DW ?
e_oeminfo DW ?
e_res2 DW 10 DUP ( ? )
e_lfanew DD ?
_IMAGE_DOS_HEADER ENDS
PIMAGE_DOS_HEADER TYPEDEF NEAR PTR _IMAGE_DOS_HEADER
Quien no entienda esta estructura puede orientarse por la siguiente
tabla:
Encabezado
EXE MZ
0000 |
Word |
ID 'MZ' - Etiqueta de archivo EXE |
0002 |
Word |
Número de bytes en la última página o bloque de 512 bytes del ejecutable |
0004 |
Word |
Número de todas las páginas de 512 bytes en el ejecutable (incluyendo
la última) |
0006 |
Word |
Número de entradas de la tabla de relocalizaciones |
0008 |
Word |
Tamaño del encabezado en parágrafos (16 bytes) |
000A |
Word |
Tamaño mínimo de los parágrafos de memoria localizada por encima
del final del programa ya cargado en RAM. |
000C |
Word |
Tamaño máximo de los parágrafos de memoria localizada por encima
del final del programa ya cargado en RAM. |
000E |
Word |
SS (Stack Segment) relativo al inicio del ejecutable |
0010 |
Word |
SP (Stack Pointer) inicial |
0012 |
Word |
Checksum o 0. Valor de verificación de la suma de las palabras
en el ejecutable, usado para verificar la validación por posibles
datos perdidos. |
0014 |
Dword |
CS:IP relativo al inicio del ejecutable (Entry point = Punto de entrada) |
0018 |
Word |
Desplazamiento (offset) de la tabla de relocalización.
40h para los nuevos (NE, LE, LX, W3, PE, etc) ejecutables |
001A |
Word |
Número de traslape (0 = programa principal) |
El primer campo de esta estructura, en el desplazamiento 0000,
hay dos caracteres: "MZ", que indican que se trata de un archivo
ejecutable .EXE.
Si abrimos con HEX WORKSHOP u otro editor hexadecimal un archivo
.EXE de DOS, por ejemplo DEBUG.EXE, generalmente ubicado en el directorio
C:\WINDOWS\COMMAND, veremos en la parte izquierda, en la ventana que despliega
caracteres en ASCII, que en el desplazamiento 0000 hay dos caracteres: 'MZ'.
Es el número mágico que identifica los archivos .EXE.
A este antiguo encabezado EXE MZ se le han agregado algunos
campos que informan al cargador del Sistema Operativo (SO) dónde está el encabezado
PE con información relevante para W32.
Encabezado
MZ Extendido
001C |
Dword |
Tabla de relocalización con un número variable
de reubicación de elementos. |
0020 |
Dword |
Identifuicador OEM |
0024 |
Dword |
Información OEM. |
0028 |
26Bytes |
Reservado. |
003C |
Dword |
Desplazamiento del nuevo encabezado EXE desde el inicio
del archivo o 0 si es un archivo MZ EXE |
El último campo de esta extensión, 'e_lfanew', indica la dirección
donde está la signatura que identifica el formato del archivo. Si se trata
de un archivo con un programa W32, este campo apunta a dos caracteres: "PE"
(Portable Executable), el formato elegido por M$ para los archivos con programas
W32.
Si abrimos NOTEPAD.EXE con con HEX WORKSHOP y revisamos el campo
e_lfanew en el desplazamiento 003Ch, veremos el número 8000h, que al revés
es 0080h, el desplazamiento donde veremos los caracteres 'PE', que identifican
el formato del archivo (Si trabajas con HEX WORKSHOP, no cierres todavía este
archivo). Si ahora abrimos con HEX WORKSHOP el archivo WINFILE.EXE, generalmente
ubicado en el directorio C:\WINDOWS, veremos que el desplazamiento 003Ch apunta
al desplazamiento 0400h, donde encontramos los caracteres 'NE', que es el
formato de los archivos W16.
Inmediatamente después de la signatura hay dos bytes o una palabra
(WORD) con ceros, después de los cuales inicia el encabezado PE. En la actualidad,
el formato NE está practicamente extinguido. Lo pasaré por alto y me concentraré
en el formato PE.
Entre el encabezado MZ DOS y la signatura PE, está la
seccuón "STUB" del archivo, la cual se incluye para el despliegue
de un mensaje que indica que el programa sólo puede correr en Windows.
¿32 BITS?: MÁS CABEZAS
======================
Dos bytes delante de la signatura se inicia el encabezado PE,
cuya estructura es:
_IMAGE_FILE_HEADER STRUC
Machine DW ?
NumberOfSections DW ? ; No. de secciones
TimeDateStamp DD ?
PointerToSymbolTable DD ? ; Dir. de la tabla de símbolos
NumberOfSymbols DD ? ; No. de simbolos
SizeOfOptionalHeader DW ? ; Tamaño del proximo encabezado
Characteristics DW ?
_IMAGE_FILE_HEADER ENDS
PIMAGE_FILE_HEADER TYPEDEF NEAR PTR _IMAGE_FILE_HEADER
IMAGE_SIZEOF_FILE_HEADER EQU 20
Traduzcamos ésta a estructura a una tabla a desplazamientos:
Encabexado
PE
0000 |
Word |
CPU_TYPE = Tipo de CPU
0000 - Desconocido 0162 - MIPS I
014c - 80386
0163 - MIPS II
014d - 80486
0166 - MIPS III
014e - 80586 |
0002 |
Word |
Número de objetos en la tabla de objetos |
0004 |
Dword |
Estampa Tiempo/Fecha |
0008 |
8Bytes |
Puntero a la tabla de símbolos |
0010 |
Word |
Tamaño del encabezado siguiente, encabezado opcional NT |
0012 |
Word |
Banderas
0 - Imagen del Programa 2 - EXE
200 - Dirección fijada
2000 - Librería |
Uno de los campos más importantes de este encabezado es el segundo,
en el desplazamiento 0004 desde el inicio del encabezado, que indica el número
de secciones en el que se divide el ejecutable. Podemos ver en el volcado
de NOTEPAD.EXE en HEX WORKSHOP, en el desplazamiento 0086h, el valor 0600h,
que invertido es 0006h, el número de secciones que hay en el archivo.
Como dijimos al comienzo, el formato PE divide su contenido
en varias secciones con información de tipo específico. El campo NumberOfSections
indica este número.
Otro campo que puede ser de utilidad es SizeOfOptionalHeader,
que indica el tamaño del encabezado opcional, inmediatamente después del encabezado
PE. Este valor para NOTEPAD.EXE está en el desplazamiento 94h y es E000h,
que invertido es 00E0h=224D, es decir, el encabezado opcional tiene un tamaño
de 224 bytes.
OTRA CABEZA MÁS
=================
Inmediatamente después inicia el encabezado opcional NT. Randy
Katz, en su clásico artículo de 1993, "The Portable Executable File Format
from Top to Bottom", divide este encabezado opcional en dos partes, una
con campos standard y otra con campos adicionales:
_IMAGE_OPTIONAL_HEADER STRUC
; //////////////////////////////////////
; CAMPOS STANDARD
; //////////////////////////////////////
Magic DW ?
MajorLinkerVersion DB ?
MinorLinkerVersion DB ?
SizeOfCode DD ? ; Tamaño del codigo
SizeOfInitializedData DD ? ; Tamaño de datos inicializados
SizeOfUninitializedData DD ? ; Tamaño de datos no inicializados
AddressOfEntryPoint DD ? ; Dir. virtual del punto de entrada del prog.
BaseOfCode DD ? ; Dir fisica de la base del cod.
BaseOfData DD ? ; Dir fisica de los datos
; //////////////////////////////////////////////////
; CAMPOS ADICIONALES NT
; //////////////////////////////////////////////////
ImageBase DD ? ; Dir virtual de la base de la img.
SectionAlignment DD ? , Alineamiento de secc.
FileAlignment DD ? ; Alinamiento de archivo
MajorOperatingSystemVersion DW ?
MinorOperatingSystemVersion DW ?
MajorImageVersion DW ?
MinorImageVersion DW ?
MajorSubsystemVersion DW ?
MinorSubsystemVersion DW ?
Reserved1 DD ?
SizeOfImage DD ? ; Espacio reservado en memoria para el archivo
SizeOfHeaders DD ? ; Tamaño del conjunto de los encabezados
CheckSum DD ?
Subsystem DW ?
DllCharacteristics DW ?
SizeOfStackReserve DD ?
SizeOfStackCommit DD ?
SizeOfHeapReserve DD ?
SizeOfHeapCommit DD ?
LoaderFlags DD ?
NumberOfRvaAndSizes DD ?
DataDirectory _IMAGE_DATA_DIRECTORY 16 DUP ( <> ) ; Tabla de directorios
_IMAGE_OPTIONAL_HEADER ENDS
;
de secciones
PIMAGE_OPTIONAL_HEADER TYPEDEF NEAR PTR _IMAGE_OPTIONAL_HEADER
A continuación la tabla de desplazamientos equivalentes para
esta estructura:
Encabezado
opcional NT
Campos Estandard
|
0000 |
Word |
Reservado |
0002 |
Word |
Versión del enlazador (LINKER) |
0004 |
Dword |
Tamaño de la sección o segmento de código |
0008 |
Dword |
Tamaño de la sección de datos inicializados
|
0010 |
Dword |
Tamaño de la sección de datos
no inicializados |
0014 |
Dword |
Dirección virtual del punto de entrada (RVA: Relative
Virtual Address) - La ejecución comienza aquí. |
0018 |
Dword |
Base de la sección de código |
001C |
Dword |
Base de la sección de datos |
Campos
Adicionales
|
0020 |
Dword |
Base de la Imagen - inicio de la imagen en la memoria
virtual. |
0024 |
Dword |
Alineamiento de los Objetos (Potencia de 2 512-256M) |
0028 |
Dword |
Alineamiento del Archivo (Potencia de 2 512-64k) |
0032 |
Dword |
Versión requerida de sistema operativo |
0036 |
Dword |
Versión de usuario |
003A |
Dword |
Versión de subsistema |
003E |
Dword |
Reservado |
0044 |
Dword |
Tamaño de la imagen: espacio reservado en memoria para
el archivo. |
0048 |
Dword |
Tamaño del encabezado |
005C |
Dword |
Suma de chequeo del archivo |
005E |
Word |
Subsistema
0 - Desconocido 1 - Nativo
2 - Win GUI
3 - Carácter Win |
0060 |
Word |
Banderas DLL |
0064 |
Dword |
Memoria reservada para la pila (stack) |
0068 |
Dword |
Memoria comprometida para la pila |
006C |
Dword |
Memoria reservada para el montículo (heap) |
0070 |
Dword |
Memoria comprometida para el montículo |
0074 |
Dword |
Reservado |
0078 |
Dword |
Número de directorios RVA/Tamaño presentes
Todas las entradas RVA tienen tamaño Dword |
0080 |
8Bytes |
RVA/Tamaño de la tabla de Exportaciones |
0088 |
8Bytes |
RVA/Tamaño de la tabla de Importaciones |
0090 |
8Bytes |
RVA/Tamaño de la tabla de Recursos |
0098 |
8Bytes |
RVA/Tamaño de la tabla de Excepciones |
00A0 |
8Bytes |
RVA/Tamaño de la tabla de la tabla de Seguridad |
00A8 |
8Bytes |
RVA/Tamaño de la tabla Fixup |
00B0 |
8Bytes |
RVA/Tamaño de la tabla Debug |
00B8 |
8Bytes |
RVA/Tamaño de la tabla de description de la imagen |
00C0 |
8Bytes |
RVA/Tamaño de la tabla de máquina específica |
00C8 |
8Bytes |
RVA/Tamaño de la tabla de almacenamiento del hilo
local |
CAMPOS CRÍTICOS DEL ENCABEZADO
=================================
- Tamaño de las secciones -
Para montar el ejecutable, W32 necesita reservar espacio en
memoria. Como hemos adelantado, W32 no divide la memoria en segmentos de 64KB
como lo hacía DOS sino en secciones de tamaño variable. Los campos SizeOfCode,
SizeOfInitializedData y SizeOfUninitializedData, informan el espacio de memoria
que el sistema debe reservar para cargar cada una de estas secciones.
Estos valores para NOTEPAD.EXE, tal como puede verse en los
desplazamientos 009Ch, 00A0h y 00A4h, respectivamente son 0000 3A00h (tamaño
de la sección de código), 0000 4600h (tamaño de la sección de datos inicializados)
y 0000 0000h (no hay sección de datos no inicializados).
- Punto de entrada (RVA Entry Point) -
Otro dato que necesita el sistema para ejecutar el programa
en el archivo es el punto de entrada, es decir, la dirección de la primera
instrucción del programa. Esta información se encuentra en el campo AddressOfEntryPoint,
que sigue al campo SizeOfUninitializedData del encabezado opcional NT. Para
NOTEPAD.EXE este valor, en el desplazamiento 00A8h, es 0010 0000h, que invertido
es 0000 1000h.
RVA es la abreviatura de Relative Virtual Address, que en español
significa Dirección Virtual Relativa. Significa una dirección
virtual, no real, relativa a la base del archivo. El valor de la base del
archivo se encuentra en el campo ImageBase (Base de la Imagen).
- Bases de las secciones -
Los campos BaseOfCode y BaseOfData indican las direcciones virtuales
relativas (RVAs) a la base del archivo de la sección de código
y de la sección de datos, respectivamente. En NOTEPAD.EXE, estos valores
se encuentran en los desplazamientos 00ACh y 00B0h y son, respectivamente,
0000 1000h para la sección de código, y 0000 5000h para la sección
de datos.
Observa que, para NOTEPAD.EXE, la base del código (BaseOfCode)
coincide con el punto de entrada del programa (EntryPoint). Quiere decir que
el programa inicia en la primera instrucción de la sección de
código.
- Base de la imagen (Image Base) -
Es la dirección virtual relativa a la base del archivo. Esta
información la suministra el campo ImageBase del conjunto de campos opcionales
del encabezado opcional NT (estructura _IMAGE_OPTIONAL_HEADER). Para NOTEPAD.EXE,
este valor está en el desplazamiento 00B4h y es 0000 0040, invertido 0040
0000h. La experiencia dice que este es el valor por defecto elegido por los
enlazadores como base para los archivos EXE con formato PE.
Entonces, en el caso de NOTEPAD.EXE, el programa inicia en la
dirección virtual Base_de_la_Imagen + Punto_de_Entrada: 0040 0000h + 0000
1000h = 0040 1000h.
Generalmente, 0040 1000h es la dirección virtual de la entrada
de los programas en archivos ejecutables con formato PE.
Esto lo podemos comprobar rápidamente ejecutando NOTEPAD.EXE
con el cargador (LOADER) de SICE. Veremos en la ventana desplegada por SICE
al detenerse en la primera instrucción que el depurador (DEBUGGER)
apunta a la dirección 00401000h.
- Alineamientos -
Para el archivo con formato PE se dan dos alineamientos. El
alineamiento de las secciones, campo SectionAlignment, y alineamiento de archivo,
campo FileAlignment. Estos campos son relevantes porque determinan cuanta
memoria destina el sistema para las secciones y el archivo.
Los ejecutables W32 no están divididos en segmentos de hasta
64 KB, como en DOS, sino en secciones cuyo tamaño es el múltiplo de una página
de memoria. El tamaño de una página de memoria es de 4 KB. Si una sección
ocupa 8 páginas de memoria, su tamaño es de 8*4=32 KB.
Cada sección del archivo PE es cargada secuencialmente
en el espacio de direcciones de un proceso, comenzando
en ImageBase. El campo SectionAlignment dicta la cantidad mínima de
espacio que una sección puede ocupar cuando es cargada --es decir,
las secciones están alineadas sobre los límtes o fronteras de
SectionAlignment.
El alineamiento de sección no puede ser menor al tamaño
de una página (generalmente 4096 bytes en la plataforma x86) y debe
ser, en todo caso, un múltiplo del tamaño de la página,
tal como dicta el manejador de memoria virtual de Windows. El enlazador (LINKER)
establece un valor de 4096 bytes por defecto, pero esto puede establecerse
usando del conmutador de enlazador: -ALIGN.
Por ejemplo, si una sección del programa tiene un tamaño
de 1200 bytes, el enlazador asignará a esta sección una página,
es decir 4096 bytes. Entonces, cuando el programa sea cargado en RAM, el sistema
comprometerá una página de memoria física para esta sección.
Para NOTEPAD.EXE, SectionAlignment se encuentra en el desplazamiento
0B8h y tiene un valor de 0000 1000h = 4096 bytes, que es el tamaño
de una página.
El campo FileAlignment informa sobre la granularidad mínima
de trozos (chunks) de información dentro de la imagen antes de ser
cargada. Por ejemplo, el enlazador llena con ceros (zero-pads) un cuerpo de
sección (datos brutos [raw data] para una sección) por encima
del límite o frontera más cercana de FileAlignment en el archivo.
Este valor, FileAlignment, está restringido a ser una potencia de 2
entre 512 y 65,535. En el archivo PE, los datos brutos que comprenden cada
sección deben comenzar en un múltiplo de FileAlignment. El valor
por defecto es 512 bytes, probablemente para asegurar que las secciones siempre
inicien en el comienzo de un sector del disco (un sector de disco tiene un
tamaño de 512).
En NOTEPAD.EXE, FileAlignmentse encuentra en el desplazamiento
0BCh y tiene un valor de 0000 0200h = 512 bytes.
- Tamaño de la imagen -
Otro campo indispensable es SizeOfImage, ya que para montar
el archivo W32 necesita reservar espacio en memoria. Esta información se encuentra
en el campo SizeOfImage. Este valor no es propiamente el tamaño del ejecutable
sino la cantidad de espacio que se reserva en el espacio de direcciones para
cargar el ejecutable. Este número depende bastante del valor SectionAlignment.
Si un archivo tiene seis secciones, alineadas sobre fronteras
de 65,536 bytes, el campo SizeOfImage debería ser 6 * 65.536 = 393.216
bytes (96 páginas). El mismo archivo enlazado con un alineamiento de
sección de 4096 bytes (1 página) debería dar 6 * 4096
= 24576 bytes (6 páginas) en el campo SizeOfImage. Pero esto sólo
es así si todas las secciones tienen el mismo tamaño. Puede
haber secciones con un tamaño mayor al de una página, lo cual
cambia el valor de SizeOfImage.
Podemos calcular SizeOfImage para NOTEPAD.EXE. Tiene 6 secciones
y su valor SectionAlignement es 1000h = 4096 bytes, entonces SizeOfImage debería
ser 6 * 1000h = 6000h, sin embargo este no es el caso.
En NOTEPAD.EXE SizeOfImage está en el desplazamiento 00D0h,
cuyo contenido es 00C0 0000h, que invertido es 0000 C000h. Esto es así
porque seguramente hay secciones con un tamaño mayor a una página
de memoria.
- Directorio de Datos -
Al final del encabezado opcional hay un directorio, que ocupa
el campo DataDirectory. Se trata de un vector o arreglo (array) que guarda
las direcciones donde se encuentran las tablas de datos de las secciones del
archivo. Cada una de estas entradas tiene un tamaño de 8 bytes y se divide
en dos campos. El primero indica la dirección virtual relativa a la base donde
se encuentra la tabla con datos acerca de alguna sección. El otro campo dice
el tamaño de la tabla. Cada entrada de este directorio tendría entonces la
siguiente estructura:
_IMAGE_DATA_DIRECTORY STRUC
VirtualAddress DD ? ; Dirección virtual donde está la tabla
Size@ DD ? ; Tamaño de la tabla
_IMAGE_DATA_DIRECTORY ENDS
PIMAGE_DATA_DIRECTORY TYPEDEF NEAR PTR _IMAGE_DATA_DIRECTORY
El número de entradas del directorio está indicado en el campo
NumberOfRvaAndSizes del encabezado opcional. Generalmente este valor es 0000
0010h=16D. El tamaño del directorio de datos sería el valor igual al número
de entradas por ocho bytes de cada entrada. En este caso es 128 bytes. En
NOTEPAD.EXE este valor está en el desplazamiento 00F4h y contiene 1000
0000, que invertido es 0000 0010h = 16d, es decir, 16 entradas en el directorio
de datos.
De acuerdo al archivo WINNT.H, las siguientes son las entradas
del direcorio de datos.
; Números correspondientes a las entradas del directorio de
datos
IMAGE_DIRECTORY_ENTRY_EXPORT EQU 0
IMAGE_DIRECTORY_ENTRY_IMPORT EQU 1
IMAGE_DIRECTORY_ENTRY_RESOURCE EQU 2
IMAGE_DIRECTORY_ENTRY_EXCEPTION EQU 3
IMAGE_DIRECTORY_ENTRY_SECURITY EQU 4
IMAGE_DIRECTORY_ENTRY_BASERELOC EQU 5
IMAGE_DIRECTORY_ENTRY_DEBUG EQU 6
IMAGE_DIRECTORY_ENTRY_COPYRIGHT EQU 7
IMAGE_DIRECTORY_ENTRY_GLOBALPTR EQU 8
IMAGE_DIRECTORY_ENTRY_TLS EQU 9
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG EQU 10
El número que se asigna a cada entrada de directorio de datos
se emplea para facilitar las rutinas de búsqueda de información sobre las
secciones.
VERIFIQUEMOS
==============
Ahora cerremos HEX WORKSHOP. Tomemos nota de los valores que
hemos encontrado. Abramos NOTEPAD.EXE en EXESCOPE o en PROCDUMP y comparemos
los valores desplegados con los que hemos obtenido hasta ahora. Para esta
evaluación es mejor usar EXESCOPE ya que muestra el nombre de los encabezados
y los desplazamientos donde están los datos.
Comparemos entonces... Nada mal ¿cierto?
Este proceso de ubicación manual, que puede ser tan engorroso
hacerlo de esta manera, lo realiza el cargador de Windows de una manera muy
rápida y automática. También es el mismo proceso seguido
por los volcadores y editores de encabezados de archivos PE.
MÁS CABEZAS ¡HASTA CUÁNDO!
============================
La información sobre las secciones del archivo PE se encuentra
en las tablas de secciones, después del encabezado opcional están, ordenadas
secuencialmente. Cada una de estas tablas tiene la siguiente estructura:
_IMAGE_SECTION_HEADER STRUC
Name DB 8 DUP ( ? ) ; Cadena con el nombre de la sección
; ////
; Misc
; ////
tag$0 <>
VirtualAddress DD ?
SizeOfRawData DD ?
PointerToRawData DD ?
PointerToRelocations DD ?
PointerToLinenumbers DD ?
NumberOfRelocations DW ?
NumberOfLinenumbers DW ?
Characteristics DD ?
_IMAGE_SECTION_HEADER ENDS
PIMAGE_SECTION_HEADER TYPEDEF NEAR PTR _IMAGE_SECTION_HEADER
tag$0 UNION
PhysicalAddress DD ?
VirtualSize DD ?
tag$0 ENDS
Espero que ya no se necesite presentar esta estructura con una
tabla de desplazamientos.
El primer campo de la tabla es una arreglo (array) de 8 bytes
de largo donde se escribe una cadena de caracteres con el nombre de la sección.
Sigue una unión con el tamaño virtual de la sección. Luego el campo VirtualAddress
es la dirección virtual relativa a la base de la imagen donde se encuentra
la sección.
SizeOfRawData es el tamaño del FileAlignment relativo al cuerpo
de la sección. El tamaño actual del cuerpo de la sección será menor o igual
al múltiplo de FileAlignment. Una vez que la imagen es cargada dentro del
espacio de direcciones de un proceso, el tamaño del
cuerpo de la sección llega a ser menor o igual a un multiplo de SectionAlignment.
PROCDUMP despliega este campo bajo el nombre "RAW DATA".
PointerToRawData es el desplazamiento a la localización del
cuerpo de la sección en el archivo. PROCDUMP llama a este campo "RAW
OFFSET".
El campo Characteristics indica las propiedades de la sección,
es decir, si se trata de código objeto, de datos inicializados o no inicializados,
si se puede escribir y leer sobre la sección, si es
ejecutable, si es compartible, etc. PROCDUMP llama a este campo "CHARACTERISTICS".
A continuación las equivalencias:
· 000000020h __Código.
· 000000040h __Datos inicializados.
· 000000080h __Datos no inicializados.
· 040000000h __Sección cacheable.
· 080000000h __Sección paginable.
· 100000000h __Sección compartida.
· 200000000h __Ejecutable.
· 400000000h __Se puede leer.
· 800000000h __Se puede escribir en la sección.
Por ejemplo, si las características es E0000020H, entonces se
trata de una sección en la que
1. se pueden escribir datos
80000000h
2. se pueden leer datos
40000000h
3. se pueden compartir datos 10000000h
4. hay código
00000020h
---------------
E0000020h
Se trata de una sección con código ejecutable. Esta comúnmente
lleva como nombre .text.
Hay una tabla de estas para cada sección. NOTEPAD.EXE tiene
seis secciones, por lo que tiene seis tablas de secciones.
Abramos de nuevo NOTEPAD.EXE con el editor hexadecimal.
En NOTEPAD.EXE la primera tabla se encuentra en el desplazamiento
0178h. Visualmente es simple ubicarla con el editor hexadecimal ya que cada
tabla se inicia con el campo Name, que es un campo de ocho bytes con una cadena
de caracteres ASCII con el nombre de la sección. El nombre de la primera sección
en NOTEPAD.EXE es .text. Es el nombre que por defecto se asigna a la sección
con código ejecutable.
Hay varias secciones predefinidas, cuyos nombres son:
· Sección de código ejecutable: .text
· Secciones de datos: .bss, .rdata, .data
· Sección de recursos: .rsrc
· Sección de datos exportados: .edata
· Sección de datos importados: .idata
· Sección de información para depuración: .debug
"Export" contiene información sobre los nombres que exporta
el programa y pueden ser solicitados y usados por otros procesos.
"Import" indica las funciones que el programa solicita a otros;
generalmente son funciones que se encuentran en las DLL del sistema, donde
se encuentran las funciones de la API de Windows, que son incluidas en los
prototipos que definen las funciones empleadas en el programa.
"Resources" contiene información sobre los recursos que contiene
el programa: menúes, diálogos, iconos, bitmaps, cadenas de texto, etc.
"Debug" es la sección donde el enlazador, si así lo quizo el
programador, guarda información para facilitar la depuración del programa,
con un depurador (debugger).
El siguiente campo de una entrada del directorio de datos, después
del nombre, es una unión con el tamaño de la sección. La dirección virtual
donde se encuentra la sección está en el campo VirtualAddress. La dirección
física de la sección está en PointerToRawData.
TABLAS DE DATOS
================
Cada sección tiene una tabla, generalmente al comienzo, con
información particular sobre ella.
Para localizar una tabla de éstas, se determina su dirección
virtual relativa (RVA: RELATIVA VIRTUAL ADDRESS). Este valor está en el primer
campo de la entrada correspondiente en el directorio de datos. Luego se usa
esta dirección virtual para determinar en cuál sección está.
La dirección física de la sección nos la dá el campo PointerToRawData
de la tabla de sección. La tabla de datos de la sección está en un desplazamiento
igual al dato en el campo VirtualAddress en la entrada del directorio de datos,
al final del encabezado opcional, menos el dato del campo VirtualAddress en
la tabla de sección correspondiente. Este desplazamiento es relativo a la
dirección en el campo PointerToRawData.
Como vimos, las tablas de secciones se encuentran inmediatamente
después del directorio de datos, es decir después del encabezado opcional.
Con la dirección física del desplazamiento a la primera tabla
de las secciones de la imagen, con el número de secciones de la imagen (en
el campo NumberOfSection del encabezado PE, la RVA donde inicia la sección,
la RVA de la tabla de datos de la sección y el puntero a los datos brutos
de
la sección PointerToRawData, puedo localizar donde se hallan los datos que
conforman la tabla de datos de una sección.
Como ilustración localicemos la tabla de la sección de nombres
importados .idata en NOTEPAD.EXE.
Primero buscamos en el directorio de datos la entrada que corresponde
a .idata. Es la entrada número 1, es decir, la segunda porque la primera es
la número 0 (véase los números correspondientes a las entradas del directorio
de datos). En NOTEPAD.EXE localizamos esta entrada en el desplazamiento
0100h, que inicia con el campo VirtualAddress de la entrada del directorio
que dice la dirección virtual donde se ubica la tabla de datos de la
sección .idata. El valor es 0070 0000, que invertido es 0000 7000h.
Luego localizamos el segundo campo de la tabla de sección correspondiente
a .idata. Este campo se llama también VirtualAddress e indica la dirección
virtual donde inicia la sección. Para encontrarlo manualmente, buscamos la
cadena ".idata"; la tabla de sección correspondiente a esta
sección inicia en el deplazamiento donde hallamos esa cadena. Este
campo se ubica, en NOTEPAD.EXE, en 01F0. El campo VirtualAddres
de esta sección estará entonces en 01FC, ya que el primer campo,
donde está la cadena con el nombre, tiene 8 bytes y el siguiente campo,
con el tamaño de la sección, tiene 4 bytes. El valor en 01FC
es 0000 7000h. Quiere decir que la tabla de datos de la sección se
encuentra en el inicio de ésta.
Ya que hemos localizado la tabla de seccción, obtenemos el valor
del campo PointerToRawData. Este campo lo ubicamos en 0204, y contiene
0042 0000, que invertido es 0000 4200h. Es el desplazamiento del archivo donde
están los datos brutos de la sección.
Con estos datos ya podemos localizar la tabla de datos de la
sección .idata en NOTEPAD.EXE. Resto la dirección virtual de la sección (campo
VirtualAddress de la tabla de sección) a la dirección de la tabla de datos
de esa sección (campo VirtualAddress en el directorio de datos). En este caso,
la diferencia es cero: la tabla de datos está al inicio de la sección.
A la diferencia obtenida le sumo el valor del campo PointerToRawData y obtengo
el desplazamiento donde se encuentra la tabla de datos de la sección .idata.
Como la dirección virtual de la sección coincide con la dirección de su tabla
de datos, el desplazamiento dentro del archivo, donde se encuentra la tabla
de datos de la sección .idata, coincide el valor en PointerToRawData:
4200h.
Puede parecer un proceso muy complejo. Pero basta imaginar lo
que significaría que el cargador del SO tuviera que buscar de manera
secuencial en un ejecutable de 3.2 MB, por ejemplo, qué funciones el
programa importa de las librerías DLL del sistema.
Bien, ahora que hemos localizado la tabla de datos de la sección,
podemos buscar en ella los nombres importados.
La tabla de datos de la sección de datos importados tiene también
una estructura de directorio, es decir, es una tabla con varias entradas con
la misma estructura cada una. Por este motivo podemos llamar a esta tabla
Directorio de Datos Importados, cuyas entradas tienen la siguiente estructura,
que llamamos IMAGE_IMPORT_DESCRIPTOR:
IMAGE_IMPORT_DESCRIPTOR struc
dwRVAFunctionNameList DWORD ? ;
TimeDateStamp DWORD ? ;
ForwarderChain DWORD ? ;
dwRVAModuleName DWORD ? ;
dwRVAFunctionAddressList DWORD ? ;
IMAGE_IMPORT_DESCRIPTOR ends
PIMAGE_IMAGE_IMPORT_DESCRIPTOR TYPEDEF PTRIMAGE_IMPORT_DESCRIPTOR
Hay tantas entradas de este tipo como módulos importados
sean importados. Es decir, hay una entrada ImportDirectory para cada módulo
importado.
El primer campo, dwRVAFunctionNameList, apunta a un arreglo
(array) de punteros a estructuras llamadsa IMAGE_IMPORT_BY_NAME, que son las
listas con los nombres y ordinales de las funciones a usar del módulo
importado.
TimeDateStamp indica cuando el archivo fue fabricado.
Uno de los campos importantes de esta estructura es dwRVAModuleName,
una dirección virtual relativa a la base que apunta al nombre del módulo importado.
Si resto al valor de dwRVAModuleName la dirección virtual
de la sección, obtengo el desplazamiento desde el inicio de la sección.
En NOTEPAD.EXE, el campo dwRVAModuleName está en el desplazamiento
4200h + 4 + 4 + 4 = 420Ch, y contiene E874 0000, que invertido es 0000 74E8h.
La dirección virtual de la sección .idata es 7000h. Entonces
el nombre del primer módulo importado se encuentra en:
0000 74E8h - 0000 70000h = 0000 04E8h + 0000 4200h = 0000 46E8h.
En el desplazamiento 46E8h encuentro una cadena con el primer
módulo importado por NOTEPAD.EXE: Shell32.dll. Este nombre estará
antecedido por el nombre de una de las funciones importadas del módulo
y estará seguido por las demás funciones.
El campo dwRVAFunctionNameList apunta a la lista de punteros
a los nombres de las funciones importadas en el módulo. Confirmemos esto en
NOTEPAD.EXE.
El campo dwRVAFunctionNameList está en 4200h y contiene
0000 7160h, lo que indica que la lista de punteros a nombres importados del
módulo correspondiente está en:
0000 7160h - 0000 70000h = 0000 0160h + 4200h = 0000 4360h.
El valor en este desplazamiento es 000 74D8. Es la dirección
virtual de la lista de nombres:
0000 74D8 - 0000 7000 = 0000 04D8h + 4200h = 0000 46D8h.
Si vamos a esta dirección encontraremos que contiene
el número 004Eh, seguido por la cadena 'ShellExecuteA'. El primer número
es el ordinal de exportación de la función con que inicia la
lista de nombres importados del módulo, y la cadena es el nombre de
la función. El ordinal es un número de referencia que puedo
emplear para llamar a la función importada si no poseo su nombre.
El campo dwRVAFunctionAddressList es sumamente importante. Apunta
también a un arreglo (array) donde el cargador de W32 coloca la dirección
virtual del punto de entrada de la función importada. Esto es de sumo
interés: cuando el cargador monta el programa en RAM llena este arreglo
con las direcciones virtuales donde inician las funciones importadas. Cuando
el programa llama a una de estas funciones lo hace indirectamente a través
de una llamada como esta:
jmp [RVA del THUNK]
El campo dwRVAFunctionAddressList está, en NOTEPAD, en
0000 4210h, y contiene el valor 0000 7370h, ya invertido. Quiere decir que
apunta a:
7370 - 7000 = 0370
+ 4200 = 4570h ;
<-- dwRVAFunctionAddressList
Anota este número.
Ahora fíjate. Desensambla NOTEPAD.EXE con W32DASM. Busca
la cadena "ShellExecuteA" hasta llegar a:
* Reference To: SHELL32.ShellExecuteA, Ord:004Eh |
:00402DEE FF1570734000 Call dword ptr [00407370]
Es una típica llamada a una función API de W32.
Tiene la forma "CALL DWORD PTR [THUNK RVA]. "THUNK RVA" es
la RVA donde el cargador de W32 coloca la dirección virtual donde inicia
la función importada en el espacio de direcciones del proceso. Si con
un programa como OFFSET (de Iczelion) o OFFCAL (de MrCrimson) revisas el desplazamiento
en el archivo que corresponde a la dirección de memoria 00407370, verás
que esta dirección corresponde al desplazamiento 000 4570h, el mismo
apuntado por el campo dwRVAFunctionAddressList
correspondiente a la información sobre "ShellExecuteA".
No olvides esto último, porque es muy útil para
redirigir los llamados a funciones de la API de W32.
Tenemos ya una idea de como se estructura el encabezado significado
de sus campos. Todavía queda analizar otras importantes secciones predefinidas,
como la sección de recursos .rcrs. Es una de las secciones más atractivas
de los archivos PE. Debido a la complejidad de su estructura, prefiero diferir
por ahora su análisis, ya que implica un concepto fundamental para el programador:
árbol de búsqueda. Se trata de un concepto de datos estructurados que merecería
una atención especial.
De todos modos hagamos un ejercicio para consolidar conocimientos.
|