Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Como explicado no capítulo anterior, a maioria dos tipos de arquivo que trabalhamos possuem 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 os programas no Windows, isso inclui os famosos arquivos EXE mas também DLL, OCX, CPL e SYS. Seu nome deriva do fato de que o formato não está preso a 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-format.
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.
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.
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.
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 tipo de dado distinto, e assim é com as seções, apesar de não ser uma regra muito rígida. Elas são necessárias porque diferentes 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:
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, normalmente iniciados por um ponto. As seções padrão importantes são discutidas a seguir:
Também nomeada CODE em programas compilados com o 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.
Também chamada de CODE 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 C string declarada e já inicializada. Por exemplo, 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, o código na linha 6 a altera. Sendo assim, é bem possível que um compilador coloque seu conteúdo 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 fazer do seu jeito.
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.
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 gravação. Entenderemos o motivo em breve.
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 (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.
O tamanho final do executável em disco é 13 KB.
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 na 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 dizer qual o tamanho da página de memória na sua versão do Windows.
Bit | Nome |
---|---|
6
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
8
IMAGE_DLLCHARACTERISTICS_NX_COMPAT
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 |
Onde já se viu dois executáveis com o ImageBase em 0x400000 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, dando a eles 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:
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 "acham" que estão sozinhos no sistema, mas na verdade estão juntos sob controle do kernel.
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. 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.
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 a seguir:
No exemplo acima, o campo entrypoint tem o valor 0x39c2, que é um RVA. 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 a base da imagem. É preciso consultar na documentação qual a relatividade de um RVA para convertê-lo corretamente para o VA correspondente.
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 | Comentários |
---|
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 |
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.
Foi visto que um dos diretórios de dados presentes no cabeçalho opcional é reservado para a Import Table, também conhecida por sua sigla IT. Nele há um ponteiro para a IDT (Import Directory Table), apontado pelo valor do campo VirtualAddress.
A IDT, por sua vez, é um array de estruturas do tipo IMAGE_IMPORT_DESCRIPTOR definidas a seguir:
Não se deve confundir a IDT (Import Descriptor Table) com a IDT (Interrupt Descriptor Table). Esta última é uma estrutura que mapeia interrupções para seus devidos handlers, assunto que não é coberto neste 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 DLL's das quais o executável importa funções. Há ainda um elemento adicional, completamente zero (preenchido com null bytes) para indicar o fim do array.
O campo OriginalFirstThunk (chamado de rvaImportLookupTable em algumas literaturas) aponta para o que chamamos Import Lookup Table (ILT), um array de números de 32-bits (64-bits para PE32+), onde seu bit mais significativo (MSB - Most Significant Bit) define se a função será importada por número ordinal (caso o bit seja 1). Já no caso de este bit estar zerado, a importação da função dá-se por nome e os outros 31 bits do número representam um endereço para uma estrutura que finalmente contém o nome da função.
Sendo assim, o número de elementos do array ILT é igual ao número de funções importadas por uma DLL em particular, definida na estrutura IMAGE_IMPORT_DESCRIPTOR.
Este campo aponta finalmente para o que chamamos de IAT (Import Address Table), muito conhecida dos engenheiros reversos. 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), é preenchida com os endereços reais das funções importadas. Isto porque um executável dinamicamente linkado não sabe ainda 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 utilize possam chamar suas funções. Por isso todo este esquema de preenchimento da IAT pelo loader é necessário.
Para fixar o conteúdo, é interessante validar tais informações. O exemplo abaixo utiliza o DIE para ver o diretório de dados de um arquivo PE:
Neste exemplo o campo VirtualAddress do Import Directory tem o valor 0x3000. Este é um endereço relativo, que você aprenderá sobre na próxima seção, no entanto, por agora você precisa apenas saber que este endereço é somado ao ImageBase para funcionar. No caso, o ImageBase deste binário é 0x400000 (muito comum), então o endereço da Import Descriptor Table, apontado por este campo VirtualAddress, é 0x403000.
Perceba que o DIE chama o campo VirtualAddress dos diretórios apenas de Address.
Ao clicar no botão "H" à direita do diretório IMPORT, vemos o conteúdo do primeiro elemento do array IDT, que é justamente o campo OriginalFirstThunk:
Neste exemplo o valor é 0x00003078 (lembre-se que números são armazenados em little-endian). Sendo novamente um endereço relativo, então o endereço da ILT é 0x403078.
Seguindo este endereço, encontramos achamos a ILT, que é um array de números de 32-bits como já dissemos:
O primeiro número deste array é então 0x000032cc. Como seu MSB está zerado, sabemos que se trata de uma importação por nome (e não por número da função). Se seguirmos este endereço, novamente somando o ImageBase, finalmente chegamos ao nome da função:
A estrutura que contém o nome da função é chamada de Hint/Name Table _**_onde o nome da função começa no terceiro byte, neste caso, em 0x4032ce. O tamanho do nome é variável (naturalmente o tamanho em bytes do nome de uma função pode variar).
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