Criar EXE

O auto-isolamento é um ótimo momento para começar algo que exige muito tempo e esforço. Então decidi fazer o que sempre quis - escrever meu próprio compilador.



Agora ele é capaz de construir o Hello World, mas neste artigo não quero falar sobre a análise e a estrutura interna do compilador, mas sobre uma parte tão importante como a montagem byte a byte de um arquivo exe.



Começar



Quer um spoiler? Nosso programa terá 2048 bytes.



Normalmente, trabalhar com arquivos exe é estudar ou modificar sua estrutura. Os próprios arquivos executáveis ​​são formados pelos compiladores e esse processo parece um pouco mágico para os desenvolvedores.



Mas agora vamos tentar consertar!



Para construir nosso programa, precisamos de qualquer editor HEX (eu pessoalmente usei HxD).



Para começar, vamos pegar o pseudocódigo:



Fonte
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)
}




As primeiras duas linhas indicam funções importadas de bibliotecas WinAPI . A função MessageBoxA exibe uma caixa de diálogo com o nosso texto, e ExitProcess informa o sistema sobre o fim do programa.

Não faz sentido considerar a função principal separadamente, uma vez que ela usa as funções descritas acima.



Cabeçalho DOS



Primeiro, precisamos gerar o cabeçalho DOS correto, este é o cabeçalho dos programas DOS e não deve afetar a inicialização do exe no Windows.



Observei campos mais ou menos importantes, o resto está preenchido com zeros.



Estrutura 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
}




Mais importante, este cabeçalho contém o campo e_magic, o que significa que este é um arquivo executável, e e_lfanew, que indica o deslocamento do cabeçalho PE desde o início do arquivo (em nosso arquivo, este deslocamento é 0x80 = 128 bytes).



Ótimo, agora que conhecemos a estrutura do cabeçalho DOS, vamos escrever em nosso arquivo.



(1) Cabeçalho DOS RAW (deslocamento 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 ..

Pronto, os primeiros 64 bytes foram gravados. Agora você precisa adicionar mais 64, este é o chamado DOS Stub (Stub). Quando iniciado no DOS, deve notificar o usuário de que o programa não foi projetado para ser executado neste modo.



Mas, em geral, este é um pequeno programa DOS que imprime uma linha e sai do programa.

Vamos gravar nosso Stub em um arquivo e considerá-lo com mais detalhes.



(2) RAW DOS Stub (deslocamento 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






E agora o mesmo código, mas em forma desmontada



Asm DOS Stub
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 assim: primeiro, o stub imprime uma linha informando que o programa não pode ser iniciado e, em seguida, sai do programa com o código 1. Que é diferente do encerramento normal (Código 0).



O código stub pode ser ligeiramente diferente (de um compilador para outro). Eu comparei gcc e delphi, mas o significado geral é o mesmo.



Também é engraçado que a linha de stub termine com \ x0D \ x0D \ x0A $. Provavelmente, a razão para esse comportamento é que o c ++ abre o arquivo em modo de texto por padrão. Como resultado, o caractere \ x0A é substituído pela sequência \ x0D \ x0A. Como resultado, obtemos 3 bytes: 2 bytes de retorno de carro (0x0D), que não faz sentido, e 1 para alimentação de linha (0x0A). No modo binário (std :: ios :: binary), essa substituição não ocorre.



Para verificar se os valores estão escritos corretamente, usarei o Far com o plugin ImpEx:







Cabeçalho NT



Após 128 (0x80) bytes, chegamos ao cabeçalho NT (IMAGE_NT_HEADERS64), que também contém o cabeçalho PE (IMAGE_OPTIONAL_HEADER64). Apesar do nome IMAGE_OPTIONAL_HEADER64 é obrigatório, mas diferente para arquiteturas x64 e x86.



Estrutura 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
		}
	}
}




Vamos ver o que está armazenado nesta estrutura:



Descrição 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) — (, , ..)

.



Agora que vimos em que consiste o cabeçalho do NT, também o escreveremos em um arquivo por analogia com os outros em 0x80.



(3) RAW NT-Header (deslocamento 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, obtemos este tipo de cabeçalhos IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER64 e IMAGE_DATA_DIRECTORY: A















seguir, descrevemos todas as seções de nosso aplicativo de acordo com a estrutura IMAGE_SECTION_HEADER



Estrutura 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
}




