arrow-left

All pages
gitbookPowered by GitBook
1 of 10

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Opcional

A julgar pelo seu nome, pode não parecer, mas este cabeçalho é muito importante. Ele é opcional para arquivos objeto, mas é obrigatório em arquivos executáveis, que são o nosso foco de estudo. Ao contrário dos outros cabeçalhos que vimos até agora, o tamanho deste cabeçalho não é fixo, mas sim definido pelo campo SizeOfOptionalHeader do cabeçalho COFF, que vimos anteriormente. Sua estrutura para arquivos PE de 64-bits, também chamados de PE32+ (ou de PE64 de forma não oficial), é a seguinte:

Vamos analisar agora os campos mais importantes para o nosso estudo:

hashtag
Magic

O primeiro campo, de 2 bytes, é um outro número mágico que identifica o tipo de executável em questão. O valor 0x20b diz que é um PE32+ (executável PE de 64-bits), enquanto o valor 0x10b significa que o executável é um PE32 (executável PE de 32-bits).

A Microsoft chama os executáveis de PE de 64-bits de PE32+ e não de PE64 como alguns programas fazem. Já os de 32-bits são chamados de PE32 mesmo.

hashtag
AddressOfEntryPoint

Este é talvez o campo mais importante do cabeçalho opcional. Nele está contido o endereço do ponto de entrada (entrypoint), abreviado EP, que é onde o código do programa deve começar. Para arquivos executáveis, este endereço é relativo à base da imagem (campo que veremos a seguir). Para bibliotecas, ele não é necessário e pode ser zero, já que as funções de bilbioteca podem ser chamadas arbitrariamente.

hashtag
ImageBase

Imagem é como a Microsoft chama um arquivo executável (para diferenciar de um código-objeto) quando vai para a memória. Neste campo está o endereço de memória que é a base da imagem, ou seja, o endereço onde o programa será carregado em memória. É comum encontrar valores como 0x140000000 ou 0x400000 neste campo.

hashtag
SubSystem

Este campo define o tipo de subsistema necessário para rodar o programa. Valores interessantes para nós são:

  • 0x002 - Windows GUI (Graphical User Interface) - para programas gráficos no Windows (que usam janelas, etc).

  • 0x003 - Windows CUI (Character User Interface) - para programas de linha de comando.

hashtag
DllCharacteristics

Ao contrário do que o nome possa sugerir, este campo não é somente para DLLs. Ele está presente e é utilizado para arquivos executáveis também. Assim como o campo Characteristics do cabeçalho COFF visto anteriormente, este campo é uma máscara de bits com destaque para os possíveis valores:

Bit
Nome

O estado bit 5 nos diz se a randomização de endereços de memória, também conhecida por ASLR (Address Space Layout Randomization), está ativada para este binário, enquanto o estado do bit 7 diz respeito à DEP (Data Execution Prevention), também conhecido pela sigla NX (No eXecute). O estudo aprofundado destes recursos foge do escopo deste livro, mas é importante que saibamos que podemos desabilitar tais recursos para um binário específico simplesmente desligando estes bits se quisermos.

O último campo importante para nós é o DataDirectory, que veremos a seguir.

#define MAX_DIRECTORIES 16

typedef struct {
	uint16_t Magic;
	uint8_t MajorLinkerVersion;
	uint8_t MinorLinkerVersion;
	uint32_t SizeOfCode;
	uint32_t SizeOfInitializedData;
	uint32_t SizeOfUninitializedData;
	uint32_t AddressOfEntryPoint;
	uint32_t BaseOfCode;
	uint64_t ImageBase;
	uint32_t SectionAlignment;
	uint32_t FileAlignment;
	uint16_t MajorOperatingSystemVersion;
	uint16_t MinorOperatingSystemVersion;
	uint16_t MajorImageVersion;
	uint16_t MinorImageVersion;
	uint16_t MajorSubsystemVersion;
	uint16_t MinorSubsystemVersion;
	uint32_t Win32VersionValue;
	uint32_t SizeOfImage;
	uint32_t SizeOfHeaders;
	uint32_t CheckSum;
	uint16_t Subsystem;
	uint16_t DllCharacteristics;
	uint64_t SizeOfStackReserve;
	uint64_t SizeOfStackCommit;
	uint64_t SizeOfHeapReserve;
	uint64_t SizeOfHeapCommit;
	uint32_t LoaderFlags;
	uint32_t NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[MAX_DIRECTORIES];
} IMAGE_OPTIONAL_HEADER_64;

5

IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE

7

IMAGE_DLLCHARACTERISTICS_NX_COMPAT

Diretórios de Dados

No final do cabeçalho opcional, mas ainda como parte dele, temos os diretórios de dados, ou Data Directories. São 16 diretórios ao todo, mas um executável pode conter somente alguns. Nos concentraremos, nos mais importantes para este estudo inicial. A estrutura de cada diretório de dados é conhecida por IMAGE_DATA_DIRECTORY e tem a seguinte definição:

