ESTUDIO COLECTIVO DE DESPROTECCIONES
Última Actualizacion: 25/10/2001

Programa

Code Snippet Creator 1.05:
Descabezando archivos ejecutables portables

W95 / W98 / NT
Descripción Herramienta para crear recortes (snippets) de código a ser insertos en archivos ejecutables de formato PE y agregar funcionalidades.
Tipo Ejecutable empaquetado
Tipo de Tutorial [X]Original, []Adaptación, []Aplicación, []Traducción
Url http://www.wco.com/~micuan/Tools/snipc105.zip
Protección Empaquetamiento de ejecutable con formato PE
Dificultad 1) Principiante, 2) Amateur, 3) Aficionado, 4) Profesional, 5) Especialista
Herramientas Hex Workshop u otro buen editor hexdecimal.
ProcDump v1.5
W32DASM32 8.93 y/o IDA PRO
eXeScope v4.40
Opcional: NotePad SoftIce v3.0 o superior Oxígeno
Objetivo Comprender la estructura y el contenido del encabezado de los archivos con formato PE
Cracker nuMIT_or
Grupo KuT
Fecha 15 de Octubre de 1999

INTRODUCCION

Descabezando archivos ejecutables portables

He tenido noticias de algunos interesados en conocer sobre un tema en el que he estado trabajando: el formato de los archivos ejecutables portables, conocidos como los "PE files". Como acostumbro a llevar notas de lo que hago, he reunido y ordenado este material esperando que pueda ser útil. No me responsabilizo del uso que hagan otros de él.

PE es el formato adoptado por W32 (Windows a 32 bits) para sus archivos ejecutables (EXE), sus librerías dinámicas (DLL), dispositivos virtuales (VXD), etc. W32 adoptó este formato debido a las posibilidades que quedaban abiertas al implementar direcciones y punteros de 32 bits y debido al manejo de la memoria que estas posibilidades exigían.

 

ALGO SOBRE MEMORIA VIRTUAL
=============================

W32 implementa un manejo de memoria que supone paginación, dividide la memoria física en bloques de memoria fija 4 KB llamados "páginas". Al proceso de dividir la memoria en páginas se llama paginación. Se trata de una técnica muy empleada en los sistemas operativos multitareas (capaces de ejecutar más de una tarea al mismo tiempo) para manejar la memoria dinámica del sistema (RAM = Randow Access Memory = Memoria de Acceso Aleatorio) de manera que ninguna tarea altere los datos de otra o la intervenga negativamente en su comportamiento.

W32 emplea punteros y direcciones de 32 bits. Esto quiere decir que, si lo tuvieramos, podríamos usar una memoria de 4GB para cada programa en ejecución. Pero aunque no tengamos todavía ships con esta capacidad de memoria, los programas en W32 corren como si la tuvieran a su disposición. El sistema W32 asigna a cada proceso o programa en ejecución un espacio de direcciones ficticio de 4GB, un montón de memoria que sólo existe imaginariamente, llamada "memoruia virtual". Luego, para ejecutar código del programa o disponer de sus datos, transforma, a través de un par de tablas, las direcciones ficticias en direcciones reales en la memoria física.

Para comprender cómo W32 transforma direcciones virtuales en direcciones físicas para que el CPU pueda acceder a las instrucciones del programa montadas en la RAM, sugiero la lectura del artículo "La Memoria en W32: intro" que seguramente acompaña a este que lees ahora.

El tema de la memoria virtual da para más y conviene tratarlo aparte. Ahora nos ocupan los PE.

Teoría y un poco de práctica.

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.

AL ATAQUE

DESEMPAQUETANDO EJECUTABLES
================================

Como ejercicio podemos tomar cualquier archivo ejecutable y examinar el encabezado con un editor hexadecimal, anotar los valores críticos y después comprobar si hemos acertado con cualquier volcador (dumper) de archivos PE, como EXESCOPE o PROCDUMP.

Empleemos PROCDUMP y tomemos como víctima SNIPPETCREATOR de Iczelion.
Se trata de un fabuloso programa ideado por un experto en W32 ASM. Además de permitir editar los encabezados de los archivos PE, sirve para insertar SNIPPETS (recortes) en archivos PE. En otras palabras, este programa permite insertar código en ejecutables, no sólo cambiar unos cuantos bytes, sino rutinas enteras.

Para ver un poco la importancia de conocer los encabezados PE, este programa resulta ejemplar. Veamos.

