arrow-left

All pages
gitbookPowered by GitBook
1 of 6

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

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.

#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;

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

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:

  • 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.

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

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;
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;
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

    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.

  • Conteúdo do binário da calculadora do Windows no HxD
    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--
    This program cannot be run in DOS mode.

    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:

    Nomenclatura Microsoft

    Nome do tipo em C (Microsoft Visual Studio)

    Tamanho em bytes

    BYTE

    unsigned char

    1

    WORD

    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.

    unsigned short

    2

    DWORD

    unsigned long

    4

    QWORD

    __int64

    8

    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;