Crear EXE

El autoaislamiento es un buen momento para comenzar algo que requiere mucho tiempo y esfuerzo. Así que decidí hacer lo que siempre quise: escribir mi propio compilador.



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










, , .



, (Offset) .



, 0x00000000, 64 (0x40 16- ), 0x00000040 ..

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.



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) — (, , ..)

.



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 — (, , , , .)



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)



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:






All Articles