Supongamos que queremos hacer una lista muerta de SNIPPETCREATOR. Lo abrimos con WDASM32 8.9 o con IDA. ¡Qué extraño!. No vemos llamado a ninguna API W32.

Carguemos el programa con SICE ¡Cáspita!, ¿qué pasa...?. No se despliega el programa en la ventana de SICE. Se abre directamente.

Ahora despleguemos SICE (ctrl+D). Instalemos un BRKP: BPX GetModuleHandleA. Ahora F5 y volvemos a Win. Iniciemos SNIPPETCREATOR. ¡Ahora sí! Se despliega SICE. Tecleamos F11 y ya estamos en SNIPPETCREATOR. Hacemos alt+C y bajamos por la ventana de código. Ahora si encontramos un montón de llamadas a APIs W32. Esto no se entiende. ¿De dónde salieron estas
llamadas que no aparecieron en la lista muerta?

Tal vez si hacemos un volcado con un editor de archivos PE, como PROCDUMP, podamos encontrar alguna orientación. Abramos SNIPPETCREATOR con el editor de PROCDUMP.

¿Qué vemos en la ventana PE Structure Editor? El campo "Entry Point" (punto de entrada) tiene el valor 00018037h. Qué rareza. Generalmente este valor es 00001000h para los ejecutables PE.

Veamos ahora el editor de estructuras (Struct Editor): pulsemos el botón "Sections". Caramba: ninguna sección del archivo lleva nombre predefinido. Los nombres que encontramos son: UPX0, UPX1 y UPX3. El archivo tiene tres secciones con nombres no predefinidos.

Ahora resumamos las observaciones:
1. Encontramos diferencias entre la lista muerta generada por WDASM y el despliegue en memoria que nos presenta SICE.
2. El punto de entrada del programa no es el convencional.
3. Las secciones no tienen nombres predefinidos.
4. El programa no es desplegado inmediatamente en SICE con el LOADER

De 1 ya podemos deducir que el archivo está encriptado o empaquetado. Cuando generamos la lista muerta, lo que conseguimos es el código empaquetado. Para ejecutar el programa, el sistema debe montarlo en RAM, entonces debe desempaquetarlo primero. Esta es la importancia de los cargadores (LOADERS), desempaquetan los archivos y despliegan su contenido desempaquetado en memoria. Una vez hecho esto, podemos ver su verdadero contenido, incluso hasta volcarlo (dumping) a un archivo en disco duro. Esto explica la diferencia entre la lista muerta y el despliegue de SICE.

El punto 2 apoya nuestra primera conclusión. El programa inicia no en su primera instrucción sino en otra, inicia en el código que va desempaquetando el programa original.

El punto 3 es interesante ya que comienza a mostrarnos la importancia de conocer sobre los encabezados PE. Los nombres de las secciones, aunque no son los predefinidos, comienzan todos por el mismo prefijo: UPX. Si el archivo está empacado, el empacador utilizado cambia el nombre a las secciones y les asigna nuevos que inician con UPX. Veremos que con este dato es suficiente para desempacar SNIPPETCREATOR con PROCDUMP.

El punto 4 nos dice que la sección con las instrucciones ejecutables del programa no tiene asignado los atributos correctos. Esto lo veremos luego.

Nuestro análisis del punto 3 nos permite que ya procedamos a desempaquetar nuestra víctima. Pulsemos el botón "UNPACK" de PROCDUMP. En la caja de diálogo "Choose Unpacker" vemos una lista de desempacadores. Naveguemos por la lista. Aún sin saber mucho de desempacadores damos de inmediato con el desempacador correcto: UPX. Doble click sobre él, escogemos SNIPPETCREATOR, lo desempacamos y lo guardamos en disco duro.

Ahora abramos el archivo desempaquetado con el PE Editor de PROCDUMP. ¡WAO!: ha cambiado el valor del punto de entrada, ahora es 00001000h, el convencional. Ahora echemos un vistazo en "Sections". ¡DE NUEVO WAO!: Ahora tenemos cuatro secciones. A las anteriores tres UPXs se agrega .idata, es decir, la sección con datos sobre nombres importados.

También han cambiado las características (CHARACTERISTICS) y los desplazamientos virtuales (Virtual Offset) para las secciones. De esto hablaremos luego.

