Introducción: Estructura PE (Portable Executable)
Que es la estructura PE?
El archivo PE (Portable Executable) es el formato que utiliza windows para los ejecutables. En fin, en pocas palabras es un .exe o un .dll
Para que sirve conocerlo?
En Ing inversa muchas veces no es necesario conocer con mucho detalle la estructura que tiene este archivo, sin embargo, para problemas más complicados si resultará util conocerlo. Nos permite separar las secciones del exe, analizarlas por separado, crear nuevas secciones, o modificar las existentes.
Las aplicaciones son bastantes, pero lo mas importante es que con esto se entenderá mejor porque pasan algunas cosas y como podemos resolverlas
Estructura basica:
Vamos por parte, primero con una división a grandes razgos de las partes del archivo, y despues vamos ir entrando en detalle en las mas importantes
El archivo PE simplemente un archivo binario, que tiene una estructura, que es basicamente esta:
Como se ve el archivo tiene 5 partes, asi de simple XD, los encabezados tienen un tamaño constante y en ellos están definidas todas las propiedades del archivo. Los stub contienen al programa en sí, los analizaremos por parte.
Sobre el Encabezado DOS y el Stub DOS
Tanto el encabezado DOS como el stub DOS no nos interesará tanto, están ahi por una especie de compatibilidad, si estamos ejecutando el programa en windows se ignorará toda esta parte, generalmente eso contiene un pequeño programa en DOS que solamente imprime en pantalla algo como esto:
"This program cannot be run in DOS mode."para indicar que estamos tratando de ejecutar un archivo que no corre en DOS.
Encabezado DOS
El encabezado DOS, asi como ya dije, es una estructura de datos que contiene variables que definen el programa, las variables que contiene este encabezado las podemos sacar de la ayuda de windows (msdn.microsoft.com).
Esto es el encabezado, no nos interesa tanto, lo importante aqui es que, para leer todo el archivo debemos por lo menos saber el tamaño que ocupa este encabezado, y lo mas importante es que en este encabezado está la variable que indica donde comienza el encabezado del NT (que es realmente el encabezado que nos interesa) dentro de esta variable e_lfanew se indica donde comienza el encabezado NT, vamos a ver con un ejemplo, esto es lo que se ve con un editor hexadacimal.
Ahora hay que fijarse en la estructura del DOS_HEADER y lo que se ve en el archivo el tamaño del encabezado podemos calcular 30*WORD+1*DWORD = 64 bytes el encabezado tiene 64 bytes = 0x40 bytes (resulta mas comodo leer todo en hexadecima) en el archivo separé con una linea, donde termina este encabezado, es exactamente en el offset 0x40.
Ahora vamos a leer la variable que nos interesa (DWORD e_lfanew), que son los ultimos 4 bytes del encabezado. en el archivo vemos que en esos 4 bytes tenemos "80 00 00 00" no hay que perder de vista que, dentro del archivo las variables se guardan con el byte menos significativo a la izquierda, asi que, si queremos leer esto como un numero hexa, debemos invertir este valor, tomando de 2 en 2, este valor en hexa será 0x00000080 (si no se entendió coloco aqui un ejemplo, si vemos en el archivo una variable asi "01 02 03 04" el valor en hexadecimal de esa variable es 0x04030201, se debe invertir los numero de 2 en 2)
Bueno, con esto ya podemos leer donde empieza el encabezado del NT, está en el offset 0x00000080
Stub DOS
El codigo en DOS realmente no nos interesa, pero solo para marcarlo, donde comienza y donde termina, está a continuación del Encabezado DOS y termina antes del encabezado del NT, asi que, en este caso, lo que tenemos en el stub del DOS es esto:
Como se puede ver, ahi esta el texto que dice que la aplicación no corre en DOS
Encabezado NT
El encabezado NT es un poco mas complicado que el de DOS, veamos su estructura.
Esta es su definición, la podemos encontrar en esta pagina IMAGE_NT_HEADERS
Esta estructura todavia no nos dice mucho, solo que tiene un DWORD, que es un valor de referencia, siempre vale 0x00004550, realmente conviene leer esta variable como un char* "50 45 00 00", en ascii significa 'PE\0\0', esto se puede ver facilmente y permite identificar donde comienza el encabezado NT sin tener que hacer el cálculo que hicimos hace un rato.
Esto lo vemos aqui, (ver los primeros 4 bytes)
Veamos las estructuras contenidas en esta estructura: IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER
IMAGE_FILE_HEADER
No vamos a ver con detalle todas las variables de este encabezado, solo las que nos interesan WORD Machine: nos dice el tipo de maquina para la que fue compilado: 386,486,586,... DWORD NumberOfSections: numero de secciones del archivo, esto es importante, si es que queremos agregar secciones.
DWORD TimeDateStamp: la fecha en la que fue compilado DWORD PointerToSymbolTable: puntero a la tabla de simbolos DWORD NumberOfSymbols: numero de simbolos (despues veremos esto en detalle) WORD SizeOfOptionalHeader: tamaño del IMAGE_OPTIONAL_HEADER, esto es interesante ya que, haciendo unas operaciones podemos llegar al stub del NT (donde comienza realmente el codigo) del encabezado DOS tenemos el offset del inicio de esta estructura, si a eso le sumamos el tamaño del IMAGE_FILE_HEADER y del IMAGE_OPTIONAL_HEADER, llegaremos al inicio del Stub NT (despues veremos en detalle como hacer este calculo)
sigamos con el IMAGE_OPTIONAL_HEADER
Aqui se puede ver la definicion de esa estructura
El IMAGE_OPTIONAL_HEADER en el ejecutable de ejemplo se puede ver aqui: (comienza en el offset 0x000000098 y termina en el 000000177)
Aqui vemos eso mismo, pero ordenando cada variable (visto con el ollydebg): hice un salto de linea donde comienza la tabla de secciones (esto no es el encabezado de las secciones, que está a continuación)
Diferencia entre Dirección RAW y Virtual
Para poder entender el significado de algunas de estas variables, hace falta una pequeña introduccion de como las secciones se colocan en memoria.
Cuando se coloca un programa en memoria, para ejecutarlo, no se coloca exactamente como está en el archivo, en el archivo se tiene todo en forma mas compactada. En memoria se alinean las secciones, generalmente de a 0x1000 bytes (esto depende de la variable que se encuentra en este encabezado, SectionAlignment es la variable que dice el valor de la alineacion, solo que generalmente vale 0x1000), esto hace mas rapida la lectura, sin embargo las secciones en el archivo, no hace falta que se separen unas de otras, aunque tambien son alineadas (la variable FileAlignment tiene el valor al cual se alinea en el archivo)
Ademas de esta diferencia, el codigo del programa no comienza de la direccion 0x0 como pasa en el archivo, asi que una direccion en el archivo es totalmente distinta a una direccion en memoria. en el encabezado, cuando se refiere a memoria Rva se refiere a la memoria en el archivo, y cuando habla de memoria virtual se refiere al programa ya colocado en memoria para la ejecución eso es necesario diferenciar para entender el encabezado.
Si queremos encontrar, donde empieza una seccion en el archivo, debemos buscar su direccion RVA, si queremos ver donde empieza esa misma seccion en memoria, debemos buscar su direccion Virtual (mas adelante veremos como identificar eso, con unos ejemplos)
Seguido de esto viene el encabezado de las secciones esta es la estructura de cada encabezado de seccion
Por la union no hay que preocuparse, considerese solo una variable, que indica el tamaño (VirtualSize)
La cantidad de secciones tenemos ya, del encabezado NT, al ir leyendo todos los encabezados de las secciones, tendremos su posicion en el archivo (PointerToRawData), y en memoria (VirtualAddress), el tamaño es el mismo, la diferencia es que, estando en memoria, se llena de 0x0 hasta ajustarse a la alineación.
En fin, despues de esto solo se tiene que ir leyendo las secciones.
.text es generalmente la seccion que contiene el codigo .data y .rdata contiene constantes, son secciones de solo lectura. .bss es una seccion de lectura y escritura, se usa para las variables globales generalmente, las variables locales se colocan en el stack (eso lo veremos en otro tuto) .idata es la tabla de importacion de funciones .edata es la tabla de exportacion de funciones (es mas comun en los DLL, aunque un exe tambien lo puede tener)
En nuestro code de ejemplo aqui esta el encabezado de las secciones
Seguido a esto (hay un espacio lleno de 0x0 donde se pueden agregar mas encabezados de secciones), y luego ya están las secciones en sí (a lo que le llamé Stub NT)
Esto es todo, se supone que con esto se debe entender como es el formato del ejectuable de windows.
A continuacion veremos un ejemplo de como encontrar una zona especifica de un programa, en memoria y en el archivo.
Para empezar buscaremos el famoso EntryPoint, esta direccion indica el lugar donde se comienza a ejecutar el code, asi que, se supone que debe caer dentro de la seccion de code, ( en el caso del ejemplo solo tiene una seccion con code .text )
Dentro del OptionalHeader tenemos una variable llamada AddressOfEntryPoint, para encontrarlo en el archivo, vamos al inicio de la estructura del OptionalHeader, y contamos los tamaños de las variables que están antes WORD+2*BYTE+3*DWORD = 0x10 bytes (16 en decimal) sabemos que el inicio de esa estructura esta en 0x000000098 (eso ya lo vimos mas arriba), asi que la direccion del EntryPint debe estar en el Offset 0x0000000A8, tambien sabemos que ocupa 1 DWORD = 4 bytes.
Comos que en el archivo es este el valor "30 11 00 00", como ya dijimos hay que voltearlo de 2 en 2 para tener el valor en Hexa 0x00001130, listo esta es la direccion del entrypoint, pero, tambien como vimos, todas estas direcciones estan referidas al ImageBase, de la misma forma que buscamos el valor del EntryPoint busquemos el ImageBase, esta variable está 3 Dword abajo del entrypoint, osea 8bytes, 0x0000000A8 + 0xC = 0x0000000B4 y ocupa 4 bytes, este es el valor "00 00 40 00" volteando = 0x00400000, bien, asi que el EntryPoint está realmente en 0x00400000 + 0x00001130 = 0x00401130 (eso lo podemos comprobar con algun otro programa como el olly)
Que pasa si queremos ver ese entrypoint en el archivo, como lo vemos?
primero hay que saber a que seccion corresponde, en este caso, es evidente que corresponde a .text veamos El VirtualAdress de .text, y el tamaño para acegurarnos de que está en esa seccion este es el encabezado de esa seccion (comienza en 0x000000178)
su VirtualAdress esta a 8 Bytes + 1 Dword del inicio del encabezado, osea a 0xC bytes "00 10 00 00" = 0x00001000, tambien nos hara falta conocer el PointerToRawData "00 04 00 00"= 0x00000400
El virtual adress sabemos que es relativo al ImageBase, pero solo nos interesa saber la distancia a la que está el EntryPoint del inicio de esa seccion. asi que ese Offset sera EntryPint - VirtualAdress = 0x130.
Ese es el offset desde el inicio de la seccion, en el archivo esa seccion inicia en el PointerToRawData, asi que, el entrypoint en el archivo está en PointerToRawData + 0x130 = 0x530
Vemos el archivo
y lo comparamos con el dump del Entrypoint en memoria (visto con el Ollydbg)
Podemos ver que el code coincide exactamente, ya que estamos parados en el mismo lugar.
Enlaces:
ESTUDIO DE LOS ENCABEZADOS PE (parte 1) POR SICK TROEN
ESTUDIO DE LOS ENCABEZADOS PE (parte 2) POR SICK TROEN
Estudios sobre las cabeceras AE - 1 - Las Secciones
Estudios sobre las cabeceras AE-2
Autor: Jep
El archivo PE (Portable Executable) es el formato que utiliza windows para los ejecutables. En fin, en pocas palabras es un .exe o un .dll
Para que sirve conocerlo?
En Ing inversa muchas veces no es necesario conocer con mucho detalle la estructura que tiene este archivo, sin embargo, para problemas más complicados si resultará util conocerlo. Nos permite separar las secciones del exe, analizarlas por separado, crear nuevas secciones, o modificar las existentes.
Las aplicaciones son bastantes, pero lo mas importante es que con esto se entenderá mejor porque pasan algunas cosas y como podemos resolverlas
Estructura basica:
Vamos por parte, primero con una división a grandes razgos de las partes del archivo, y despues vamos ir entrando en detalle en las mas importantes
El archivo PE simplemente un archivo binario, que tiene una estructura, que es basicamente esta:
-------------------- | ENCABEZADO DOS | -------------------- | STUB DOS | -------------------- | ENCABEZADO NT | -------------------- | ENCABEZADO SEC | -------------------- | STUB NT | --------------------
Como se ve el archivo tiene 5 partes, asi de simple XD, los encabezados tienen un tamaño constante y en ellos están definidas todas las propiedades del archivo. Los stub contienen al programa en sí, los analizaremos por parte.
Sobre el Encabezado DOS y el Stub DOS
Tanto el encabezado DOS como el stub DOS no nos interesará tanto, están ahi por una especie de compatibilidad, si estamos ejecutando el programa en windows se ignorará toda esta parte, generalmente eso contiene un pequeño programa en DOS que solamente imprime en pantalla algo como esto:
"This program cannot be run in DOS mode."para indicar que estamos tratando de ejecutar un archivo que no corre en DOS.
Encabezado DOS
El encabezado DOS, asi como ya dije, es una estructura de datos que contiene variables que definen el programa, las variables que contiene este encabezado las podemos sacar de la ayuda de windows (msdn.microsoft.com).
typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10]; DWORD e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Esto es el encabezado, no nos interesa tanto, lo importante aqui es que, para leer todo el archivo debemos por lo menos saber el tamaño que ocupa este encabezado, y lo mas importante es que en este encabezado está la variable que indica donde comienza el encabezado del NT (que es realmente el encabezado que nos interesa) dentro de esta variable e_lfanew se indica donde comienza el encabezado NT, vamos a ver con un ejemplo, esto es lo que se ve con un editor hexadacimal.
"000000000 4D 5A 90 00 03 00 00 00-04 00 00 00 FF FF 00 00 |MZ##############|" "000000010 B8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 |########@#######|" "000000020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 |################|" "000000030 00 00 00 00 00 00 00 00-00 00 00 00 80 00 00 00 |################|" "000000040 0E 1F BA 0E 00 B4 09 CD-21 B8 01 4C CD 21 54 68 |######.#!##L#!Th|" "000000050 69 73 20 70 72 6F 67 72-61 6D 20 63 61 6E 6E 6F |is program canno|" "000000060 74 20 62 65 20 72 75 6E-20 69 6E 20 44 4F 53 20 |t be run in DOS |" "000000070 6D 6F 64 65 2E 0D 0D 0A-24 00 00 00 00 00 00 00 |mode....$#######|" "000000080 50 45 00 00 4C 01 05 00-24 A9 8A 4C 00 6C 00 00 |PE##L###$##L#l##|"
Ahora hay que fijarse en la estructura del DOS_HEADER y lo que se ve en el archivo el tamaño del encabezado podemos calcular 30*WORD+1*DWORD = 64 bytes el encabezado tiene 64 bytes = 0x40 bytes (resulta mas comodo leer todo en hexadecima) en el archivo separé con una linea, donde termina este encabezado, es exactamente en el offset 0x40.
Ahora vamos a leer la variable que nos interesa (DWORD e_lfanew), que son los ultimos 4 bytes del encabezado. en el archivo vemos que en esos 4 bytes tenemos "80 00 00 00" no hay que perder de vista que, dentro del archivo las variables se guardan con el byte menos significativo a la izquierda, asi que, si queremos leer esto como un numero hexa, debemos invertir este valor, tomando de 2 en 2, este valor en hexa será 0x00000080 (si no se entendió coloco aqui un ejemplo, si vemos en el archivo una variable asi "01 02 03 04" el valor en hexadecimal de esa variable es 0x04030201, se debe invertir los numero de 2 en 2)
Bueno, con esto ya podemos leer donde empieza el encabezado del NT, está en el offset 0x00000080
"000000070 6D 6F 64 65 2E 0D 0D 0A-24 00 00 00 00 00 00 00 |mode....$#######|" "000000080 50 45 00 00 4C 01 05 00-24 A9 8A 4C 00 6C 00 00 |PE##L###$##L#l##|" "000000090 31 03 00 00 E0 00 07 03-0B 01 02 38 00 5A 00 00 |1##########8#Z##|" "0000000A0 00 68 00 00 00 0C 00 00-30 11 00 00 00 10 00 00 |#h######0#######|"
Stub DOS
El codigo en DOS realmente no nos interesa, pero solo para marcarlo, donde comienza y donde termina, está a continuación del Encabezado DOS y termina antes del encabezado del NT, asi que, en este caso, lo que tenemos en el stub del DOS es esto:
"000000040 0E 1F BA 0E 00 B4 09 CD-21 B8 01 4C CD 21 54 68 |######.#!##L#!Th|" "000000050 69 73 20 70 72 6F 67 72-61 6D 20 63 61 6E 6E 6F |is program canno|" "000000060 74 20 62 65 20 72 75 6E-20 69 6E 20 44 4F 53 20 |t be run in DOS |" "000000070 6D 6F 64 65 2E 0D 0D 0A-24 00 00 00 00 00 00 00 |mode....$#######|"
Como se puede ver, ahi esta el texto que dice que la aplicación no corre en DOS
Encabezado NT
El encabezado NT es un poco mas complicado que el de DOS, veamos su estructura.
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
Esta es su definición, la podemos encontrar en esta pagina IMAGE_NT_HEADERS
Esta estructura todavia no nos dice mucho, solo que tiene un DWORD, que es un valor de referencia, siempre vale 0x00004550, realmente conviene leer esta variable como un char* "50 45 00 00", en ascii significa 'PE\0\0', esto se puede ver facilmente y permite identificar donde comienza el encabezado NT sin tener que hacer el cálculo que hicimos hace un rato.
Esto lo vemos aqui, (ver los primeros 4 bytes)
"000000080 50 45 00 00 4C 01 05 00-24 A9 8A 4C 00 6C 00 00 |PE##L###$##L#l##|"
Veamos las estructuras contenidas en esta estructura: IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER
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;
No vamos a ver con detalle todas las variables de este encabezado, solo las que nos interesan WORD Machine: nos dice el tipo de maquina para la que fue compilado: 386,486,586,... DWORD NumberOfSections: numero de secciones del archivo, esto es importante, si es que queremos agregar secciones.
DWORD TimeDateStamp: la fecha en la que fue compilado DWORD PointerToSymbolTable: puntero a la tabla de simbolos DWORD NumberOfSymbols: numero de simbolos (despues veremos esto en detalle) WORD SizeOfOptionalHeader: tamaño del IMAGE_OPTIONAL_HEADER, esto es interesante ya que, haciendo unas operaciones podemos llegar al stub del NT (donde comienza realmente el codigo) del encabezado DOS tenemos el offset del inicio de esta estructura, si a eso le sumamos el tamaño del IMAGE_FILE_HEADER y del IMAGE_OPTIONAL_HEADER, llegaremos al inicio del Stub NT (despues veremos en detalle como hacer este calculo)
sigamos con el IMAGE_OPTIONAL_HEADER
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD 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; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
DWORD SizeOfCode; indica el tamaño total del code, osea, es la suma de los tamaños de las secciones que contienen codigo. DWORD AddressOfEntryPoint; direccion del EntryPoint DWORD BaseOfCode esto indica donde empieza la seccion que contiene el codigo (generalmente la seccion .text) DWORD BaseOfData; indica donde empieza la seccion que contiene las constantes del programa (generalmente .data) DWORD ImageBase; todas las direcciones virtuales están colocadas relativas a esta direccion, asi que a todas hay que sumarle este valor para sacar la direccion DWORD SectionAlignment; Alineacion de las secciones, en memoria DWORD FileAlignment; alineacion de las secciones, en el archivo DWORD SizeOfImage; suma de los tamaños de las secciones DWORD SizeOfHeaders; suma de los tamaños de los encabezados de las secciones DWORD NumberOfRvaAndSizes; esto da el numero de secciones y tamaños de cada seccion, esto es necesario para leer la info de IMAGE_DATA_DIRECTORY DataDirector, esta estructura que contiene la direccion de las secciones en el archivo, y los tamaños de estas secciones. estas secciones son secciones especiales, que siempre deben estar en los programas, ya vamos a ver que tienen una relacion con las secciones reales del programa, pero en nuestro programa podemos tener otras secciones que no estén en esta tabla. las secciones de esta tabla son: {"Export Table","Import Table","Resource Table","Exception Table","Certificate Table","Base Relocation Table","Debug","Architecture","Global Ptr","TLS Table","Load Config Table","Bound Import","IAT","Delay Import Descriptor","COM+ Runtime Header","Reserved"} si alguna de estas secciones no existe en el programa se coloca su direccion 0x0 y tamaño 0x0, como podremos ver en los programas reales
Aqui se puede ver la definicion de esa estructura
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
El IMAGE_OPTIONAL_HEADER en el ejecutable de ejemplo se puede ver aqui: (comienza en el offset 0x000000098 y termina en el 000000177)
"000000090 31 03 00 00 E0 00 07 03-0B 01 02 38 00 5A 00 00 |1##########8#Z##|" "0000000A0 00 68 00 00 00 0C 00 00-30 11 00 00 00 10 00 00 |#h######0#######|" "0000000B0 00 70 00 00 00 00 40 00-00 10 00 00 00 02 00 00 |#p####@#########|" "0000000C0 04 00 00 00 01 00 00 00-04 00 00 00 00 00 00 00 |################|" "0000000D0 00 B0 00 00 00 04 00 00-A9 4C 01 00 03 00 00 00 |#########L######|" "0000000E0 00 00 20 00 00 10 00 00-00 00 10 00 00 10 00 00 |## #############|" "0000000F0 00 00 00 00 10 00 00 00-00 00 00 00 00 00 00 00 |################|" "000000100 00 A0 00 00 B8 04 00 00-00 00 00 00 00 00 00 00 |################|" "000000110 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 |################|" "000000120 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 |################|" "000000130 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 |################|" "000000140 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 |################|" "000000150 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 |################|" "000000160 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 |################|" "000000170 00 00 00 00 00 00 00 00-2E 74 65 78 74 00 00 00 |########.text###|"
Aqui vemos eso mismo, pero ordenando cada variable (visto con el ollydebg): hice un salto de linea donde comienza la tabla de secciones (esto no es el encabezado de las secciones, que está a continuación)
00400098 0B01 DW 010B ; MagicNumber = PE32 0040009A 02 DB 02 ; MajorLinkerVersion = 2 0040009B 38 DB 38 ; MinorLinkerVersion = 38 (56.) 0040009C 005A0000 DD 00005A00 ; SizeOfCode = 5A00 (23040.) 004000A0 00680000 DD 00006800 ; SizeOfInitializedData = 6800 (26624.) 004000A4 000C0000 DD 00000C00 ; SizeOfUninitializedData = C00 (3072.) 004000A8 30110000 DD 00001130 ; AddressOfEntryPoint = 1130 004000AC 00100000 DD 00001000 ; BaseOfCode = 1000 004000B0 00700000 DD 00007000 ; BaseOfData = 7000 004000B4 00004000 DD 00400000 ; ImageBase = 400000 004000B8 00100000 DD 00001000 ; SectionAlignment = 1000 004000BC 00020000 DD 00000200 ; FileAlignment = 200 004000C0 0400 DW 0004 ; MajorOSVersion = 4 004000C2 0000 DW 0000 ; MinorOSVersion = 0 004000C4 0100 DW 0001 ; MajorImageVersion = 1 004000C6 0000 DW 0000 ; MinorImageVersion = 0 004000C8 0400 DW 0004 ; MajorSubsystemVersion = 4 004000CA 0000 DW 0000 ; MinorSubsystemVersion = 0 004000CC 00000000 DD 00000000 ; Reserved 004000D0 00B00000 DD 0000B000 ; SizeOfImage = B000 (45056.) 004000D4 00040000 DD 00000400 ; SizeOfHeaders = 400 (1024.) 004000D8 A94C0100 DD 00014CA9 ; CheckSum = 14CA9 004000DC 0300 DW 0003 ; Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI 004000DE 0000 DW 0000 ; DLLCharacteristics = 0 004000E0 00002000 DD 00200000 ; SizeOfStackReserve = 200000 (2097152.) 004000E4 00100000 DD 00001000 ; SizeOfStackCommit = 1000 (4096.) 004000E8 00001000 DD 00100000 ; SizeOfHeapReserve = 100000 (1048576.) 004000EC 00100000 DD 00001000 ; SizeOfHeapCommit = 1000 (4096.) 004000F0 00000000 DD 00000000 ; LoaderFlags = 0 004000F4 10000000 DD 00000010 ; NumberOfRvaAndSizes = 10 (16.) 004000F8 00000000 DD 00000000 ; Export Table address = 0 004000FC 00000000 DD 00000000 ; Export Table size = 0 00400100 00A00000 DD 0000A000 ; Import Table address = A000 00400104 B8040000 DD 000004B8 ; Import Table size = 4B8 (1208.) 00400108 00000000 DD 00000000 ; Resource Table address = 0 0040010C 00000000 DD 00000000 ; Resource Table size = 0 00400110 00000000 DD 00000000 ; Exception Table address = 0 00400114 00000000 DD 00000000 ; Exception Table size = 0 00400118 00000000 DD 00000000 ; Certificate File pointer = 0 0040011C 00000000 DD 00000000 ; Certificate Table size = 0 00400120 00000000 DD 00000000 ; Relocation Table address = 0 00400124 00000000 DD 00000000 ; Relocation Table size = 0 00400128 00000000 DD 00000000 ; Debug Data address = 0 0040012C 00000000 DD 00000000 ; Debug Data size = 0 00400130 00000000 DD 00000000 ; Architecture Data address = 0 00400134 00000000 DD 00000000 ; Architecture Data size = 0 00400138 00000000 DD 00000000 ; Global Ptr address = 0 0040013C 00000000 DD 00000000 ; Must be 0 00400140 00000000 DD 00000000 ; TLS Table address = 0 00400144 00000000 DD 00000000 ; TLS Table size = 0 00400148 00000000 DD 00000000 ; Load Config Table address = 0 0040014C 00000000 DD 00000000 ; Load Config Table size = 0 00400150 00000000 DD 00000000 ; Bound Import Table address = 0 00400154 00000000 DD 00000000 ; Bound Import Table size = 0 00400158 00000000 DD 00000000 ; Import Address Table address = 0 0040015C 00000000 DD 00000000 ; Import Address Table size = 0 00400160 00000000 DD 00000000 ; Delay Import Descriptor address = 0 00400164 00000000 DD 00000000 ; Delay Import Descriptor size = 0 00400168 00000000 DD 00000000 ; COM+ Runtime Header address = 0 0040016C 00000000 DD 00000000 ; Import Address Table size = 0 00400170 00000000 DD 00000000 ; Reserved 00400174 00000000 DD 00000000 ; Reserved
Diferencia entre Dirección RAW y Virtual
Para poder entender el significado de algunas de estas variables, hace falta una pequeña introduccion de como las secciones se colocan en memoria.
Cuando se coloca un programa en memoria, para ejecutarlo, no se coloca exactamente como está en el archivo, en el archivo se tiene todo en forma mas compactada. En memoria se alinean las secciones, generalmente de a 0x1000 bytes (esto depende de la variable que se encuentra en este encabezado, SectionAlignment es la variable que dice el valor de la alineacion, solo que generalmente vale 0x1000), esto hace mas rapida la lectura, sin embargo las secciones en el archivo, no hace falta que se separen unas de otras, aunque tambien son alineadas (la variable FileAlignment tiene el valor al cual se alinea en el archivo)
Ademas de esta diferencia, el codigo del programa no comienza de la direccion 0x0 como pasa en el archivo, asi que una direccion en el archivo es totalmente distinta a una direccion en memoria. en el encabezado, cuando se refiere a memoria Rva se refiere a la memoria en el archivo, y cuando habla de memoria virtual se refiere al programa ya colocado en memoria para la ejecución eso es necesario diferenciar para entender el encabezado.
Si queremos encontrar, donde empieza una seccion en el archivo, debemos buscar su direccion RVA, si queremos ver donde empieza esa misma seccion en memoria, debemos buscar su direccion Virtual (mas adelante veremos como identificar eso, con unos ejemplos)
Seguido de esto viene el encabezado de las secciones esta es la estructura de cada encabezado de seccion
typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; /* IMAGE_SIZEOF_SHORT_NAME = 8 bytes */ 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;
Por la union no hay que preocuparse, considerese solo una variable, que indica el tamaño (VirtualSize)
La cantidad de secciones tenemos ya, del encabezado NT, al ir leyendo todos los encabezados de las secciones, tendremos su posicion en el archivo (PointerToRawData), y en memoria (VirtualAddress), el tamaño es el mismo, la diferencia es que, estando en memoria, se llena de 0x0 hasta ajustarse a la alineación.
En fin, despues de esto solo se tiene que ir leyendo las secciones.
.text es generalmente la seccion que contiene el codigo .data y .rdata contiene constantes, son secciones de solo lectura. .bss es una seccion de lectura y escritura, se usa para las variables globales generalmente, las variables locales se colocan en el stack (eso lo veremos en otro tuto) .idata es la tabla de importacion de funciones .edata es la tabla de exportacion de funciones (es mas comun en los DLL, aunque un exe tambien lo puede tener)
En nuestro code de ejemplo aqui esta el encabezado de las secciones
"000000170 00 00 00 00 00 00 00 00-2E 74 65 78 74 00 00 00 |########.text###|" "000000180 E8 59 00 00 00 10 00 00-00 5A 00 00 00 04 00 00 |#Y#######Z######|" "000000190 00 00 00 00 00 00 00 00-00 00 00 00 60 00 50 60 |############`#P`|" "0000001A0 2E 64 61 74 61 00 00 00-44 00 00 00 00 70 00 00 |.data###D####p##|" "0000001B0 00 02 00 00 00 5E 00 00-00 00 00 00 00 00 00 00 |#####^##########|" "0000001C0 00 00 00 00 40 00 30 C0-2E 72 64 61 74 61 00 00 |####@#0#.rdata##|" "0000001D0 20 05 00 00 00 80 00 00-00 06 00 00 00 60 00 00 | ############`##|" "0000001E0 00 00 00 00 00 00 00 00-00 00 00 00 40 00 60 40 |############@#`@|" "0000001F0 2E 62 73 73 00 00 00 00-78 0A 00 00 00 90 00 00 |.bss####x.######|" "000000200 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 |################|" "000000210 00 00 00 00 80 00 40 C0-2E 69 64 61 74 61 00 00 |######@#.idata##|" "000000220 B8 04 00 00 00 A0 00 00-00 06 00 00 00 66 00 00 |#############f##|" "000000230 00 00 00 00 00 00 00 00-00 00 00 00 40 00 30 C0 |############@#0#|"
Seguido a esto (hay un espacio lleno de 0x0 donde se pueden agregar mas encabezados de secciones), y luego ya están las secciones en sí (a lo que le llamé Stub NT)
Esto es todo, se supone que con esto se debe entender como es el formato del ejectuable de windows.
A continuacion veremos un ejemplo de como encontrar una zona especifica de un programa, en memoria y en el archivo.
Para empezar buscaremos el famoso EntryPoint, esta direccion indica el lugar donde se comienza a ejecutar el code, asi que, se supone que debe caer dentro de la seccion de code, ( en el caso del ejemplo solo tiene una seccion con code .text )
Dentro del OptionalHeader tenemos una variable llamada AddressOfEntryPoint, para encontrarlo en el archivo, vamos al inicio de la estructura del OptionalHeader, y contamos los tamaños de las variables que están antes WORD+2*BYTE+3*DWORD = 0x10 bytes (16 en decimal) sabemos que el inicio de esa estructura esta en 0x000000098 (eso ya lo vimos mas arriba), asi que la direccion del EntryPint debe estar en el Offset 0x0000000A8, tambien sabemos que ocupa 1 DWORD = 4 bytes.
"0000000A0 00 68 00 00 00 0C 00 00-30 11 00 00 00 10 00 00 |#h######0#######|" "0000000B0 00 70 00 00 00 00 40 00-00 10 00 00 00 02 00 00 |#p####@#########|"
Comos que en el archivo es este el valor "30 11 00 00", como ya dijimos hay que voltearlo de 2 en 2 para tener el valor en Hexa 0x00001130, listo esta es la direccion del entrypoint, pero, tambien como vimos, todas estas direcciones estan referidas al ImageBase, de la misma forma que buscamos el valor del EntryPoint busquemos el ImageBase, esta variable está 3 Dword abajo del entrypoint, osea 8bytes, 0x0000000A8 + 0xC = 0x0000000B4 y ocupa 4 bytes, este es el valor "00 00 40 00" volteando = 0x00400000, bien, asi que el EntryPoint está realmente en 0x00400000 + 0x00001130 = 0x00401130 (eso lo podemos comprobar con algun otro programa como el olly)
Que pasa si queremos ver ese entrypoint en el archivo, como lo vemos?
primero hay que saber a que seccion corresponde, en este caso, es evidente que corresponde a .text veamos El VirtualAdress de .text, y el tamaño para acegurarnos de que está en esa seccion este es el encabezado de esa seccion (comienza en 0x000000178)
"000000170 00 00 00 00 00 00 00 00-2E 74 65 78 74 00 00 00 |########.text###|" "000000180 E8 59 00 00 00 10 00 00-00 5A 00 00 00 04 00 00 |#Y#######Z######|" "000000190 00 00 00 00 00 00 00 00-00 00 00 00 60 00 50 60 |############`#P`|"
su VirtualAdress esta a 8 Bytes + 1 Dword del inicio del encabezado, osea a 0xC bytes "00 10 00 00" = 0x00001000, tambien nos hara falta conocer el PointerToRawData "00 04 00 00"= 0x00000400
El virtual adress sabemos que es relativo al ImageBase, pero solo nos interesa saber la distancia a la que está el EntryPoint del inicio de esa seccion. asi que ese Offset sera EntryPint - VirtualAdress = 0x130.
Ese es el offset desde el inicio de la seccion, en el archivo esa seccion inicia en el PointerToRawData, asi que, el entrypoint en el archivo está en PointerToRawData + 0x130 = 0x530
Vemos el archivo
"000000530 55 89 E5 83 EC 18 C7 04-24 01 00 00 00 FF 15 50 |U#######$######P|" "000000540 A1 40 00 E8 D8 FE FF FF-90 8D B4 26 00 00 00 00 |#@#########&####|" "000000550 55 89 E5 53 83 EC 14 8B-45 08 8B 00 8B 00 3D 91 |U##S####E#####=#|" "000000560 00 00 C0 77 3B 3D 8D 00-00 C0 72 4B BB 01 00 00 |###w;=####rK####|" "000000570 00 C7 44 24 04 00 00 00-00 C7 04 24 08 00 00 00 |##D$#######$####|"
y lo comparamos con el dump del Entrypoint en memoria (visto con el Ollydbg)
00401130 55 89 E5 83 EC 18 C7 04 24 01 00 00 00 FF 15 50 U.å.ì.Ç.$......P 00401140 A1 40 00 E8 D8 FE FF FF 90 8D B4 26 00 00 00 00 .@.èØþ....&.... 00401150 55 89 E5 53 83 EC 14 8B 45 08 8B 00 8B 00 3D 91 U.åS.ì..E.....=. 00401160 00 00 C0 77 3B 3D 8D 00 00 C0 72 4B BB 01 00 00 ..Àw;=..ÀrK.... 00401170 00 C7 44 24 04 00 00 00 00 C7 04 24 08 00 00 00 .ÇD$.....Ç.$....
Podemos ver que el code coincide exactamente, ya que estamos parados en el mismo lugar.
Enlaces:
ESTUDIO DE LOS ENCABEZADOS PE (parte 1) POR SICK TROEN
ESTUDIO DE LOS ENCABEZADOS PE (parte 2) POR SICK TROEN
Estudios sobre las cabeceras AE - 1 - Las Secciones
Estudios sobre las cabeceras AE-2
Autor: Jep
0 comentarios:
Publicar un comentario