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.
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 visualizador hexadecimal como o hexdump no Linux para verificar tal informação. Vamos pedir, por exemplo, os primeiros 16 bytes de um arquivo putty.exe:
Perceba os bytes 0x4d e 0x5a logo no início do arquivo.
O hexdump exibe um caractere de ponto (.) na terceira coluna quando o byte não está na faixa ASCII imprimível, ao contrário do wxHexEditor que exibe um caractere de espaço em branco.
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. Isso se refere aos campos e não a seus valores que, naturalmente, podem variar de arquivo para arquivo. No caso do e_lfanew, se fizermos as contas, veremos que ele sempre estará 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 hexdump que pule (opção -s de skip) então 0x3c bytes antes de começar a imprimir o conteúdo do arquivo em hexadecimal. No comando a seguir também limitamos a saída em 16 bytes com a opção -n (number):
O número de 32 bits na posição 0x3c é então 0x000000f8 ou simplesmente 0xf8 (lembre-se do little endian). 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 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.
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 (0xd8 no caso deste executável).
Logo após os 4 bytes do campo e_lfanew, começa o código do programa de MS-DOS, 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 DOS stub é todavia bem visível).
Finalmente, na posição 0xd8 encontra-se a assinatura PE\0\0. Aqui sim, começa o formato PE propriamente dito.
Cabeçalhos, como o nome sugere, são áreas de dados no início de um arquivo. Basicamente são definidos por conjuntos de diferentes campos, que admitem valores.
Cada campo possui um tipo que também já 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:
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. Um bom exemplo é o campo "Characteristics" do cabeçalho de seções do arquivo PE, que veremos mais adiante.
Não parece, mas este cabeçalho é muito importante. Seu nome deve-se ao fato de que ele é opcional para arquivos objeto, mas é obrigatório em arquivos executáveis, que são o nosso foco de estudo. O tamanho desde cabeçalho não é fixo. É definido pelo campo SizeOfOptionalHeader do cabeçalho COFF, que vimos anteriormente. Sua estrutura para arquivos PE de 32-bits, também chamados de PE32, é a seguinte:
Vamos analisar agora os campos mais importantes no nosso estudo:
O primeiro campo, de 2 bytes, é um outro número mágico que identifica o tipo de executável em questão. O valor 0x10b significa que o executável é um PE32 (executável PE de 32-bits), enquanto o valor 0x20b diz que é um PE32+ (executável PE de 64-bits).
A Microsoft chama os executáveis de PE de 64-bits de PE32+ e não de PE64.
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.
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, onde o programa será carregado em memória. Para arquivos executáveis (.EXE) o padrão é 0x400000. Já para bibliotecas (.DLL), o padrão é 0x10000000, embora executáveis do Windows como o calc.exe também apresentem este valor no ImageBase.
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.
Ao contrário do que possa parecer, este campo não é somente para DLL's. 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:
O estado bit 6 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 8 diz respeito ao DEP (Data Execution Prevention), também conhecido pela sigla NX (No eXecute). O estudo aprofundado destes recursos foge do escopo inicial deste livro, mas é importante que saibamos que podemos desabilitar tais recursos simplesmente forçando estes bits para zero.
Ainda como parte do cabeçalho opcional, temos os diretórios de dados, ou Data Directories. São 16 diretórios ao todo, cada um com uma função. Concentraremos, no entanto, 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 agora alguns diretórios:
O primeiro diretório de dados aponta para a tabela de exports, ou seja, de funções exportadas pela aplicação. Por isso mesmo sua presença (campos VirtualAddress e Size 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). Entenderemos mais sobre estas e outras estruturas em breve.
Sendo a contraparte da Export Table, a Import Table aponta para a tabela de imports, ou seja, de funções importadas pela aplicação. O campo VirtualAddress aponta para a IDT (Import Directory Table), que tem um ponteiro para a IAT (Import Address Table), que estudaremos mais à frente.
Aponta para uma estrutura de árvore binária que armazena todos os resources num executável (ícones, janelas, strings - principalmente quando o programa suporta vários idiomas), etc. Também estudaremos um pouco mais sobre estes "recursos" no futuro.
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:
Campo de 2 bytes que define a arquitetura da máquina para qual o programa foi construído. Valores comuns são 0x14c (Intel i386 ou compatíveis) e 0x8664 (AMD64 ou compatíveis). A tabela completa está disponível na documentação oficial.
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.
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 lembrar que este campo não é utilizado pelo loader de arquivos PE no Windows e seu valor pode ser alterado 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.
Contém o tamanho do próximo cabeçalho, conhecido como Cabeçalho Opcional, que estudaremos muito em breve.
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:
Analise novamente o (dump hexadecimal do executável da calculadora)[dos.md#exercicios] considere que:
Logo após a assinatura PE na posição 0xd8 temos o primeiro campo do cabeçalho COFF que é o Machine. Ele é um campo de 2 bytes conforme já dito, então os bytes 0x4c e 0x01 definem seu valor. Considerando o endianness, chegamos ao valor 0x14c, que define que este executável foi criado para máquinas Intel i386 ou compatíveis.
Em seguida, na posição 0xde, temos o NumberOfSections que é 4.
Depois vem o campo TimeDateStamp com o número inteiro de 32 bits (4 bytes) sem sinal 0x4ce7979d que é 1290246045 em decimal. Podemos usar o comando date do Linux para converter para data e hora atuais:
Pulamos então 8 bytes referentes aos campos PointerToSymbolTable e NumberOfSymbols (normalmente zerados mesmo), encontrando o valor da word SizeOfOptionalHeader em 0xec de valor 0xe0.
A próxima word é o valor do campo Characteristics, que neste arquivo é 0x102. Convertendo para binário temos o valor 100000010 (bits 2 e 9 setados) significando que o arquivo é um executável de 32-bits.
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.
Baixe e descompate o arquivo CRACKME.ZIP em https://www.mentebinaria.com.br/files/file/19-crackme-do-cruehead/. Usando o comando dumpbin através do Visual Studio Developer Command Prompt (instalado com o Visual Studio Community), exiba o COFF/File Header do binário CRACKME.EXE. Você deve ver algo assim:
Com o DIE, é preciso carregar o CRACKME.EXE nele, marcar a caixa de seleção Advanced, clicar no botão PE (Alt+P), na aba NT Headers e por fim, na aba File Header. Você deve ver uma janela como a abaixo:
Os botões com "..." localizados ao lado direito de vários valores de campos provêem informações adicionais sobre tais valores. Não deixe de experimentar.
Bit | Nome |
---|
Bit | Nome | Comentários |
---|
Nomenclatura Microsoft
Nome do tipo em C (Microsoft Visual Studio)
Tamanho em bytes
BYTE
unsigned char
1
WORD
unsigned short
2
DWORD
unsigned long
4
QWORD
__int64
8
6 | IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE |
8 | IMAGE_DLLCHARACTERISTICS_NX_COMPAT |
2 | IMAGE_FILE_EXECUTABLE_IMAGE | Obrigatório para arquivos executáveis |
9 | IMAGE_FILE_32BIT_MACHINE | Arquivo de 32-bits |
14 | IMAGE_FILE_DLL | O arquivo é uma DLL |
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:
Este campo define o nome da seção. Como é um array de 8 elementos do tipo uint8_t, este nome está limitado à 8 caracteres. A string .text por exemplo ocupa apenas 5 bytes, então os outros 3 devem estar zerados. Há suporte à UTF-8 para estes nomes.
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.
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.
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".
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 é 0x400 e o valor do campo SizeOfRawData é 0x1800, para ver somente seu conteúdo em hexadecimal com o hexdump poderíamos fazer:
Este é um campo que define algumas flags 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:
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. Aliás, agora é uma boa hora para abrir o DIE e analisar alguns arquivos executáveis, colocando em prática tudo o que foi visto até aqui.
Bit | Nome da flag | Descrição |
---|---|---|
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
30
IMAGE_SCN_MEM_READ
Terá permissão de leitura
31
IMAGE_SCN_MEM_WRITE
Terá permissão de escrita