Ahora hagamos una lista muerta del archivo desempacado. Con WDASM aparecen las llamadas a APIs aunque por ordinales. Con IDA PRO sí aparecen las llamadas a las APIs por nombre. El archivo ha sido desempaquetado. Ahora podemos sentarnos cómodamente a analizarlo y aprender como se hace un buen editor de archivos PE.

En realidad no necesitamos PROCDUMP para hacer el desempaquetado. Sólo necesitamos hacer un volcado del encabezado PE y ver los nombres de las secciones para identificar el desempaquetador. También, conociendo el formato del encabezado, es suficiente abrir el archivo con un buen editor hexadecimal y buscar "manualmente" el encabezado de tablas de secciones y veremos los nombres incriminadores que revelan el empacador empleado.

A propósito, podemos encontrar UPX en http://www.nexus.hu/upx/. Es freeware. Para el momento en que escribo esto, la versión disponible es la v0.84. Si comparamos la diferencia de tamaño entre el ejecutable empaquetado y el desempaquetado, nos daremos cuenta de la calidad de este empaquetador. No desempaca paquetes de versiones anteriores, así que no sirve para desempacar SNIPPETCREATOR v1.05.

Ahora hasta podemos renombrar las secciones con los nombres predefinidos correspondientes, si quisieramos. Esto puede ser útil en ciertas ocasiones que por ahora no pienso mencionar.

Ahora, supongamos que no tenemos a la mano el desempaquetador preciso, ni tampoco lo suministra PROCDUMP. Entonces podemos recurrir a otro método, el descrito por Volatily en su artículo Manually Unpacking - ASPack v1.083, que se puede conseguir en el inmeso WEB site de fravia. Es un método un tanto engorroso, pero funciona. Lo único es que no desempaca la sección .idata, entonces cuando lo desensamblamos no aparecen los nombres de las funciones API W32 en sus llamadas.

Resumo los pasos de este procedimiento para desempacar manualmente un archivo ejecutable:

1. Abrimos la víctima (SNIPPETCREATOR) con el PE Editor de PROCDUMP.
2. Anotamos el punto de entrada (Entry Point): 00018037h
3. En el editor de estructuras (Structures Editor) pulso "Sections".
4. Anotamos los datos de la ventana Sections Editor:

5. Buscamos una sección ejecutable con la bandera de datos inicializados.
Es la sección UPX0:

 

000000040h datos inicializados.
200000000h ejecutable.
400000000h se puede leer.
800000000h se puede escribir en la sección.
--------------
E00000040h

7. Para que el programa pueda ser desplegado por SICE al iniciar, cambiamos las características de la sección UPX0 para hacerla una sección de código: reemplazamos E00000040h por E00000020h. Para hacer esto pulsamos sobre el nombre de la sección con el botón derecho del ratón y elegimos Edit Section del menú despegado. Se despliega ahora la caja de diálogo Modify section value. Reemplazamos el valor en el campo "Section Characteristics". Ahora podemos desplegar el ejecutable con el LOADER de SICE y desplegarlos en la pantalla del depurador. Anotamos también el desplazamiento (Virtual Offset) de esta sección: 1000h. Seguramente será la dirección donde inicia el programa original.

8. Cargamos la víctima con SICE. Bajamos por el código del programa empleando la tecla F10, teniendo cuidado de colocar puntos de ruptura (BREACKPOINTS) o trampas en las instrucciones inmediatamente después de aquellas donde hay saltos hacia arriba. Por ejemplo:
en la instrucción 0041806F hay un salto hacia arriba: JB 00418060, lo que indica que se trata de un bucle de miles vueltas. Entonces hacemos establecemos una trampa en la instrucción siguiente: BPX 00418073, y tecleamos F5 para pasar por alto este bucle. Este procedimiento debe repetirse para cada bucle encontrado, si no queremos tardar años.

9. En cualquier momento llegaremos a las instrucciones:

004181CA          61                        POPPAD
004181CB           E9308EfEFF       JMP 00401000

Es un salto a la dirección 00401000h, que es el desplazamiento donde suponemos que inicia el código original del programa. Basamos esta suposición en el hecho de que esta es la dirección que generalmente establece por defecto el enlazador como punto de entrada. también en el hecho de que una de las secciones, seguramente la de código (.text) inicia aquí. Para asegurarnos que este es el caso, hacemos BPX 004181CB y seguimos (F10). Cuando arribamos a la dirección 00401000h ya podemos ver llamadas a funciones W32 API. Quiere decir que estamos en la sección de código.

