Ahora es capaz de compilar Hello World, pero en este artículo quiero hablar no sobre el análisis y la estructura interna del compilador, sino sobre una parte tan importante como el ensamblaje byte por byte del archivo exe.
comienzo
¿Quieres un spoiler? Nuestro programa será de 2048 bytes.
Normalmente, trabajar con archivos exe es estudiar o modificar su estructura. Los propios archivos ejecutables están formados por los compiladores, y este proceso parece un poco mágico para los desarrolladores.
¡Pero ahora intentaremos arreglarlo!
Para construir nuestro programa, necesitamos cualquier editor HEX (yo personalmente usé HxD).
Para empezar, tomemos el pseudocódigo:
Fuente
func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll']
func ExitProcess(u32 code) ['kernel32.dll']
func main()
{
MessageBoxA(0, 'Hello World!', 'MyApp', 64)
ExitProcess(0)
}
Las dos primeras líneas indican funciones importadas de bibliotecas WinAPI . La función MessageBoxA muestra un cuadro de diálogo con nuestro texto, y ExitProcess informa al sistema sobre el final del programa.
No tiene sentido considerar la función principal por separado, ya que utiliza las funciones descritas anteriormente.
Encabezado DOS
Primero, necesitamos generar el encabezado de DOS correcto, este es un encabezado para programas de DOS y no debería afectar el inicio de exe en Windows.
Noté campos más o menos importantes, el resto están llenos de ceros.
Estructura IMAGE_DOS_HEADER
Struct IMAGE_DOS_HEADER
{
u16 e_magic // 0x5A4D "MZ"
u16 e_cblp // 0x0080 128
u16 e_cp // 0x0001 1
u16 e_crlc
u16 e_cparhdr // 0x0004 4
u16 e_minalloc // 0x0010 16
u16 e_maxalloc // 0xFFFF 65535
u16 e_ss
u16 e_sp // 0x0140 320
u16 e_csum
u16 e_ip
u16 e_cs
u16 e_lfarlc // 0x0040 64
u16 e_ovno
u16[4] e_res
u16 e_oemid
u16 e_oeminfo
u16[10] e_res2
u32 e_lfanew // 0x0080 128
}
Lo más importante es que este encabezado contiene el campo e_magic, lo que significa que se trata de un archivo ejecutable, y e_lfanew, que indica el desplazamiento del encabezado PE desde el principio del archivo (en nuestro archivo, este desplazamiento es 0x80 = 128 bytes).
Genial, ahora que conocemos la estructura del encabezado DOS, escribámoslo en nuestro archivo.
(1) Encabezado de DOS RAW (Desplazamiento 0x00000000)
4D 5A 80 00 01 00 00 00 04 00 10 00 FF FF 00 00
40 01 00 00 00 00 00 00 40 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00
Hecho, se escriben los primeros 64 bytes. Ahora necesita agregar 64 más, este es el llamado DOS Stub (Stub). Cuando se inicia desde DOS, debe notificar al usuario que el programa no está diseñado para ejecutarse en este modo.
, , .
, (Offset) .
, 0x00000000, 64 (0x40 16- ), 0x00000040 ..
Pero en general, este es un pequeño programa de DOS que imprime una línea y sale del programa.
Escribamos nuestro Stub en un archivo y lo consideremos con más detalle.
(2) Stub de DOS RAW (Desplazamiento 0x00000040)
0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68
69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F
74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20
6D 6F 64 65 2E 0D 0A 24 00 00 00 00 00 00 00 00
Y ahora el mismo código, pero en forma desmontada
Stub de DOS de ASM
0000 push cs ; Code Segment(CS) ( )
0001 pop ds ; Data Segment(DS) = CS
0002 mov dx, 0x0E ; DS+DX, $( )
0005 mov ah, 0x09 ; ( )
0007 int 0x21 ; 0x21
0009 mov ax, 0x4C01 ; 0x4C ( )
; 0x01 ()
000c int 0x21 ; 0x21
000e "This program cannot be run in DOS mode.\x0D\x0A$" ;
Funciona así: primero, el stub imprime una línea que indica que el programa no puede iniciarse y luego sale del programa con el código 1. Que es diferente de la terminación normal (Código 0).
El código auxiliar puede diferir ligeramente (de un compilador a otro). Comparé gcc y delphi, pero el significado general es el mismo.
También es gracioso que la línea auxiliar termine con \ x0D \ x0D \ x0A $. Lo más probable es que la razón de este comportamiento sea que c ++ abre el archivo en modo texto de forma predeterminada. Como resultado, el carácter \ x0A se reemplaza con la secuencia \ x0D \ x0A. Como resultado, obtenemos 3 bytes: 2 bytes de retorno de carro (0x0D) que no tiene sentido, y 1 para avance de línea (0x0A). En modo binario (std :: ios :: binary), esta sustitución no ocurre.
Para verificar la exactitud de la escritura de los valores, usaré Far con el complemento ImpEx:
Encabezado NT
Después de 128 (0x80) bytes, llegamos al encabezado NT (IMAGE_NT_HEADERS64), que también contiene el encabezado PE (IMAGE_OPTIONAL_HEADER64). A pesar del nombre, se requiere IMAGE_OPTIONAL_HEADER64, pero es diferente para las arquitecturas x64 y x86.
Estructura IMAGE_NT_HEADERS64
Struct IMAGE_NT_HEADERS64
{
u32 Signature // 0x4550 "PE"
Struct IMAGE_FILE_HEADER
{
u16 Machine // 0x8664 x86-64
u16 NumberOfSections // 0x03
u32 TimeDateStamp //
u32 PointerToSymbolTable
u32 NumberOfSymbols
u16 SizeOfOptionalHeader // IMAGE_OPTIONAL_HEADER64 ()
u16 Characteristics // 0x2F
}
Struct IMAGE_OPTIONAL_HEADER64
{
u16 Magic // 0x020B PE64
u8 MajorLinkerVersion
u8 MinorLinkerVersion
u32 SizeOfCode
u32 SizeOfInitializedData
u32 SizeOfUninitializedData
u32 AddressOfEntryPoint // 0x1000
u32 BaseOfCode // 0x1000
u64 ImageBase // 0x400000
u32 SectionAlignment // 0x1000 (4096 )
u32 FileAlignment // 0x200
u16 MajorOperatingSystemVersion // 0x05 Windows XP
u16 MinorOperatingSystemVersion // 0x02 Windows XP
u16 MajorImageVersion
u16 MinorImageVersion
u16 MajorSubsystemVersion // 0x05 Windows XP
u16 MinorSubsystemVersion // 0x02 Windows XP
u32 Win32VersionValue
u32 SizeOfImage // 0x4000
u32 SizeOfHeaders // 0x200 (512 )
u32 CheckSum
u16 Subsystem // 0x02 (GUI) 0x03 (Console)
u16 DllCharacteristics
u64 SizeOfStackReserve // 0x100000
u64 SizeOfStackCommit // 0x1000
u64 SizeOfHeapReserve // 0x100000
u64 SizeOfHeapCommit // 0x1000
u32 LoaderFlags
u32 NumberOfRvaAndSizes // 0x16
Struct IMAGE_DATA_DIRECTORY [16]
{
u32 VirtualAddress
u32 Size
}
}
}
Veamos qué se almacena en esta estructura:
Descripción IMAGE_NT_HEADERS64
Signature — PE
IMAGE_FILE_HEADER x86 x64.
Machine — x64
NumberOfSections — ( )
TimeDateStamp —
SizeOfOptionalHeader — IMAGE_OPTIONAL_HEADER64, IMAGE_OPTIONAL_HEADER32.
Characteristics — , , (EXECUTABLE_IMAGE) 2 RAM (LARGE_ADDRESS_AWARE), ( ) (RELOCS_STRIPPED | LINE_NUMS_STRIPPED | LOCAL_SYMS_STRIPPED).
SizeOfCode — ( .text)
SizeOfInitializedData — ( .rodata)
SizeOfUninitializedData — ( .bss)
BaseOfCode —
SectionAlignment —
FileAlignment —
SizeOfImage —
SizeOfHeaders — (IMAGE_DOS_HEADER, DOS Stub, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER[IMAGE_FILE_HEADER.NumberOfSections]) FileAlignment
Subsystem — GUI Console
MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorSubsystemVersion, MinorSubsystemVersion — exe, . 5.2 Windows XP (x64).
SizeOfStackReserve — . 1 , 1. Rust , C++ .
SizeOfStackCommit — 4 . .
SizeOfHeapReserve — . 1 .
SizeOfHeapCommit — 4 . SizeOfStackCommit, .
IMAGE_DATA_DIRECTORY — . , , 16 . .
, , . :
Export(0) — . DLL. .
Import(1) — DLL. VirtualAddress = 0x3000 Size = 0xB8. , .
Resource(2) — (, , ..)
.
IMAGE_FILE_HEADER x86 x64.
Machine — x64
NumberOfSections — ( )
TimeDateStamp —
SizeOfOptionalHeader — IMAGE_OPTIONAL_HEADER64, IMAGE_OPTIONAL_HEADER32.
Characteristics — , , (EXECUTABLE_IMAGE) 2 RAM (LARGE_ADDRESS_AWARE), ( ) (RELOCS_STRIPPED | LINE_NUMS_STRIPPED | LOCAL_SYMS_STRIPPED).
SizeOfCode — ( .text)
SizeOfInitializedData — ( .rodata)
SizeOfUninitializedData — ( .bss)
BaseOfCode —
SectionAlignment —
FileAlignment —
SizeOfImage —
SizeOfHeaders — (IMAGE_DOS_HEADER, DOS Stub, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER[IMAGE_FILE_HEADER.NumberOfSections]) FileAlignment
Subsystem — GUI Console
MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorSubsystemVersion, MinorSubsystemVersion — exe, . 5.2 Windows XP (x64).
SizeOfStackReserve — . 1 , 1. Rust , C++ .
SizeOfStackCommit — 4 . .
SizeOfHeapReserve — . 1 .
SizeOfHeapCommit — 4 . SizeOfStackCommit, .
IMAGE_DATA_DIRECTORY — . , , 16 . .
, , . :
Export(0) — . DLL. .
Import(1) — DLL. VirtualAddress = 0x3000 Size = 0xB8. , .
Resource(2) — (, , ..)
.
Ahora que hemos visto en qué consiste el encabezado NT, también lo escribiremos en un archivo por analogía con los demás en 0x80.
(3) Encabezado NT RAW (Desplazamiento 0x00000080)
50 45 00 00 64 86 03 00 F4 70 E8 5E 00 00 00 00
00 00 00 00 F0 00 2F 00 0B 02 00 00 3D 00 00 00
13 00 00 00 00 00 00 00 00 10 00 00 00 10 00 00
00 00 40 00 00 00 00 00 00 10 00 00 00 02 00 00
05 00 02 00 00 00 00 00 05 00 02 00 00 00 00 00
00 40 00 00 00 02 00 00 00 00 00 00 02 00 00 00
00 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00
00 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00
00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00
00 30 00 00 B8 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
Como resultado, obtenemos este tipo de encabezados IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER64 e IMAGE_DATA_DIRECTORY:
A continuación, describimos todas las secciones de nuestra aplicación de acuerdo con la estructura IMAGE_SECTION_HEADER
Estructura IMAGE_SECTION_HEADER
Struct IMAGE_SECTION_HEADER
{
i8[8] Name
u32 VirtualSize
u32 VirtualAddress
u32 SizeOfRawData
u32 PointerToRawData
u32 PointerToRelocations
u32 PointerToLinenumbers
u16 NumberOfRelocations
u16 NumberOfLinenumbers
u32 Characteristics
}
Descripción de IMAGE_SECTION_HEADER
Name — 8 ,
VirtualSize —
VirtualAddress — SectionAlignment
SizeOfRawData — FileAlignment
PointerToRawData — FileAlignment
Characteristics — (, , , , .)
VirtualSize —
VirtualAddress — SectionAlignment
SizeOfRawData — FileAlignment
PointerToRawData — FileAlignment
Characteristics — (, , , , .)
En nuestro caso, tendremos 3 secciones.
Por qué Virtual Address (VA) comienza desde 1000, y no desde cero, no lo sé, pero todos los compiladores que consideré hacen esto. Como resultado, 1000 + 3 secciones * 1000 (SectionAlignment) = 4000 que escribimos en SizeOfImage. Este es el tamaño total de nuestro programa en la memoria virtual. Probablemente se usa para asignar espacio para un programa en la memoria.
Name | RAW Addr | RAW Size | VA | VA Size | Attr
--------+---------------+---------------+-------+---------+--------
.text | 200 | 200 | 1000 | 3D | CER
.rdata | 400 | 200 | 2000 | 13 | I R
.idata | 600 | 200 | 3000 | B8 | I R
Decodificación de atributos:
I - Datos inicializados, datos inicializados
U - Datos no inicializados, datos no inicializados
C - Código, contiene código ejecutable
E - Ejecutar, permite ejecutar
R - Leer código , permite leer datos de la sección
W - Escribir, permite escribir datos en la sección
.text (.code): almacena el código ejecutable (el programa en sí), atributos CE
.rdata (.rodata): almacena datos de solo lectura, como constantes, cadenas, etc., atributos IR
.data: almacena datos que se pueden leer y escribir, como variables estáticas o globales. Atributos de IRW
.bss: almacena datos no inicializados, como variables estáticas o globales. Además, esta sección generalmente tiene un tamaño RAW cero y un tamaño VA distinto de cero, por lo que no ocupa espacio en el archivo.
Atributos URW .idata: una sección que contiene funciones importadas de otras bibliotecas. Atributos de IR
Un punto importante, las secciones deben seguirse unas a otras. Además, tanto en el archivo como en la memoria. Al menos cuando cambié su orden arbitrariamente, el programa dejó de ejecutarse.
Ahora que sabemos qué secciones contendrá nuestro programa, las escribiremos en nuestro archivo. Aquí, el desplazamiento termina en 8 y la grabación comenzará desde la mitad del archivo.
(4) Secciones RAW (Desplazamiento 0x00000188)
2E 74 65 78 74 00 00 00
3D 00 00 00 00 10 00 00 00 02 00 00 00 02 00 00
00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60
2E 72 64 61 74 61 00 00 13 00 00 00 00 20 00 00
00 02 00 00 00 04 00 00 00 00 00 00 00 00 00 00
00 00 00 00 40 00 00 40 2E 69 64 61 74 61 00 00
B8 00 00 00 00 30 00 00 00 02 00 00 00 06 00 00
00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 40
La siguiente dirección de escritura será 00000200 que corresponde al campo SizeOfHeaders del PE-Header. Si agregamos una sección más, y esto es más 40 bytes, entonces nuestros encabezados no cabrían en 512 (0x200) bytes y tendrían que usar 512 + 40 = 552 bytes alineados por FileAlignment, es decir, 1024 (0x400) bytes. Y todo lo que queda desde 0x228 (552) hasta la dirección 0x400 debe llenarse con algo, mejor por supuesto con ceros.
Echemos un vistazo a cómo se ve un bloque de secciones en Far:
A continuación, escribiremos las secciones en nuestro archivo, pero hay un matiz.
Como puede ver con el ejemplo de SizeOfHeaders, no podemos simplemente escribir el encabezado y pasar a la siguiente sección. Dado que para registrar un encabezado necesitamos saber cuánto tiempo tomarán todos juntos. Como resultado, necesitamos calcular de antemano cuánto espacio se necesita o escribir valores vacíos (cero) y, después de escribir todos los encabezados, devolver y escribir su tamaño real.
Por lo tanto, los programas se compilan en varias pasadas. Por ejemplo, la sección .rdata viene después de la sección .text, mientras que no podemos encontrar la dirección virtual de la variable en .rdata, porque si la sección .text crece en más de 0x1000 (SectionAlignment) bytes, ocupará las direcciones 0x2000 del rango. Y, en consecuencia, la sección .rdata ya no estará ubicada en 0x2000, sino en 0x3000. Y tendremos que volver atrás y recalcular las direcciones de todas las variables en la sección .text que viene antes de .rdata.
Pero en este caso, ya he calculado todo, por lo que inmediatamente escribiremos los bloques de código.
.Sección de texto
Asm segmento .text
0000 push rbp
0001 mov rbp, rsp
0004 sub rsp, 0x20
0008 mov rcx, 0x0
000F mov rdx, 0x402000
0016 mov r8, 0x40200D
001D mov r9, 0x40
0024 call QWORD PTR [rip + 0x203E]
002A mov rcx, 0x0
0031 call QWORD PTR [rip + 0x2061]
0037 add rsp, 0x20
003B pop rbp
003C ret
Específicamente para este programa, las primeras 3 líneas, exactamente como las últimas 3, son opcionales.
Los últimos 3 ni siquiera se ejecutarán, ya que el programa saldrá en la segunda función llamada.
Pero digamos esto, si no fuera la función principal, sino una subfunción, debería hacerse de esta manera.
Pero los 3 primeros en este caso, aunque no son necesarios, son deseables. Por ejemplo, si no usamos MessageBoxA, sino printf, sin estas líneas recibiríamos un error.
De acuerdo con la convención de llamadas para sistemas MSDN de 64 bits, los primeros 4 parámetros se pasan en los registros RCX, RDX, R8, R9. Si encajan allí y no son, por ejemplo, un número de coma flotante. Y el resto se pasa por la pila.
En teoría, si pasamos 2 argumentos a una función, entonces debemos pasarlos a través de registros y reservar dos lugares en la pila para ellos, de modo que, si es necesario, la función pueda empujar los registros a la pila. Además, no debemos esperar que estos registros nos sean devueltos en su estado original.
Entonces, el problema con la función printf es que si le pasamos solo 1 argumento, aún sobrescribirá los 4 lugares de la pila, aunque parece tener que sobrescribir solo uno, por el número de argumentos.
Por lo tanto, si no desea que el programa se comporte de manera extraña, siempre reserve al menos 8 bytes * 4 argumentos = 32 (0x20) bytes si pasa al menos 1 argumento a la función.
Considere un bloque de código con llamadas a funciones
MessageBoxA(0, 'Hello World!', 'MyApp', 64)
ExitProcess(0)
Primero pasamos nuestros argumentos:
rcx = 0
rdx = la dirección absoluta de la cadena en la memoria ImageBase + Secciones [". Rdata"]. VirtualAddress + Offset de la cadena desde el principio de la sección, la cadena se lee al byte cero
r8 = similar al anterior
r9 = 64 (0x40) MB_ICONINFORMATION , icono de información
Y luego hay una llamada a la función MessageBoxA, con la que no todo es tan sencillo. El punto es que los compiladores intentan utilizar los comandos más cortos posibles. Cuanto menor sea el tamaño de la instrucción, más instrucciones encajarán en la caché del procesador, respectivamente, habrá menos pérdidas de caché, sobrecargas y mayor velocidad del programa. Para obtener más información sobre los comandos y el funcionamiento interno del procesador, consulte los Manuales para desarrolladores de software de arquitecturas Intel 64 e IA-32.
Podríamos llamar a la función en la dirección completa, pero eso tomaría al menos (1 código de operación + 8 direcciones = 9 bytes), y con una dirección relativa, el comando de llamada toma solo 6 bytes.
Echemos un vistazo más de cerca a esta magia: rip + 0x203E no es más que una llamada de función en la dirección especificada por nuestro desplazamiento.
Miré un poco más adelante y encontré las direcciones de las compensaciones que necesitamos. Para MessageBoxA es 0x3068 y para ExitProcess es 0x3098.
Es hora de convertir la magia en ciencia. Cada vez que un código de operación llega al procesador, calcula su longitud y la agrega a la dirección de instrucción actual (RIP). Por lo tanto, cuando usamos RIP dentro de una instrucción, esta dirección indica el final de la instrucción actual / el comienzo de la siguiente.
Para la primera llamada, el desplazamiento indicará el final del comando de llamada, esto es 002A. No olvide que en la memoria esta dirección estará en las Secciones de desplazamiento [". Texto"]. VirtualAddress, es decir 0x1000. Por lo tanto, el RIP de nuestra llamada será 102A. La dirección que necesitamos para MessageBoxA es 0x3068. Considere 0x3068 - 0x102A = 0x203E . Para la segunda dirección, todo es igual que 0x1000 + 0x0037 = 0x1037, 0x3098 - 0x1037 = 0x2061 .
Son estas compensaciones las que vimos en los comandos del ensamblador.
0024 call QWORD PTR [rip + 0x203E]
002A mov rcx, 0x0
0031 call QWORD PTR [rip + 0x2061]
0037 add rsp, 0x20
Escribamos la sección .text en nuestro archivo, agregando ceros a la dirección 0x400:
(5) Sección de texto RAW (Offset 0x00000200-0x00000400)
55 48 89 E5 48 83 EC 20 48 C7 C1 00 00 00 00 48
C7 C2 00 20 40 00 49 C7 C0 0D 20 40 00 49 C7 C1
40 00 00 00 FF 15 3E 20 00 00 48 C7 C1 00 00 00
00 FF 15 61 20 00 00 48 83 C4 20 5D C3 00 00 00
........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
4 . FileAlignment. 0x000003F0, 0x00000400, . 1024 , ! .
.Rdata sección
Esta es quizás la sección más simple. Solo pondremos dos líneas aquí, agregando ceros a 512 bytes.
.rdata
0400 "Hello World!\0"
040D "MyApp\0"
(6) Sección RAW .rdata (Offset 0x00000400-0x00000600)
48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00 4D 79 41
70 70 00 00 00 00 00 00 00 00 00 00 00 00 00 00
........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
.Sección de datos
Bueno, aquí está la última sección, que describe las funciones importadas de las bibliotecas.
Lo primero que nos espera es la nueva estructura IMAGE_IMPORT_DESCRIPTOR
Estructura IMAGE_IMPORT_DESCRIPTOR
Struct IMAGE_IMPORT_DESCRIPTOR
{
u32 OriginalFirstThunk (INT)
u32 TimeDateStamp
u32 ForwarderChain
u32 Name
u32 FirstThunk (IAT)
}
Descripción IMAGE_IMPORT_DESCRIPTOR
OriginalFirstThunk — , Import Name Table (INT)
Name — ,
FirstThunk — , Import Address Table (IAT)
Name — ,
FirstThunk — , Import Address Table (IAT)
Primero, necesitamos agregar 2 bibliotecas importadas. Recordar:
func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll']
func ExitProcess(u32 code) ['kernel32.dll']
(7) IMAGE_IMPORT_DESCRIPTOR RAW (Desplazamiento 0x00000600)
58 30 00 00 00 00 00 00 00 00 00 00 3C 30 00 00
68 30 00 00 88 30 00 00 00 00 00 00 00 00 00 00
48 30 00 00 98 30 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
Usamos 2 bibliotecas, y decir que hemos terminado de enumerarlas. La última estructura está llena de ceros.
INT | Time | Forward | Name | IAT
--------+--------+----------+--------+--------
0x3058 | 0x0 | 0x0 | 0x303C | 0x3068
0x3088 | 0x0 | 0x0 | 0x3048 | 0x3098
0x0000 | 0x0 | 0x0 | 0x0000 | 0x0000
Ahora agreguemos los nombres de las propias bibliotecas:
Nombres de bibliotecas
063 "user32.dll\0"
0648 "kernel32.dll\0"
(8) Nombres de biblioteca RAW (Offset 0x0000063C)
75 73 65 72
33 32 2E 64 6C 6C 00 00 6B 65 72 6E 65 6C 33 32
2E 64 6C 6C 00 00 00 00
A continuación, describamos la biblioteca user32:
(9) RAW user32.dll (compensación 0x00000658)
78 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 78 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 4D 65 73 73 61 67
65 42 6F 78 41 00 00 00
El campo Nombre de la primera biblioteca apunta a 0x303C si miramos un poco más arriba, veremos que en la dirección 0x063C hay una biblioteca "user32.dll \ 0".
Sugerencia, recuerde que la sección .idata corresponde al desplazamiento de archivo 0x0600 y al desplazamiento de memoria 0x3000. Para la primera biblioteca, INT es 3058, lo que significa que se desplazará 0x0658 en el archivo. En esta dirección, vemos la entrada 0x3078 y el segundo cero. Significa el final de la lista. 3078 se refiere a 0x0678 esta es la cadena RAW
"00 00 4D 65 73 73 61 67 65 42 6F 78 41 00 00 00"
Los primeros 2 bytes no nos interesan y son iguales a cero. Y luego hay una línea con el nombre de la función, que termina en cero. Es decir, podemos representarlo como "\ 0 \ 0MessageBoxA \ 0".
En este caso, IAT se refiere a una estructura similar a la tabla IAT, pero solo las direcciones de función se cargarán en ella cuando se inicie el programa. Por ejemplo, la primera entrada 0x3068 en la memoria tendrá un valor distinto de 0x0668 en el archivo. Allí estará la dirección de la función MessageBoxA cargada por el sistema a la que nos referiremos a través de la llamada call en el código del programa.
Y la última pieza del rompecabezas, el kernel32. Y no olvide agregar ceros a SectionAlignment.
(10) RAW kernel32.dll (Desplazamiento 0x00000688-0x00000800)
A8 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 A8 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 45 78 69 74 50 72
6F 63 65 73 73 00 00 00 00 00 00 00 00 00 00 00
........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Comprobamos que Far pudo determinar correctamente qué funciones importamos:
¡Genial! Todo estaba bien, así que ahora nuestro archivo está listo para ejecutarse.
Redoble de tambores ...
El final
¡Felicitaciones, lo logramos!
El archivo ocupa 2 KB = Encabezados 512 bytes + 3 secciones de 512 bytes.
El número 512 (0x200) no es más que el FileAlignment que especificamos en el encabezado de nuestro programa.
Además:
si desea profundizar un poco más, puede reemplazar la inscripción "¡Hola mundo!" a otra cosa, simplemente no olvide cambiar la dirección de la línea en el código del programa (sección .text). La dirección en la memoria es 0x00402000, pero el archivo tendrá el orden de bytes inverso 00 20 40 00.
O la búsqueda es un poco más complicada. Agregue otra llamada de MessageBox al código. Para hacer esto, deberá copiar la llamada anterior y volver a calcular la dirección relativa (0x3068 - RIP) en ella.
Conclusión
El artículo resultó bastante arrugado, por supuesto, constaría de 3 partes separadas: Encabezados, Programa, Tabla de importación.
Si alguien ha compilado su exe, entonces mi trabajo no fue en vano.
Estoy pensando en crear un archivo ELF de una manera similar pronto, ¿sería interesante un artículo de este tipo?)
Enlaces:
- Manuales para desarrolladores de software de arquitecturas Intel 64 e IA-32
Guía de comandos y arquitectura del procesador.
- PE (Portable Executable): On Stranger Tides
Excelente artículo sobre la estructura de archivos exe. - Repositorio de documentación de Microsoft
Aquí puede encontrar cualquier información sobre encabezados, estructuras, tipos y su descripción