Vejamos alguns diretórios importantes neste momento:

hashtag
Export Table

O primeiro diretório de dados aponta para a tabela de exports, ou seja, de funções exportadas pela aplicação. Por este motivo, a presença deste diretório (campos VirtualAddress e Size com valores diferentes de zero) é muito comum em bibliotecas.

O campo VirtualAddress aponta para uma outra estrutura chamada EDT (Export Directory Table), que contém os nomes das funções exportadas e seus endereços, além de um ponteiro para uma outra estrutura, preenchida em memória, chamada de EAT (Export Address Table).

hashtag
Import Table

Sendo a contraparte do Export Table, o diretório Import Table aponta para uma tabela de imports, ou seja, de funções importadas pela aplicação. Tal tabela é chamada de IDT (Import Directory Table). Nós a estudaremos em mais à frente.

hashtag
Resource Table

Aponta para uma estrutura de árvore binária que armazena todos os resources num executável como ícones, janelas e strings, principalmente quando o programa suporta vários idiomas.

hashtag
Certificate Table

Antigamente chamado de "Security", este diretório contém o endereço da Certificate Table, que pode conter um certificado para binários assinados digitalmente.

hashtag
IAT

Este diretório aponta para a Import Address Table, que veremos em breve.

hashtag
CLR Runtime Header

Para binários criados em .NET, há um outro cabeçalho específico que é apontado por este diretório.

A ordem na qual estes diretórios aparecem no arquivo PE é especificada na documentação do formato. Vamos agora para o nosso último cabeçalho, o das Seções.

typedef struct _IMAGE_DATA_DIRECTORY {
    uint32_t   VirtualAddress;
    uint32_t   Size;
} IMAGE_DATA_DIRECTORY;

COFF

Common Object File Format Specification

Imediatamente após a assinatura PE temos o cabeçalho COFF (Common Object File Format Specification) às vezes chamado simplesmente de Cabeçalho do Arquivo (File Header ) ou mesmo Cabeçalho do Arquivo PE (PE File Header). Trata-se de um cabeçalho especificado antes mesmo do formato PE para o sistema operacional VAX/VMS (depois chamado de OpenVMS) da DEC (empresa comprada pela Compaq, que por sua vez, foi comprada pela HP) na década de 70. A razão pela qual a Microsoft teria aproveitado o formato COFF é que boa parte dos engenheiros do time que desenvolveu o Windows NT trabalhavam para a DEC antes.

O cabeçalho COFF possui apenas 20 bytes e é representado pela seguinte estrutura:

Vamos à definição dos campos importantes para nós:

hashtag
Machine

Campo de 2 bytes que define a arquitetura da máquina para qual o programa foi construído. Valores comuns incluem 0x8664 para executáveis x86-64 de 64-bits, 0x14c para executáveis x86 de 32-bits e 0xaa64 para executáveis ARM de 64-bits. A tabela completa está disponível na documentação oficial do formato.

hashtag
NumberOfSections

Também de 2 bytes, o valor deste campo é o número de seções que o arquivo PE em questão possui. As seções serão estudadas mais a frente.

hashtag
TimeDateStamp

Este é um número de 32 bits que define o número de segundos desde à meia-noite do dia 1 de Janeiro de 1970, conhecido também por Epoch time. Com este valor é possível saber quando o arquivo foi criado.

Vale dizer que este campo não é utilizado pelo loader de arquivos PE no Windows e seu valor pode ser alterado pelo compilador ou após a compilação, logo, não é 100% confiável, ou seja, você não pode garantir que um binário PE foi compilado na data e hora informadas pelo valor neste campo.

hashtag
SizeOfOptionalHeader

Contém o tamanho do próximo cabeçalho, conhecido como Cabeçalho Opcional, que estudaremos muito em breve.

hashtag
Characteristics

Campo que define alguns atributos do arquivo. Este campo é uma máscara de bits, ou seja, cada bit desses 2 bytes diz respeito à uma característica específica do binário. Não cabe aqui explicar todos os possíveis valores, mas os mais comuns são:

Bit
Nome
Comentários

Vamos analisar novamente o executável da calculadora. Considere que:

  • Logo após a assinatura PE na posição 0x100, temos o primeiro campo do cabeçalho COFF que é o Machine. Ele é um campo de 2 bytes conforme já dito, então os bytes 0x64 e 0x86 definem seu valor. Considerando o endianness, chegamos ao valor 0x8664, que define que este executável foi criado para rodar em computadores com processadores de arquitetura x86-64.

  • Em seguida, na posição 0x106, temos o NumberOfSections que é 7.

  • Depois vem o campo TimeDateStamp