10. F5 para salir de SICE. Cerramos la víctima y la corremos de nuevo. Seguramente se desplegará SICE en algunos de los BREAKPOINTS que dejamos cuando saltábamos bucles grandes.Debemos pulsar F5 hasta llegar a 004181CB.

11. Cuando llegamos aquí, cambiamos la instrucción:
004181CB          E9308EfEFF       JMP 00401000

por:
004181CB          E9308EfEFF       JMP 004181CB

Lo hacemos así:
a 004181CB jmp 004181CB

12. F5 para volver a Win. El programa se ha quedado en un bucle infinito en la memoria. Abrimos PROCDUMP. Buscamos en la ventana de tareas (Tasks) de PROCDUMP a la víctima. Pulsamos sobre ella con el botón derecho del ratón. Elegimos "Dump (Full)" en el menú desplegado y guardamos el archivo desempacado en disco.

13. Matamos la tarea víctima.

14. Cambiamos el punto de entrada: abrimos el archivo desplegado con el PE Editor de PROCDUMP, y reemplazamos el valor en Entry Point por 00001000.

15. Abrimos el archivo ya desempaquetado. Perfecto: funciona. Como vemos su tamaño se ha incrementado una barbaridad, lo que indica la potencia del empaquetador UPX.

Si desensamblamos este desempaquetado con WDASM o con IDA, veremos los problemas de este método de desempaquetado: no se desempaqueta la sección de importaciones .idata, por lo tanto, no se despliegan los nombres de las funciones W32 API.

 

ALGUNAS PREGUNTAS - ALGUNAS RESPUESTAS ==========================================

Los primeros bytes del archivo... ¿Corresponden siempre y únicamente a la cabecera?

Imagino que sí. El encabezado puede diividirse en cuatro grandes partes. La primera es el viejo encabezado DOS. El último campo de este encabezado apunta a un campo con un par de caracteres que dicen el formato del archivo ejecutable. Ahora, imaginate que este encabezado no esté aquí. El cargador no podría encontrar el encabezado PE ni tampoco podría determinar si se trata de un archivo PE (W32) o NE (W16) o LE (OS2). Hay algunas direcciones que varían pero cuando esto ocurre, algún campo del encabezado especifica dónde han sido relocalizados los datos.

El encabezado es una estructura de datos por la cual el cargador de W32 siempre se orienta para acelerar la carga del ejecutable en vez de implementar algún algorritmo inteligente de búsqueda que impicaría un mayor costo en cuanto tiempo y trabajo de programación.

 

¿Cuantos bytes tiene la cabecera?

Respecto al tamaño, hay un campo que especifica este valor y se llama SizeOfHeaders y está en el encabezado PE (estructura _IMAGE_FILE_HEADER). El valor varía de acuerdo al número de secciones u objetos del PE.

 

El valor decimal de "size of image" en ProcDump no corresponde al tamaño real del fichero. ¿Por qué?

Esto es consecuencia de los alineamientos. Realmente este campo indica la cantidad de espacio que se reserva en el espacio de direcciones para cargar el ejecutable. Una cosa es la memoria virtual reservada y otra la memoria física comprometida. Yo puedo reservar 10 MB e ir comprometiendo bloques de 300 KB según la necesidad que tenga de ello. Evidentemente, la memoria reservada deberá ser igual o mayor al tamaño de la imagen. El valor de "size of image"que vemos, por ejemplo, en ProcDump depende del valor "SectionAlignement", es decir, del alineamiento de las secciones. Es uno de los campos opcionales de la estructura _IMAGE_FILE_HEADER.

Una página mide 4096 bytes. Si un PE tiene 3 secciones, todas de un tamaño menor a 4096 bytes, y si estas, de acuerdo a SectionAlignement" están alineadas en límites de 65.536 bytes, el tamaño de la imagen será 3 * 65.356 = 196.608 bytes.

Si yo estoy creando el programa puedo controlar el valor de "size of image" variando "SectionAlignement" en las opciones del enlazador (de TLINK o de LINK), pero esto no es necesario, porque el mismo enlazador determina el valor de "SectionAlignement" a partir del tamaño de cada sección.

 

Si la mayoría de los ejecutables tienen la misma dirección base 00400000h ¿como pueden proyectarse varios ejecutables a la vez? ¿Qué pasa cuando dos ejecutables tienen igual la: direccionbase + puntoentrada?

