Entendendo a estrutura PE do Windows (Parte 1)
Aviso: este post faz parte de uma série sobre IAT Hooking com fins puramente educacionais. As técnicas aqui descritas são as mesmas usadas por malware, mas também por ferramentas de segurança defensiva, EDRs, debuggers, ferramentas de análise e cheats de jogos antigos. Conhecimento técnico não é crime uso pra atacar sistemas que não são seus, é. Não faça isso.
Tô estudando IAT Hooking acompanhando um curso no YouTube, e decidi transformar minhas anotações em posts pra fixar o conteúdo e ajudar quem tiver começando agora. Essa é a parte 1, onde vou mapear toda a estrutura de um executável PE do Windows base teórica obrigatória pra qualquer coisa de hook, injeção, reverse engineering ou análise de binários.
Na parte 2 a gente vai pra prática: construir uma DLL que faz hook de uma função da Win32 API usando essa estrutura toda que vamos ver agora.
O que é PE?
PE (Portable Executable) é o formato dos arquivos .exe, .dll, .sys e similares no Windows. É o equivalente ao ELF no Linux ou Mach-O no macOS. Esse formato define como o binário tá organizado em disco e como ele é mapeado pra memória quando o Windows carrega o programa.
Pra fazer hook de função importada (IAT hooking), a gente precisa navegar nessa estrutura na memória, achar a tabela de imports, e patchear o ponteiro da função que queremos interceptar. Pra navegar, precisamos conhecer as structs que descrevem o layout.
Onde estão definidas
Tudo que vou mostrar abaixo vive em winnt.h, que vem com o Windows SDK. Se você incluir <windows.h> no seu código, todas essas structs já estão disponíveis automaticamente.
Pra fins de estudo, vou copiar as definições aqui (igual fiz nas minhas anotações originais) num arquivo struct.h separado mas no projeto real basta o #include <windows.h>.
1. IMAGE_DOS_HEADER o cabeçalho mais antigo
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier
WORD e_oeminfo; // OEM information
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Esse cabeçalho é uma herança do MS-DOS. Quando você executa um .exe moderno num MS-DOS antigo, ele simplesmente imprime “This program cannot be run in DOS mode” esse stub DOS vive aqui.
Pra nós, dois campos importam:
e_magicvale0x5A4D(em ASCII,"MZ", iniciais de Mark Zbikowski, engenheiro da Microsoft que projetou o formato). Tem que ser igual aIMAGE_DOS_SIGNATUREpra você ter certeza que tá lidando com um PE válido.e_lfanewo offset (a partir do início do arquivo) onde começa o cabeçalho NT, o que realmente importa pra gente. É a “ponte” entre o mundo DOS antigo e o PE moderno.
2. IMAGE_NT_HEADERS o cabeçalho NT (PE de verdade)
Existem duas versões dessa struct, uma pra 32 bits e outra pra 64 bits:
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
O windows.h automaticamente define IMAGE_NT_HEADERS como uma das duas dependendo da arquitetura que você tá compilando.
Campos importantes:
Signaturevale0x00004550(em ASCII,"PE\0\0"). Tem que bater comIMAGE_NT_SIGNATURE. Segunda verificação que fazemos pra garantir que é um PE válido.FileHeadermetadados gerais (arquitetura, número de seções, timestamp, etc).OptionalHeaderapesar do nome, não é opcional em executáveis. É onde mora a informação crítica.
3. IMAGE_FILE_HEADER metadados do arquivo
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // Arquitetura (x86, x64, ARM...)
WORD NumberOfSections; // Quantas seções tem (.text, .data, etc)
DWORD TimeDateStamp; // Timestamp de compilação
DWORD PointerToSymbolTable; // Tabela de símbolos (debug)
DWORD NumberOfSymbols; // Quantidade de símbolos
WORD SizeOfOptionalHeader; // Tamanho do OptionalHeader
WORD Characteristics; // Flags (é DLL? Executável? etc)
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Aqui tem coisa útil pra análise forense o TimeDateStamp, por exemplo, fala quando o binário foi compilado (alguns malwares mexem nesse campo pra esconder origens). Characteristics tem flags tipo IMAGE_FILE_DLL que indicam se o binário é uma DLL ou um EXE.
Pro IAT hooking, esse cabeçalho não é o foco a gente só atravessa ele pra chegar no próximo.
4. IMAGE_OPTIONAL_HEADER o coração da estrutura PE
typedef struct _IMAGE_OPTIONAL_HEADER {
// Standard fields
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
// NT additional fields
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Struct gigante, eu sei. Vou destacar só o que importa pra nossa missão:
AddressOfEntryPointRVA do primeiro código a ser executado quando o binário roda (no caso de DLL, é aDllMain).ImageBaseendereço base “preferido” onde o binário quer ser carregado em memória (com ASLR ativo, o Windows pode escolher outro).SizeOfImagequantos bytes o binário ocupa em memória depois de carregado.DataDirectory[]a parte mais importante pra nós. Array de 16 ponteiros pra “diretórios” dentro do PE.
O que é RVA?
RVA = Relative Virtual Address = endereço virtual relativo.
A ideia é: o PE não pode hardcodar endereços absolutos, porque o Windows pode carregar ele em endereços diferentes a cada execução (graças ao ASLR). Então o formato armazena offsets relativos ao endereço base (ImageBase).
Pra converter RVA em endereço absoluto na memória:
endereço_absoluto = endereço_base_do_módulo + RVA
Exemplo: se o módulo foi carregado em 0x00007FF600000000 e a IAT está no RVA 0x00026910, então o endereço real da IAT na memória é 0x00007FF600026910.
Esse cálculo é a operação mais comum quando você navega num PE em memória. Você vai ver pTarget + algumaCoisa.VirtualAddress várias vezes no código da parte 2 é exatamente isso.
O que é o DataDirectory?
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Cada entrada do DataDirectory[] tem dois campos: o RVA onde aquele diretório começa, e o tamanho dele em bytes. Os 16 diretórios são indexados por constantes tipo:
IMAGE_DIRECTORY_ENTRY_EXPORTtabela de funções que o módulo exporta (usada peloGetProcAddress).IMAGE_DIRECTORY_ENTRY_IMPORTtabela de funções que o módulo importa de outros módulos. É essa que vamos hookear.IMAGE_DIRECTORY_ENTRY_RESOURCErecursos embutidos (ícones, strings, etc).IMAGE_DIRECTORY_ENTRY_BASERELOCinformação pra ASLR conseguir remapear o módulo em outro endereço.- … e mais alguns.
Pro IAT hooking, o que nos interessa é o índice IMAGE_DIRECTORY_ENTRY_IMPORT.
5. Visualizando tudo isso na prática PE-bear
Antes de cair no código, recomendo muito instalar o PE-bear e abrir um .exe qualquer (no curso usei o C:\Windows\System32\notepad.exe).
Você consegue ver visualmente tudo que descrevi acima os cabeçalhos, as seções, e principalmente a tabela de imports.
Na aba Imports, você vê todas as DLLs das quais o programa depende (KERNEL32.dll, USER32.dll, etc), e dentro de cada uma, todas as funções que ele importa daquela DLL. Pra cada função, tem:
- O nome (
CreateFileW,MessageBoxW…) - O RVA da entrada na IAT esse é exatamente o ponteiro que vamos sobrescrever no hook.

Logo abaixo da lista de DLLs tem uma outra parte bem interessante: as funções que cada DLL exporta pro programa, com seus respectivos offsets. Selecionando KERNEL32.dll, dá pra ver todas as funções que o notepad importa de lá GetProcAddress, CreateFileW, ReadFile, etc com a coluna Call via mostrando o offset de cada uma.

Como pode ver na imagem acima, o KERNEL32.dll tem o endereço virtual call via 26910 pra chamar a função CreateFileW. Isso é o que nos ajuda a acessar essa função enquanto ela está mapeada na memória.
Tem também o conceito de thunk que aparece no PE-bear: é o slot na memória que o Windows preenche com o endereço real da função durante o carregamento. Quando o programa chama CreateFileW, ele basicamente faz algo como call [endereço_do_thunk], e o thunk aponta pra função real na KERNEL32.dll.
A ideia do IAT hooking é simples: sobrescrever o thunk com o endereço da nossa função. Quando o programa pensa que tá chamando CreateFileW, na real ele chama a nossa.
6. IMAGE_SECTION_HEADER as seções do binário
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Logo depois do IMAGE_NT_HEADERS vem um array de IMAGE_SECTION_HEADER um pra cada seção do binário. Seções típicas:
.textcódigo executável.datadados inicializados (variáveis globais com valor).rdatadados read-only (constantes, tabela de imports/exports).bssdados não inicializados.rsrcrecursos
Cada seção tem suas próprias permissões (leitura/escrita/execução) definidas em Characteristics. É por isso que a gente precisa do VirtualProtect no hook: a página onde mora a IAT geralmente é read-only, e precisamos liberar escrita temporariamente pra patchear.
Pra o nosso hook, não vamos manipular seções diretamente mas é importante entender que elas existem e que cada uma tem suas regras de proteção.
Resumindo o mapa mental
Quando o Windows carrega um PE na memória, o layout fica mais ou menos assim:
A navegação pra chegar na tabela de imports é sempre essa:
- Lê o
IMAGE_DOS_HEADERno início do módulo - Soma
e_lfanewao base pra chegar noIMAGE_NT_HEADERS - Pega
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] - Soma o
VirtualAddressdesse diretório ao base pra chegar na IAT
Esse é exatamente o passo a passo que vamos implementar em código na parte 2.
Próximo passo
Na parte 2 vamos colocar tudo isso em prática:
- Criar uma DLL
- Implementar a função
HookIATque navega na estrutura PE - Patchear o ponteiro de uma função (
MessageBoxW) com a nossa - Carregar essa DLL num executável e ver o hook acontecendo ao vivo
Até lá, recomendo brincar com o PE-bear, abrir alguns .exe e identificar visualmente cada um dos cabeçalhos que vimos aqui. Ajuda muito a fixar.
Esse post faz parte da série, minhas anotações de estudo sobre IAT Hooking. Se achou algum erro ou tem sugestão, manda aí.