Em algumas referências o leitor encontrará o cabeçalho COFF como parte do cabeçalho NT (IMAGE_NT_HEADER), onde o primeiro campo é chamado de Signature Bytes, que é onde fica a assinatura PE para binários PE, mas também pode conter os bytes equivalentes das strings NE, LE ou MZ (executáveis puros de MS-DOS). Na verdade o COFF é uma especificação completa para arquivos do tipo "código-objeto", mas não exploraremos seu uso além do formato PE neste livro.

hashtag
Exercício

Abra o Developer Command Prompt for VS 2022 (ele é instalado junto ao Visual Studio Community) e exiba o COFF/File Header do binário da calculadora do Windows:

Você deve ver uma saída parecida com essa:

O que o dumpbin e outros analisadores de PE fazem é interpretar toda a estrutura do arquivo com base na documentação do formato PE e produzir uma saída mais legível.

Com o Detect It Easy, também é possível ver os cabeçalhos de um PE. Para ver o cabeçalho COFF, abra o executável no DIE e marque a caixa de seleção Advanced. Clique no botão PE e no menu à esquerda escolha IMAGE_FILE_HEADER. Você deve ver uma janela como esta:

Aproveite e nague pelo programa para descobrir mais detalhes sobre o arquivo antes de irmos para o Cabeçalho Opcional.

MS-DOS

Não estranhe o nome deste cabeçalho. Ele é parte do que chamamos de stub do MS-DOS: um executável completo de MS-DOS presente no início de todo executável PE para fins de retrocompatibilidade.

Sendo assim, todo executável PE começa com este cabeçalho que é definido pela seguinte estrutura:

Este cabeçalho possui 64 bytes de tamanho. Para chegar a esta conclusão basta somar o tamanho de cada campo, onde uint16_t é um tipo na linguagem C que define uma variável de 16 bits ou 2 bytes. Os seguintes campos variam deste tamanho:

Cabeçalhos

Cabeçalhos, como o próprio nome sugere, são áreas de dados no início de um arquivo. Eles contém diferentes campos, que admitem valores.

Cada campo possui um tipo que define seu tamanho. Por exemplo, se dissermos que o primeiro campo de um primeiro cabeçalho é do tipo WORD, estamos afirmando que este tem 2 bytes de tamanho, conforme a tabela a seguir:

typedef struct {
    uint16_t Machine;
    uint16_t NumberOfSections;
    uint32_t TimeDateStamp;
    uint32_t PointerToSymbolTable;
    uint32_t NumberOfSymbols;
    uint16_t SizeOfOptionalHeader;
    uint16_t Characteristics;
} IMAGE_FILE_HEADER, IMAGE_COFF_HEADER;

2

DWORD

unsigned long

4

QWORD

__int64

8

Há também os campos que possuem o que chamamos de máscara de bits. Neste campos, cada bit de seus bytes podem significar alguma coisa se estiverem ligados. Um bom exemplo é o campo "Characteristics" do cabeçalho de seções do arquivo PE, que veremos oportunamente.

Nomenclatura Microsoft

Nome do tipo em C (Microsoft Visual Studio)

Tamanho em bytes

BYTE

unsigned char

1

WORD

unsigned short