Esto se aclara viendo cómo W32 convierte direcciones virtuales en direcciones reales. Es una cuestión crucial en SOs multiprocesos. W32 es uno de estos SOs, también UNIX y LINUX. Para no enredar mucho las cosas, se puede decir que W32 crea en memoria física una tabla para cada proceso en ejecución. Esta tabla contiene direcciones de entradas de otra tabla que son punteros hacia las direcciones en memoria real donde se encuentran las instrucciones o los datos cargados en la RAM. La dirección física de la primera entrada de la tabla que transforma direcciones virtuales en reales está en un registro del CPU llamado CR3 (CONTROL REGISTER 3). Para cualquier aplicación puedes obtener el valor en este registro empleando SICE simplemente con el comando CPU, el cual muestra el valor en los diferentes registros del CPU. Hay que tener en cuenta que el valor de CR3 es sólo la dirección física de la primera entrada de una tabla de punteros a direcciones físicas de otra tabla. Se requiere todavía tener el número de la entrada de esta primera tabla para saber donde está en la memoria física la segunda tabla.

Fíjate que casi todas las direcciones virtuales de una aplicación comienzan con 0040XXXXh. No voy a explicarlo, pero esto indica la primera entrada de la tabla apuntada por CR3. Lo cierto es que cada tabla primaria es también una página de 4096 bytes, dividida en 1024 entradas de 32 bytes que son punteros a la base de otra tabla en memoria física.

Ahora, ¿por qué un ejecutable en memoria no interrumpe a otro, aunque tengan las mismas bases virtuales? Las posibles respuestas a esta pregunta me parece que ya se tienen de alguna manera. La dirección real o física de la página de directorio activa se almacena en el registro CR3 del CPU, el cual cambia cada vez que W32 conmuta el control entre procesos. Para cada proceso, el sistema crea un directorio de página particular. Para procesos distintos no deben coincidir los directorios de página, de lo contrario se producirá un error de protección, como a veces ocurre.

 

¿Qué utilidad tiene dividir los ejecutables en secciones?

Para facilitar la ubicación de los datos. Además porque, como expliqué, cada sección tiene diferente función y diferentes atributos. Imagina el enrredo si el enlazador al crear el programa pusiera el código junto a los datos, mezclados con los recursos y otras cosas: uff! Esto es así siempre, incluso en DOS. Lo que pasa es que en DOS se segmenta la memoria; W32 la pagina. También podemos encontrar otras razones que exigirían el manejo de conceptos que finalmente terminan enrredándonos más y haciéndonos parecer que se trata de una cosa muy compleja. Se trata de explicaciones que podemos encontrar en libros sobre arquitectura de computadoras. Hay varios en español, posiblemente en alguna biblioteca.

 

W32 puede manejar 4 Gb ffff:ffffffff que vemos en el sice. FFFFh x FFFFFFFFh = FFFF0001h = 4294901761 bytes = 4 Gb Lo cual quiere decir que las direcciones que siempre vemos en sice no son de la ram sino de la memoria virtual ¿no?

Muy cierto. Esto creo que cambiará el día que ya no necesitemos memoria virtual. De todos modos, si se tiene suficiente cantidad de RAM, en ocasiones podemos prescindir de ella. Yo nunca he probado, pero me han comentado que es más rápido.

 

Las paginas de memoria parecen importantes ¿qué son? ¿Qué relación tienen con las direcciones que vemos en SICE?

W32 divide la memoria física en "páginas" que tiene una longitud de 4096 bytes (4 KB). Una máquina con 8 MB de memoria tiene 2048 páginas. W32 mantiene una colección de tablas de página (también de 4 KB) para traducir direcciones virtuales a direcciones físicas. Cada proceso tiene su propia "página de directorio": una colección de hasta 1024 entradas de 32 bits almacenadas contiguamente. La dirección real o física de la página de directorio activa se almacena en el registro CR3 del CPU, el cual cambia cada vez que W32 conmuta el control entre procesos. Por eso, las direcciones virtuales iguales entre dos procesos diferentes no apuntan a la misma dirección física, a no ser que refieran a espacios de memoria compartidos.