Descrição de IMAGE_SECTION_HEADER
Name — 8 ,

VirtualSize —

VirtualAddress — SectionAlignment

SizeOfRawData — FileAlignment

PointerToRawData — FileAlignment

Characteristics — (, , , , .)



No nosso caso, teremos 3 seções.



Por que o Virtual Address (VA) começa do 1000, e não do zero, não sei, mas todos os compiladores que considerei fazem isso. Como resultado, 1000 + 3 seções * 1000 (SectionAlignment) = 4000 que escrevemos em SizeOfImage. Este é o tamanho total do nosso programa na memória virtual. Provavelmente usado para alocar espaço para um programa na memória.



 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


Decodificação dos atributos:



I - Dados inicializados,

U - Dados não inicializados,

C - Código , dados não inicializados , contém executável

E - Código de execução, permite a execução de

R - Ler código , permite leitura de dados da seção

W - Escrita, permite escrever dados para a seção



.text (.code) - armazena o código executável (o próprio programa), atributos CE

.rdata (.rodata) - armazena dados somente leitura, como constantes, strings, etc., atributos de IR

.data - armazena dados que podem ser lidos e gravados, como variáveis ​​estáticas ou globais. Atributos IRW

.bss - Armazena dados não inicializados, como variáveis ​​estáticas ou globais. Além disso, esta seção geralmente tem tamanho RAW zero e tamanho VA diferente de zero, portanto, não ocupa espaço no arquivo.

Atributos URW .idata - uma seção que contém funções importadas de outras bibliotecas. Atributos de IR



Um ponto importante, as seções devem seguir umas às outras. Além disso, tanto no arquivo quanto na memória. Pelo menos quando mudei sua ordem arbitrariamente, o programa parou de funcionar.



Agora que sabemos quais seções nosso programa conterá, iremos gravá-las em nosso arquivo. Aqui, o deslocamento termina em 8 e a gravação começará no meio do arquivo.



