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

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.
, , .
, (Offset) .
, 0x00000000, 64 (0x40 16- ), 0x00000040 ..
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) — (, , ..)
.
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 — (, , , , .)
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)
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