Las direcciones que despliega SICE son virtuales y a partir de ellas se puede determinar la dirección de memoria física donde están los datos o instrucciones correspondientes. Para ello se puede emplear el comando PHYS, que busca la tabla de páginas y el Directorio de páginas asociado con el contexto de direcciones actual desplegado por SICE. Es decir, PHYS despliega todas las direcciones virtuales para direcciones físicas. Puede emplearse PEEK para leer desde la memoria física, ya que muestra el byte, word, o dword en una dirección física dada. PEEK es útil para leer registros de entrada o salida proyectados en memoria. También es útil el comando PAGE, el cual permite ver la proyección de todo el rango de direcciones lineales. Esto permite obtener la dirección física del directorio de página y verificar si las tablas de páginas del sistema operativo están proyectadas en la dirección lineal 0xC0000000. Véase La Memoria en W32: intro.

 

¿Por qué necesitamos alineamientos? ¿ Por qué FileAligment usualmente usa límites en 512 bytes y SectionAlignment (después de que la imagen fue cargada en RAM) usualmente usa límites de 4096 bytes?

W32 trata con paginación de memoria física y usa memoria virtual. Entonces W32 tiene que manejar memoria virtual y debe tener una manera rápida de encontrar los datos del archivo. Estas son las razones de la existencia del encabezado PE.

W32 pagina la memoria física, la divide en regiones de tamaño fijo. En la arquitectura x86 cada página tiene un tamaño de 4096 bytes. Cualquier sección cargada en RAM debe tener un alineamiento para soportar paginación de memoria. Cuando el sistema reserva memoria, asegura que la región reservada sea un múltiplo par del tamaño de página. SectionAlignment garantiza que cada sección comience en una dirección virtual alineada a una página

FileAlignment es un valor que debe ser potencia de 2 entre 512 y 65,535. Es "la granularidad mínima de pedazos (chunks) de información dentro de la imagen antes de ser cargada" [Katz]. 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 (el cual también tiene un tamaño de 512).

 

¿Qué es un proceso?

W32 llama proceso a un programa en ejecución. Un proceso posee un espacio de 4 GB con los datos y el código del archivo EXE de la aplicación. Un proceso es inerte, no hace nada. Para que haga algo debe poseer un hilo, el cual se responsabiliza de ejecutar el código presente en el espacio de direcciones del proceso. Incluso, un proceso puede tener más de un hilo en el mismo espacio de direcciones ejecutándose al mismo tiempo. Pero el proceso necesita por lo menos un hilo para ejecutar el código proyectado en su espacio de direcciones, en caso contrario el sistema destruye el proceso y su espacio de direcciones.

 

¿Qué es un hilo (thread)?

Es la descripción de la trayectoria que sigue la ejecución de un proceso. Cuando arranca un proceso, el sistema crea un hilo principal (llamando a CreateThread) el cual comienza llamando a la función WinMain, ejecutándola hasta alcanzar la función ExitProcess que culmina el proceso. La noción de hilo fue implementada por W32 para dar cuenta de la posiblidad de los procesos W32 de correr más de un subproceso al mismo tiempo. W32 llama hilos a los subprocesos.


Bueno. Lo visto hasta ahora puede servir como una introducción al conocimiento de los archivos con formato PE y para adquirir cierta conciencia de la importancia de este asunto. Realmente deberíamos profundizar más.

Como vieron he escogido como víctima a SNIPPETCREATOR. No es casual. Se trata de un programa muy poderoso, pero que requiere conocimiento de programación en ASM W32 y conocer el formato PE. Si tienes a la mano MASM 6.X o TASM 5.X, entonces podemos avanzar ya al estudio de SNIPPETCREATOR, lo cual nos dará mayor conocimiento sobre los archivos PE:

snippetcreator: tutorial (en elaboración)

 

Si no tienes conocimiento del lenguaje ensamblador para W32, pero todavía te interesa el tema de los PE, entonces pasemos a estudiar la sección de recursos de estos archivos.

Y tú, cabeza de recursos (en elaboración)

 

 


GRACIAS A:

  • GERZA: por haberme animado a escribir sobre el tema y orientarme cómo hacerlo.
  • Iczelion y Stone: creadores de dos herramientas maravillosas.

Observaciones y correcciones, comunicarse con:

nuMIT_or@iname.com

[ Entrada | Documentos Genéricos | WkT! Web Site ]
[ Todo el ECD | x Tipo de Protección | x Fecha de Publicación | x Orden Alfabético ]
(c) Whiskey Kon Tekila [WkT!] - The Original Spanish Reversers.
Si necesitas contactar con
nosotros , lee esto antes e infórmate de cómo puedes ayudarnos