(4) Seções RAW (deslocamento 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






O próximo endereço de entrada será 00000200, que corresponde ao campo SizeOfHeaders do PE-Header. Se adicionarmos mais uma seção, e isso é mais 40 bytes, então nossos cabeçalhos não caberiam em 512 (0x200) bytes e teriam que usar 512 + 40 = 552 bytes alinhados por FileAlignment, ou seja, 1024 (0x400) bytes. E tudo o que resta de 0x228 (552) ao endereço 0x400 precisa ser preenchido com algo, melhor, é claro, com zeros.



Vamos dar uma olhada na aparência de um bloco de seções em Far: A







seguir, vamos escrever as próprias seções em nosso arquivo, mas há uma nuance.



Como você pode ver no exemplo SizeOfHeaders, não podemos simplesmente escrever o cabeçalho e seguir para a próxima seção. Pois, para registrar um título, precisamos saber quanto tempo todos os títulos levarão juntos. Como resultado, precisamos calcular antecipadamente quanto espaço é necessário ou escrever valores vazios (zero) e, depois de escrever todos os cabeçalhos, retornar e anotar seu tamanho real.



Portanto, os programas são compilados em várias etapas. Por exemplo, a seção .rdata vem depois da seção .text, embora não possamos descobrir o endereço virtual da variável em .rdata, porque se a seção .text crescer em mais de 0x1000 (SectionAlignment) bytes, ela ocupará os endereços 0x2000 do intervalo. E, consequentemente, a seção .rdata não estará mais localizada em 0x2000, mas em 0x3000. E precisaremos voltar e recalcular os endereços de todas as variáveis ​​na seção .text que vem antes de .rdata.



Mas, neste caso, já calculei tudo, então vamos escrever imediatamente os blocos de código.



.Seção de texto



Segmento Asm .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




Especificamente para este programa, as primeiras 3 linhas, exatamente como as últimas 3, são opcionais.

Os últimos 3 nem sequer serão executados, pois o programa sairá na segunda função de chamada.



Mas digamos assim, se não fosse a função principal, mas uma subfunção, deveria ser feito assim.



Mas os 3 primeiros neste caso, embora não sejam necessários, são desejáveis. Por exemplo, se não usamos MessageBoxA, mas printf, sem essas linhas receberíamos um erro.



De acordo com a convenção de chamada para sistemas MSDN de 64 bits, os primeiros 4 parâmetros são passados ​​nos registros RCX, RDX, R8, R9. Se eles cabem lá e não são, por exemplo, um número de ponto flutuante. E o resto é passado pela pilha.



Em teoria, se passarmos 2 argumentos para uma função, devemos passá-los por meio de registradores e reservar dois lugares na pilha para eles, de modo que, se necessário, a função possa empurrar os registradores para a pilha. Além disso, não devemos esperar que esses registros nos sejam devolvidos em seu estado original.



Portanto, o problema com a função printf é que, se passarmos apenas 1 argumento para ela, ela ainda sobrescreverá todos os 4 lugares na pilha, embora pareça ter que sobrescrever apenas um, pelo número de argumentos.



Portanto, se você não quiser que o programa se comporte de maneira estranha, sempre reserve pelo menos 8 bytes * 4 argumentos = 32 (0x20) bytes se você passar pelo menos 1 argumento para a função.



Considere um bloco de código com chamadas de função



MessageBoxA(0, 'Hello World!', 'MyApp', 64)
ExitProcess(0)


Primeiro, passamos nossos argumentos:



rcx = 0

rdx = o endereço absoluto da string na memória ImageBase + Seções [". Rdata"]. VirtualAddress + Offset da string desde o início da seção, a string é lida no byte zero

r8 = semelhante ao anterior

r9 = 64 (0x40) MB_ICONINFORMAÇÃO , ícone de informação



E depois há uma chamada à função MessageBoxA, com a qual nem tudo é tão simples. A questão é que os compiladores tentam usar os comandos mais curtos possíveis. Quanto menor o tamanho da instrução, mais essas instruções caberão no cache do processador, respectivamente, haverá menos perdas de cache, sobrecargas e maior será a velocidade do programa. Para obter mais informações sobre os comandos e o funcionamento interno do processador, consulte os manuais do desenvolvedor de software das arquiteturas Intel 64 e IA-32.



Poderíamos chamar a função no endereço completo, mas isso levaria pelo menos (1 opcode + 8 endereço = 9 bytes), e com um endereço relativo, o comando de chamada leva apenas 6 bytes.



Vamos dar uma olhada mais de perto nesta mágica: rip + 0x203E nada mais é do que uma chamada de função no endereço especificado por nosso deslocamento.



Olhei um pouco mais à frente e descobri os endereços dos offsets que precisamos. Para MessageBoxA é 0x3068 e para ExitProcess é 0x3098.



É hora de transformar magia em ciência. Cada vez que um opcode atinge o processador, ele calcula seu comprimento e o adiciona ao endereço de instrução atual (RIP). Portanto, quando usamos RIP dentro de uma instrução, este endereço indica o fim da instrução atual / o início da próxima.

Para a primeira chamada, o deslocamento indicará o fim do comando de chamada, este é 002A. Não se esqueça que na memória este endereço estará nas Seções de deslocamento [". Texto"]. VirtualAddress, ou seja, 0x1000. Portanto, o RIP para nossa chamada será 102A. O endereço de que precisamos para MessageBoxA é 0x3068. Considere 0x3068 - 0x102A = 0x203E . Para o segundo endereço, tudo é igual a 0x1000 + 0x0037 = 0x1037, 0x3098 - 0x1037 = 0x2061 .



São esses deslocamentos que vimos nos comandos do montador.



0024	call QWORD PTR [rip + 0x203E]
002A	mov rcx, 0x0
0031	call QWORD PTR [rip + 0x2061]
0037	add rsp, 0x20


Vamos escrever a seção .text em nosso arquivo, adicionando zeros ao endereço 0x400:



(5) Seção RAW .text (deslocamento 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 , ! .




Seção .Rdata



Esta é talvez a seção mais simples. Vamos apenas colocar duas linhas aqui, adicionando zeros a 512 bytes.



.rdata
0400	"Hello World!\0"
040D	"MyApp\0"




(6) Seção RAW .rdata (deslocamento 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




.Seção de dados



Bem, aqui está a última seção, que descreve funções importadas de bibliotecas.



A primeira coisa que nos espera é a nova estrutura IMAGE_IMPORT_DESCRIPTOR



Estrutura IMAGE_IMPORT_DESCRIPTOR
Struct IMAGE_IMPORT_DESCRIPTOR
{
	u32 OriginalFirstThunk (INT)
	u32 TimeDateStamp
	u32 ForwarderChain
	u32 Name
	u32 FirstThunk (IAT)
}




Descrição IMAGE_IMPORT_DESCRIPTOR
OriginalFirstThunk — , Import Name Table (INT)

Name — ,

FirstThunk — , Import Address Table (IAT)



Primeiro, precisamos adicionar 2 bibliotecas importadas. Lembre-se:



func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll']
func ExitProcess(u32 code) ['kernel32.dll']


(7) RAW IMAGE_IMPORT_DESCRIPTOR (Offset 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, e para dizer que terminamos de listá-las. A última estrutura é preenchida com zeros.



 INT	| Time	 | Forward  | Name   | IAT
--------+--------+----------+--------+--------
0x3058	| 0x0    | 0x0      | 0x303C | 0x3068
0x3088	| 0x0    | 0x0      | 0x3048 | 0x3098
0x0000	| 0x0    | 0x0      | 0x0000 | 0x0000


Agora vamos adicionar os nomes das próprias bibliotecas:



Nomes de bibliotecas
063	"user32.dll\0"
0648	"kernel32.dll\0"




(8) Nomes de biblioteca RAW (deslocamento 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 seguir, vamos descrever a biblioteca user32:



(9) RAW user32.dll (deslocamento 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




O campo Nome da primeira biblioteca aponta para 0x303C se olharmos um pouco acima, veremos que no endereço 0x063C há uma biblioteca "user32.dll \ 0".



Dica, lembre-se de que a seção .idata corresponde ao deslocamento de arquivo 0x0600 e deslocamento de memória 0x3000. Para a primeira biblioteca, INT é 3058, o que significa que no arquivo ele terá o deslocamento 0x0658. Neste endereço, vemos a entrada 0x3078 e o segundo zero. Significando o fim da lista. 3078 refere-se a 0x0678 esta é a string RAW



"00 00 4D 65 73 73 61 67 65 42 6F 78 41 00 00 00"



Os primeiros 2 bytes não nos interessam e são iguais a zero. E então há uma linha com o nome da função, terminando em zero. Ou seja, podemos representá-lo como "\ 0 \ 0MessageBoxA \ 0".



Nesse caso, o IAT se refere a uma estrutura semelhante à tabela IAT, mas apenas os endereços de função serão carregados nela quando o programa iniciar. Por exemplo, a primeira entrada 0x3068 na memória terá um valor diferente de 0x0668 no arquivo. Haverá o endereço da função MessageBoxA carregada pelo sistema ao qual faremos referência através da chamada no código do programa.



E a última peça do quebra-cabeça, o kernel32. E não se esqueça de adicionar zeros a SectionAlignment.



(10) RAW kernel32.dll (deslocamento 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






Verificamos se o Far foi capaz de identificar corretamente quais funções importamos:







Ótimo! Tudo estava bem, então agora nosso arquivo está pronto para ser executado.

Tambor ...



O final







Parabéns, conseguimos!



O arquivo ocupa 2 KB = Cabeçalhos 512 bytes + 3 seções de 512 bytes.



O número 512 (0x200) nada mais é do que o FileAlignment que especificamos no cabeçalho do nosso programa.



Além disso:

Se você quiser ir um pouco mais fundo, pode substituir a inscrição "Hello World!" para outra coisa, só não se esqueça de mudar o endereço da linha no código do programa (seção .text). O endereço na memória é 0x00402000, mas o arquivo conterá a ordem reversa dos bytes 00 20 40 00.



Ou a busca é um pouco mais complicada. Adicione outra chamada MessageBox ao código. Para fazer isso, você terá que copiar a chamada anterior e recalcular o endereço relativo nela (0x3068 - RIP).



Conclusão



O artigo acabou ficando bastante amarrotado, consistindo, é claro, em 3 partes distintas: Cabeçalhos, Programa, Tabela de Importação.



Se alguém compilou seu exe, meu trabalho não foi em vão.



Estou pensando em criar um arquivo ELF de maneira semelhante em breve, esse artigo seria interessante?)



Links:



  • Manuais do desenvolvedor de software das arquiteturas Intel 64 e IA-32

    Guia da arquitetura do comando e do processador.

  • PE (Portable Executable): On Stranger Tides

    Excelente artigo sobre estrutura de arquivos exe.
  • Repositório de Documentação da Microsoft

    Aqui você pode encontrar qualquer informação sobre cabeçalhos, estruturas, tipos e suas descrições



All Articles