com o número inteiro de 32
bits
(4
bytes
) sem sinal 0xee8136fb que é 4001445627 em decimal.
  • Pulamos então 8 bytes referentes aos campos PointerToSymbolTable e NumberOfSymbols (normalmente zerados mesmo), encontrando a word SizeOfOptionalHeader em 0x114 cujo valor é 0xf0.

  • A próxima word é o valor do campo Characteristics, que neste arquivo é 0x22. Convertendo para binário temos o valor 00100010. Contando-se a partir de zero e da direita para a esquerda, identificamos que os bits 1 e 5 estão ligados, significando que o executável pode endereçar mais de 2 GB de memória, o que é comum em executáveis de 64-bits.

  • 1

    IMAGE_FILE_EXECUTABLE_IMAGE

    Obrigatório para arquivos executáveis

    5

    IMAGE_FILE_LARGE_ADDRESS_AWARE

    Pode endereçar mais de 2 GB de memória

    8

    IMAGE_FILE_32BIT_MACHINE

    O arquivo é de 32-bits

    13

    IMAGE_FILE_DLL

    Cabeçalho COFF exibido pelo DIE

    O arquivo é uma DLL

    dumpbin /nologo /headers c:\windows\system32\calc.exe
    Dump of file c:\windows\system32\calc.exe
    
    PE signature found
    
    File Type: EXECUTABLE IMAGE
    
    FILE HEADER VALUES
                8664 machine (x64)
                   7 number of sections
            EE8136FB time date stamp
                   0 file pointer to symbol table
                   0 number of symbols
                  F0 size of optional header
                  22 characteristics
                       Executable
                       Application can handle large (>2GB) addresses
    --suprimido--
    uint16_t e_res[4] que é um array de 4 campos de 16 bits, totalizando em 64 bits ou 8 bytes.
  • uint16_t e_res2[10] que é um array de 10 campos de 16 bits, totalizando em 160 bits ou 20 bytes.

  • uint32_t e_lfanew que é um campo de 32 bits ou 4 bytes.

  • Os outros 16 campos possuem o tamanho de um uint16_t (16 bits ou 2 bytes). Então somando os tamanhos de todos os campos, temos 64 bytes.

    Por ser um cabeçalho ainda presente no formato PE somente por questões de compatibilidade com o MS-DOS, não entraremos em muitos detalhes, mas estudaremos alguns de seus campos a seguir.

    hashtag
    e_magic

    Este campo de 2 bytes sempre contém os valores 0x4d e 0x5a, que são os caracteres 'M' e 'Z' na tabela ASCII. Portanto é comum verificar que todo arquivo executável do Windows que segue o formato PE começa com tais valores, que representam as iniciais de Mark Zbikowski, um dos criadores deste formato para o MS-DOS.

    Podemos utilizar um editor hexadecimal como o HxD para verificar tal informação. Vamos abrir, em caráter de exemplo, o executável da calculadora no Windows, normalmente em C:\Windows\System32\calc.exe.

    Conteúdo do binário da calculadora do Windows no HxD

    Perceba os bytes 0x4d e 0x5a logo no início do arquivo.

    O HxD exibe um caractere de ponto (.) na coluna Decoded text quando o byte não está na faixa ASCII imprimível. Esta é uma decisão de quem programou o editor hexadecimal. Outras opções comum incluem exibir um caractere de espaço.

    hashtag
    e_lfanew

    O próximo campo importante para nós é o e_lfanew, um campo de 4 bytes cujo valor é a posição no arquivo do que é conhecido por assinatura PE, uma sequência fixa dos seguintes 4 bytes: 50 45 00 00.

    Como o cabeçalho do DOS possui um tamanho fixo, seus campos estão sempre numa posição fixa no arquivo. No entanto, seus valores podem variar de arquivo para arquivo. No caso do e_lfanew, se fizermos as contas, veremos que ele sempre está na posição 0x3c (ou 60 em decimal), já que ele é o último campo de 4 bytes de um cabeçalho de 64 bytes.

    Para ver o valor deste campo rapidamente podemos pedir ao HxD que vá para a posição 0x3c. Clique em Search -> Go to... ou aperte Ctrl+G. Certifique-se de que as opções "hex" e "begin" estão selecionadas e clique em OK.

    No meu arquivo, assim como na imagem anterior, os quatro bytes nesta posição são 00 01 00 00. Sabendo que números são armazenados em little-endian, devemos ler este número como 00 00 01 00, ou seja, 0x0000100 ou simplesmente 0x100. Este é então o endereço da assinatura PE, que consiste numa sequência dos seguintes 4 bytes: 0x50 0x45 0x00 0x00.

    Perceba que os dois primeiros bytes na assinatura PE possuem representação ASCII justamente das letras 'P' e 'E' maiúsculas. Sendo assim, essa assinatura pode ser escrita como "PE\0\0", no estilo C string.

    Logo após o cabeçalho do DOS, há o código do programa que vai imprimir na tela uma mensagem de erro caso um usuário tente rodar este arquivo PE no MS-DOS. Normalmente o texto impresso na tela é:

    Depois disso o programa sai. Mas este pequeno programa de MS-DOS é adicionado pelo compilador (pelo linker mais especificamente) e seu conteúdo pode variar, pois não há um padrão rígido a ser seguido.

    hashtag
    Exercício

    Para por em prática a análise desta primeira parte do arquivo PE, abra o executável da calculadora do Windows (normalmente em C:\Windows\System32\calc.exe) no HxD.

    Note que:

    • Logo no início do arquivo, há o número mágico "MZ".

    • Na posição 0x3c, ou seja, no campo e_lfanew, há o endereço da assinatura PE (0x100 no caso deste executável, mas pode ser diferente no seu ambiente).

    • Logo após os 4 bytes do campo e_lfanew, começa o código do stub do DOS, sempre no offset 0x40, com uma sequência de bytes que não fazem sentido para nós por enquanto (veja que o texto impresso na tela pelo programa é todavia bem visível).

    • Finalmente, na posição 0x100 encontra-se a assinatura PE\0\0. Aqui sim, começa o formato PE propriamente dito.

    Cabeçalhos das Seções

    Após o cabeçalho opcional, encontramos os cabeçalhos das seções (estas serão explicadas no próximo capítulo). Neste cabeçalho há um array de estruturas como a seguir:

    Cada estrutura define uma seção no executável e a quantidade de estrutura (quantidade de elementos neste array) é igual ao número de seções no executável, definido no campo NumberOfSections do cabeçalho COFF. Vamos aos campos importantes:

    hashtag
    Name

    Este campo define o nome da seção. Como é um array de 8 elementos do tipo uint8_t, este nome está limitado a 8 caracteres. A string .text por exemplo ocupa apenas 5 bytes, então os outros 3 devem estar zerados. A codificação usada é a UTF-8.

    hashtag
    VirtualSize

    O tamanho em bytes da seção depois de ser mapeada (carregada) em memória pelo loader. Se este valor for maior que o valor do campo SizeOfRawData, os bytes restantes são preenchidos com zeros.

    hashtag
    VirtualAddress

    O endereço relativo à base da imagem (campo ImageBase do cabeçalho Opcional) quando a seção é carregada em memória. Por exemplo, se para uma seção este valor é 0x1000 e o valor de ImageBase é 0x400000, quando carregada em memória esta seção estará no endereço 0x401000. Para chegar nesta conclusão basta somar os dois valores.

    hashtag
    SizeOfRawData

    Tamanho em bytes da seção no arquivo PE, ou seja, antes de ser mapeada em memória. Alguns autores também usam a expressão "tamanho em disco" ou simplesmente "tamanho da seção".

    hashtag
    PointerToRawData

    O offset em disco da seção no arquivo. É correto dizer que aponta para o primeiro byte da seção. Por exemplo, se para dada seção este valor é 0x1000, para ver seu conteúdo no HxD basta ir até o offset 0x1000 com Ctrl+G.

    hashtag
    Characteristics

    Este é um campo que define algumas flags (características) para a seção, além das permissões em memória que ela deve ter quando for mapeada pelo loader. Ele possui 32-bits, onde alguns significam conforme a tabela a seguir:

    Bit
    Nome da flag
    Descrição

    As flags que contém o texto "MEM" no nome dizem respeito às permissões que a seção terá quando mapeada em memória. De acordo com elas o SO vai setar as permissões nas páginas de memória nas quais a seção é carregada.

    É importante notar que campos como o Characteristics são o que chamamos de máscaras de bits. Por exemplo, a tabela anterior diz que se o bit 30 deste campo está setado (seu valor é 1), então esta seção terá permissão de leitura quando em memória. O valor de campo Characteristics seria então 01000000000000000000000000000000 em binário, mas você provavelmente vai encontrar este valor representado em hexadecimal (0x40000000) nos analisadores de executáveis que for utilizar. Vamos agora conhecer as seções que estes cabeçalhos definem.

    Import Table

    Foi visto que no diretório de dados Import Table há um ponteiro para a IDT (Import Directory Table), apontado pelo valor do campo VirtualAddress. Vamos conhecer essa IDT de uma vez.

    hashtag
    Import Directory Table

    A IDT é um array de estruturas do tipo IMAGE_IMPORT_DESCRIPTOR definidas a seguir:

    Não se deve confundir esta IDT (Import Descriptor Table) com outra IDT (Interrupt Descriptor Table). Esta última é uma estrutura que mapeia interrupções para seus devidos handlers, assunto que foge do escopo deste livro.

    O número de elementos do array de estruturas IMAGE_IMPORT_DESCRIPTOR é igual ao número de bibliotecas que o executável PE depende, ou seja, o número de diferentes DLLs das quais o executável importa funções. Há ainda um elemento final, completamente zerado (preenchido com null bytes) para indicar o fim do array.

    hashtag
    RvaImportLookupTable

    O campo RvaImportLookupTable (antigamente conhecido por OriginalFirstThunk que era unido com Characteristics) aponta para uma tabela chamada de Import Lookup Table (ILT)

    hashtag
    Import Lookup Table (ILT)

    Essa tabela é um array de números de 64-bits (ou 32-bits para PE32). Para cada um desses números, seu bit mais significativo (MSB - Most Significant Bit), se ligado, define que a função será importada por número. Se desligado, a função é importada por nome. Os bits remanescentes guardam um endereço para uma estrutura que finalmente contém o nome da função. Essa estrutura é chamada de Hint/Name Table.

    Hint/Name Table

    Os elementos desta tabela obedecem a seguinte estrutura:

    Onde Name possui tamanho variável pois contém o nome da função a ser importada.

    Note que o número de entradas na ILT (número de elementos deste array) é igual ao número de funções importadas por uma DLL em particular definida na IDT.

    hashtag
    Name

    Este campo contém o endereço de uma string que é o nome da DLL importada. Por exemplo: SHELL32.dll. A string é terminada em null.

    hashtag
    RvaImportAddressTable

    Este campo aponta finalmente para o que chamamos de IAT (Import Address Table), muito conhecida por quem faz engenharia reversa. Essa tabela é em princípio idêntica à Import Lookup Table, mas no processo de carregamento do executável (load time, que estudaremos mais à frente no livro), ela é preenchida com os endereços reais das funções importadas. Isto porque um executável dinamicamente linkado não sabe, antes de ser carregado em memória, qual o endereço de cada função de cada DLL que ele precisa chamar.

    É importante lembrar o conceito de biblioteca compartilhada aqui. A ideia é ter apenas uma cópia dela carregada em memória e todos os programas que a utilizam podem chamar suas funções. Esta é a razão para o longo esquema de preenchimento da IAT pelo loader.

    hashtag
    Exercícios

    Para fixar este conteúdo, é importante validar o que foi aprendido. Abra o executável da calculadora no DIE, marque a caixa Advanced se já não estiver marcada e clique no botão PE como a imagem a seguir mostra:

    No menu da esquerda, vá em IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> IMAGE_DIRECTORY_ENTRIES e copie o endereço (coluna Address) do segundo diretório, que é justamente o endereço da IDT, como a imagem a seguir ilustra:

    Perceba que o DIE chama o campo VirtualAddress dos diretórios apenas de Address. Inconsistências assim podem ocorrer em várias ferramentas e literaturas, mas se você souber do que se trata, vai sempre se dar bem, mesmo que nomes diferentes sejam usados para se referir à mesma coisa.

    Agora no HxD, abra o mesmo binário e vá até este offset da IDT com o Ctrl+G. No binário que usei aqui o endereço é 0x38f8.

    Lá, os primeiros quatros bytes são o valor do campo RvaImportLookupTable do primeiro elemento do array. Se você seguir este offset, vai chegar na ILT. O primeiro elemento da ILT é um número de 64-bits, ou seja, de 8 bytes que aponta para a Hint/Name Table. Nesta tabela, o nome da função começa no terceiro byte conforme definição, logo após o campo Hint.

    Como desafio adicional, descubra à qual DLL a função pertence apenas olhando para a imagem anterior. Dica: busque pelo campo Name da IDT e siga o valor que ele contém neste binário.

    typedef struct {
        uint16_t e_magic;
        uint16_t e_cblp;
        uint16_t e_cp;
        uint16_t e_crlc;
        uint16_t e_cparhdr;
        uint16_t e_minalloc;
        uint16_t e_maxalloc;
        uint16_t e_ss;
        uint16_t e_sp;
        uint16_t e_csum;
        uint16_t e_ip;
        uint16_t e_cs;
        uint16_t e_lfarlc;
        uint16_t e_ovno;
        uint16_t e_res[4];
        uint16_t e_oemid;
        uint16_t e_oeminfo;
        uint16_t e_res2[10];
        uint32_t e_lfanew;
    } IMAGE_DOS_HEADER;
    This program cannot be run in DOS mode.
    #define SECTION_NAME_SIZE 8
    
    typedef struct {
        uint8_t Name[SECTION_NAME_SIZE];
        uint32_t VirtualSize;
        uint32_t VirtualAddress;
        uint32_t SizeOfRawData;
        uint32_t PointerToRawData;
        uint32_t PointerToRelocations;
        uint32_t PointerToLinenumbers; // descontinuado
        uint16_t NumberOfRelocations;
        uint16_t NumberOfLinenumbers;  // descontinuado
        uint32_t Characteristics;
    } IMAGE_SECTION_HEADER;
    typedef struct {
        uint32_t RvaImportLookupTable; // Antigo OriginalFistThink
        uint32_t TimeDateStamp;
        uint32_t ForwarderChain;
        uint32_t Name;
        uint32_t RvaImportAddressTable; // Antigo FirstThunk (ou Thunk Table)
    } IMAGE_IMPORT_DESCRIPTOR;

    30

    IMAGE_SCN_MEM_READ

    Terá permissão de leitura

    31

    IMAGE_SCN_MEM_WRITE

    Terá permissão de escrita

    5

    IMAGE_SCN_CNT_CODE

    A seção contém código executável

    6

    IMAGE_SCN_CNT_INITIALIZED_DATA

    A seção contém dados inicializados

    7

    IMAGE_SCN_CNT_UNINITIALIZED_ DATA

    A seção contém dados não inicializados

    29

    IMAGE_SCN_MEM_EXECUTE

    Terá permissão de execução

    Obtendo informações avançadas sobre o PE no DIE
    Visualizando o diretório de Imports (Import Table) no DIE
    Olhando a IDT, ILT e Hint/Name Table no HxD
    typedef struct {
    	uint16_t Hint;
    	uint8_t Name[1];
    } IMAGE_IMPORT_BY_NAME;

    Endereçamento

    hashtag
    Memória virtual

    Como poderiam dois executáveis com o mesmo ImageBase rodarem ao mesmo tempo se ambos são carregados no mesmo endereço de memória? Bem, a verdade é que não são. Existe um esquema chamado de memória virtual que consiste num mapeamento da memória RAM real física para uma memória virtual para cada processo no sistema, o que dá aos processos a ilusão de que estão sozinhos num ambiente monotarefa como era antigamente (vide MS-DOS e outros sistemas antigos). Essa memória virtual também pode ser mapeada para um arquivo em disco, como o pagefile.sys. O desenho a seguir ilustra o mecanismo de mapeamento:

    Memória Virtual

    Conforme explicado no capítulo sobre as Seções dos arquivos PE, a memória é dividida em páginas, tanto a virtual quanto a física. No desenho, os dois processos possuem páginas mapeadas pelo kernel (pelo gerenciador de memória, que é parte deste) em memória física e em disco (sem uso no momento). Perceba que as páginas de memória não precisam ser contíguas (uma imediatamente após a outra) no layout de memória física, nem no da virtual. Além disso, dois processos diferentes podem ter regiões virtuais mapeadas para a mesma região da memória física, o que chamamos de páginas compartilhadas.

    Em resumo, o sistema gerencia uma tabela que relaciona endereço físico de memória (real) com endereço virtual, para cada processo. Todos os "acham" que estão sozinhos no sistema, mas na verdade estão juntos sob controle do kernel.

    hashtag
    Endereço Virtual

    O endereço virtual, em inglês Virtual Address, ou simplesmente VA, é justamente a localização virtual em memória de um dado ou instrução. Por exemplo, quando alguém fazendo engenharia reversa num programa diz que no endereço 0x401000 existe uma função que merece atenção, quer dizer que ela está no VA 0x401000 do binário quando carregado em memória. Para ver a mesma função, você precisa carregar o binário em memória (normalmente feito com um debugger, como veremos num capítulo futuro) e verificar o conteúdo de tal endereço.

    hashtag
    Endereço Virtual Relativo

    Em inglês, Relative Virtual Address, é um VA que, ao invés de ser absoluto, é relativo à alguma base. Por exemplo, o valor do campo entrypoint no cabeçalho Opcional é um RVA relativo à base da imagem (campo ImageBase no mesmo cabeçalho). Com isso em mente, avalie seu valor na saída do comando dumpbin a seguir:

    No exemplo acima, o campo entrypoint, que é um RVA, tem o valor 0x1740. Como este campo é relativo ao ImageBase, o VA (endereço virtual) do entrypoint é então dado pela sua soma com o valor de ImageBase:

    Os RVA's podem ser relativos à outras bases que não à da imagem. É preciso consultar na documentação qual a relatividade de um determinado RVA para convertê-lo corretamente para o VA correspondente.

    Com isso encerramos o capítulo sobre o formato PE. Agora que você conhece a estrutura de um executável, vamos ver o que acontece depois que alguém dá um duplo-clique nele.

    Dump of file c:\windows\system32\calc.exe
    
    PE signature found
    
    File Type: EXECUTABLE IMAGE
    
    FILE HEADER VALUES
                8664 machine (x64)
                   7 number of sections
            EE8136FB time date stamp
                   0 file pointer to symbol table
                   0 number of symbols
                  F0 size of optional header
                  22 characteristics
                       Executable
                       Application can handle large (>2GB) addresses
    
    OPTIONAL HEADER VALUES
                 20B magic # (PE32+)
               14.38 linker version
                2000 size of code
                9000 size of initialized data
                   0 size of uninitialized data
                1740 entry point
                1000 base of code
           140000000 image base
                1000 section alignment
                1000 file alignment
    -- suprimido --
    >>> ep = 0x1740 + 0x140000000
    >>> hex(ep)
    '0x140001740'

    O formato PE

    Como explicado no capítulo anterior, a maioria dos tipos de arquivo que trabalhamos possui uma especificação. Com os arquivos executáveis no Windows não é diferente: eles seguem a especificação do formato PE (Portable Executable) que conheceremos agora.

    O formato PE é o formato de arquivo executável atualmente utilizado para muitos programas no Windows, isso inclui os famosos arquivos EXE mas também arquivos DLL, OCX, CPL e SYS. Seu nome deriva do fato de o formato não estar preso à uma arquitetura de hardware específica.

    Os programas que criam estes programas, chamados compiladores precisam respeitar tal formato e o programa que os interpreta, carrega e inicia sua execução (chamado de loader) precisa entendê-lo também.

    A documentação completa do formato PE é mantida pela própria Microsoft e está disponível em https://learn.microsoft.com/en-us/windows/win32/debug/pe-formatarrow-up-right.

    Assim como o formato GIF e outras especificações de formato de arquivo, o formato PE possui cabeçalhos, que possuem campos e valores possíveis. Outro conceito importante é o de seções.

    A estrutura geral de um arquivo PE é apresentada na imagem abaixo:

    Conheceremos agora os cabeçalhos mais importantes para este primeiro contato com a engenharia reversa e, em seguida, as seções de um arquivo PE.

    Estrutura de um arquivo PE

    Seções

    As seções são divisões num binário PE. Uma analogia que torna o conceito de seções simples de entender é a de comparar o binário PE com uma cômoda: as seções seriam suas gavetas. Cada gaveta da cômoda, em teoria, guarda um conteúdo diferente. Assim é com as seções.

    Elas são necessárias porque diferentes tipos de conteúdos exigem diferentes tratamentos quando carregados em memória pelo sistema operacional.

    Podemos então dizer que um binário PE é completamente definido por cabeçalhos e seções (com seu conteúdo), como na seguinte ilustração:

    Versão simplificada do arquivo PE

    Como dito, a principal separação que existe entre as seções é em relação a seu conteúdo, que distinguimos entre código ou dados. Apesar de terem seus nomes ignorados pelo loader do Windows, convencionam-se alguns nomes de seção normalmente encontrados em executáveis:

    hashtag
    .text

    Também nomeada CODE em programas compilados em Delphi, esta seção contém o código executável do programa. Em seu cabeçalho normalmente encontramos as permissões de leitura e execução em memória.

    hashtag
    .data

    Também chamada de DATA em programas criados com Delphi, esta seção contém dados inicializados com permissão de leitura e escrita. Estes dados podem ser, por exemplo, uma string declarada e já inicializada. Considere o programa abaixo:

    A variável local s é um array de char e pode ser alterada a qualquer momento dentro da função main(). De fato, logo após a sua declaração ela é alterada e logo depois acessada/lida pela função puts(). Em sua configuração padrão, o compilador coloca essa string numa seção de dados inicializados com permissão tanto para leitura quanto para escrita.

    Apesar de fazer sentido, os compiladores não precisam respeitar tal lógica. O conteúdo da variável s no exemplo apresentado pode ser armazenado na seção .rdata (ou mesmo na .text) e ser manipulado na pilha de memória para sofrer alterações. Não há uma imposição por parte do formato e cada compilador escolhe como fazer.

    hashtag
    .rdata

    Seção que contém dados inicializados com permissão somente para leitura. Um bom exemplo seria com o programa abaixo:

    Neste caso declaramos a variável s como const, o que instrui o compilador a armazená-la numa região de memória somente para leitura, casando perfeitamente com a descrição da seção .rdata.

    hashtag
    .idata

    Seção para abrigar as tabelas de imports, comum em todos os binários que importam funções de outras bibliotecas. Possui permissão tanto para leitura quanto para escrita. Entenderemos o motivo dessas permissões em breve.

    hashtag
    Alinhamento de Seções

    O sistema operacional divide a memória RAM em páginas, normalmente de 4 kilobytes (ou 4096 bytes) nas versões atuais do Windows. Nestas páginas de memória o sistema configura as permissões de leitura, escrita e execução. Os arquivos executáveis precisam ser carregados na memória e cada seção pode requerer permissões diferentes. Com isso em mente, considere a seguinte situação hipotética:

    • Um executável tem seus cabeçalhos ocupando 2 KB.

    • Sua seção .text tem 6 KB de tamanho e requer leitura e execução.

    • Sua seção .data tem 5 KB de tamanho e requer leitura e escrita.

    Para mapear este executável em memória e rodá-lo, o SO precisa copiar o conteúdo de suas seções em páginas de memória e configurar suas permissões de acordo. Analise agora a figura abaixo:

    Perceba na figura que a seção .text já ocuparia duas páginas que precisariam ter permissões de leitura e execução. No que sobrou da segunda página, o SO não pode mapear a .data pois esta, apesar de compartilhar a permissão de leitura, exige escrita ao invés de execução. Logo, ele precisa mapeá-la numa próxima página.

    Como consequência, o tamanho total de cada seção em memória é maior que seu tamanho em disco, devido ao que chamamos de alinhamento de seção. No cabeçalho opcional existe o campo SectionAlignment, que pulei propositalmente. Este campo define qual fator de alinhamento deve ser utilizado para todas as seções do binário quando mapeadas em memória. O padrão é o valor do tamanho da página de memória do sistema.

    Como bônus por ter chegado até aqui, segue um código que, depois de compilado e executado, vai te dizer qual o tamanho da página de memória na sua versão do Windows.

    Já sabemos como é a estrutura de um arquivo PE, mas precisamos voltar um pouquinho no assunto de diretórios para falar da Import Table, que veremos agora.

    O tamanho final do executável em disco é 13 KB.
    Mapeamento de seções em memória
    #include <stdio.h>
    
    int main(void) {
        char s[] = "texto grande para forçar o compilador a utilizar a seção de dados";
        s[0] = 'T';
        puts(s);
    }
    int main(void) {
        const char s[] = "texto grande para o compilador utilizar a seção de dados";
        puts(s);
    }
    #include <stdio.h>
    #include <windows.h>
    
    int main(void) {
        SYSTEM_INFO info;
    
        GetNativeSystemInfo(&info);    
        printf("dwPageSize: %u\n", info.dwPageSize);
    }