|
[16/05]
:. Yahoo! abre Search Monkey para desenvolvedores [16/05] :. Facebook recusa participação no Google Friend Connect [16/05] :. DOCTYPE, enciclopédia do Google para desenvolvedores web [16/05] :. Mais detalhes sobre o Asus Eee PC 901, baseado no Atom [16/05] :. Estudante inventa alternativa aos transistores de silício [16/05] :. Samsung demonstra notebook com tela AMOLED [16/05] :. Sobre a condenação de Hans Reiser [16/05] :. OLPC: agora com Windows [15/05] :. VirtualKeyboard, um teclado internacionalizado para sites [15/05] :. Fotos do Asus Eee PC 901, baseado no Atom [15/05] :. Microsoft TouchWall, o Surface de parede [15/05] :. Adobe lança Flash Player 10 Beta [15/05] :. QGtkStyle, aplicações em Qt com visual nativo do Gnome/GTK [15/05] :. Moonlight, uma implementação livre do MS Silverlight [15/05] :. MSI Wind: o Eee killer :. Mais noticias » |
Introdução
Neste tutorial vamos falar um pouco sobre a estrutura e o funcionamento de um arquivo executável para o Microsoft Windows, mais conhecido pela sua extensão: EXE. Creio que todo o usuário de Windows já ouviu alguma vez esse nome e sabe que é o arquivo principal de qualquer aplicativo, contendo o código do programa compilado. Os executáveis (ou binários) também recebem a denominação de PE. Essa sigla vem de “Portable Executable”, que em português significaria “Executável Portável”. Essa denominação vem de um padrão estabelecido pela Microsoft nos primórdios do Windows, onde decidiram criar um formato de binário capaz de rodar em qualquer outra versão do Windows. Teoricamente eles conseguiram, pois o formato do arquivo permaneceu inalterado desde o Windows 95. Os arquivos PE não se restringem apenas aos EXE. O mesmo formato é utilizado para as bibliotecas de linkagem (DLL), componentes ActiveX (OCX), entre diversos outros. Isso significa que o padrão de todos esses arquivos é semelhante, variando apenas alguns pequenos detalhes. Vamos dar uma atenção maior aos arquivos EXE, pois além de serem os mais “famosos”, são os que levam o formato PE da forma mais abrangente possível. Para tal, criei um pequeno aplicativo, contendo uma janela e um botão, que será o programa de testes, onde faremos as análises. Apesar de simples, é o suficiente para termos um executável completo e puro (programado em Assembly).
Ferramentas
Para o nosso estudo, vamos precisar somente de algumas ferramentas.
Estrutura a funcionamento
Um executável no formato PE possui uma estrutura um tanto quanto complexa, mas ao mesmo tempo muito organizada e versátil. O arquivo é organizado basicamente desta maneira: O cabeçalho DOS não tem utilidade prática dentro do sistema Windows, ele serve apenas para apresentar uma mensagem avisando o usuário que o aplicativo em questão não pode ser utilizado em modo texto. Já o cabeçalho do Windows é de extrema importância. É nele que estão todas as informações básicas necessárias para que o aplicativo funcione, como o número de seções, tamanho de cada seção e início das mesmas, onde iniciar a execução do código, dentro dezenas de outras configurações. Veremos isso mais adiante. O EXE é divido em seções, que variam de acordo com o compilador utilizado e que podem ser modificadas pelo usuário. Cada seção fica responsável por uma característica no PE. As informações referentes a cada uma das seções ficam armazenadas na “Tabela de Seções”. Abaixo estão listadas as seções mais comuns (e oficiais) de um binário para Win32. Veremos o que cada uma dessas seções comporta mais adiante, onde entraremos em detalhes mais técnicos. Uma característica interessante sobre os arquivos PE é que eles são armazenados na memória da mesma forma que eles ficam em disco, mantendo a estrutura no arquivo praticamente idêntica nos dois casos. Quando o usuário requisita a execução de um aplicativo, o Windows Loader (parte do Kernel do Windows responsável por iniciar e organizar o binário na memória) analisa o cabeçalho do PE. Feito isso, ele possui as informações necessárias para poder copiar o executável do disco rígido para a RAM. No entanto, ele não é carregado para a RAM exatamente da mesma forma que ele se encontra no Windows. O Loader precisa fazer alguns ajustes. Esses ajustes são necessários devido à forma com que o S.O. da Microsoft gerencia a memória, utilizando uma memória virtual paginada. Quando as seções são carregadas para a memória, o Windows alinha cada uma delas para caber em páginas de 4KB. É como se ele dividisse a RAM em diversos pedaços de 4KB e criasse um índice de cada trecho. Exemplo:
Supondo que você tem um trecho de dados com um tamanho de 5KB e o Windows precisa alocar esses 5KB na memória. Inicialmente ele verifica no índice se existem páginas livres onde esses dados possam ser armazenados. Caso existam, ele vai colocar os primeiros 4KB em uma página, e os outros 1KB restantes na página seguinte. Nesta última vão sobrar 3KB livres, que ficam inutilizáveis por outras aplicações. A figura abaixo demonstra melhor a situação: ![]() O conceito por trás da memória virtual é que ao invés de deixar o software controlar diretamente a memória, o programa chama o gerenciamento do Windows que por sua vez vai consultar e analisar as leituras e gravações na RAM. Isso aumenta a segurança geral do sistema. As vantagens por trás disso é a possibilidade de criar diversos espaços de endereçamento, que consiste em restringir o acesso a determinado trecho de memória somente ao aplicativo que originou a criação do mesmo, evitando que um software corrompa a memória utilizada por outra aplicação (como ocorria com os Win 9x). Além do alinhamento na memória, ele também possui um alinhamento em disco. O alinhamento em disco segue a mesma teoria, mas as “páginas” não são divididas em 4KB, pois isso ocasionaria em um desperdício muito grande de espaço. No arquivo elas são dividas em trechos de 512 bytes, o que explica o fato de qualquer executável padrão possui um tamanho múltiplo de 512 em disco (considerando o tamanho de alocação padrão da partição). Vamos então nos focar melhor em cada trecho do executável, começando pelo cabeçalho DOS.
Especificação técnica
Cabeçalho DOS
O arquivo PE começa com um cabeçalho DOS que ocupa os primeiros 64 bytes do arquivo. A função deste cabeçalho é verificar se o executável é ou não um arquivo válido, assim como identificar se o programa pode ser rodado via MS-DOS ou necessita do Windows. Para o caso de aplicativos programados para o Windows, a única função do cabeçalho DOS é exibir esta mensagem (caso seja rodado a partir do MS-DOS): “This program must be run under Microsoft Windows” Este texto fica armazenado logo após o cabeçalho DOS, numa área chamada “DOS Stub”. Essa área tem como função o armazenamento de dados que possam ser utilizados na execução do arquivo. É no DOS Stub que ficam as instruções para imprimir o texto destacado acima. Abaixo vou adicionar a estrutura oficial desse cabeçalho, que é utilizada pelos programadores. Não há a necessidade de entender o significado de cada item, mas vou ressaltar os dois mais importantes. IMAGE_DOS_HEADER STRUCT
e_magic WORD ? e_cblp WORD ? e_cp WORD ? e_crlc WORD ? e_cparhdr WORD ? e_minalloc WORD ? e_maxalloc WORD ? e_ss WORD ? e_sp WORD ? e_csum WORD ? e_ip WORD ? e_cs WORD ? e_lfarlc WORD ? e_ovno WORD ? e_res WORD 4 dup(?) e_oemid WORD ? e_oeminfo WORD ? e_res2 WORD 10 dup(?) e_lfanew DWORD ? IMAGE_DOS_HEADER ENDS Como pode ver, temos diversos itens com tamanhos WORD e DWORD, que se forem somados, fecham os 64 bytes iniciais do cabeçalho. De todos esses nomes, vou destacar os dois mais importantes. Veja a imagem abaixo, que representa a estrutura do cabeçalho DOS, dentro do aplicativo de testes (utilize o WinHex para visualizar, caso queira) ![]() Nesta imagem podemos notar claramente aqueles dois dados mencionados anteriormente. Os dois primeiros bytes (4D5A) compõem o “e_magic”, contendo a sigla MZ (valores ASCII para 4D e 5A). Já no final do cabeçalho DOS (offset 0000003Ch) nós temos o “e_lfanew”, que indica o local no arquivo onde está localizado o cabeçalho PE.
Cabeçalho Windows
O cabeçalho Windows, ou cabeçalho PE, contém as informações fundamentais para o aplicativo. É nele que estão indicadas todas as características do binário. Ele é composto por um conjunto de estruturas, que variam de tamanho conforme a complexidade do aplicativo e/ou o número de seções que nele estão armazenadas. A primeira dessas estruturas é o cabeçalho do NT IMAGE_NT_HEADERS STRUCT
Signature DWORD ? FileHeader IMAGE_FILE_HEADER <> OptionalHeader IMAGE_OPTIONAL_HEADER32 <> IMAGE_NT_HEADERS ENDS Como podemos notar, ele é composto por três itens. O primeiro (“Signature”) possui a mesma função do “e_magic”. Ele apenas identifica o cabeçalho NT, e deve ser composto pela sigla PE, seguido de dois bytes nulos, fechando os 4 bytes da DWORD. Em seguida temos o “FileHeader”, ocupando os próximos 20 bytes do cabeçalho NT, contendo informações sobre a estrutura física do arquivo executável. Veja a estrutura do “FileHeader” abaixo:
IMAGE_FILE_HEADER STRUCT
Machine WORD ? NumberOfSections WORD ? TimeDateStamp DWORD ? PointerToSymbolTable DWORD ? NumberOfSymbols DWORD ? SizeOfOptionalHeader WORD ? Characteristics WORD ? IMAGE_FILE_HEADER ENDS
Dessa estrutura, os dados mais importantes são: Voltando ao cabeçalho NT, temos por último uma outra estrutura, chamada de “OptionalHeader”. Apesar do nome, ela é obrigatória. Essa estrutura possui um tamanho de 224 bytes, sendo que os últimos 128 são reservados para o diretório de dados, que veremos adiante. É certamente a maior estrutura, contendo o maior número de valores. IMAGE_OPTIONAL_HEADER32 STRUCT
Magic WORD ? MajorLinkerVersion BYTE ? MinorLinkerVersion BYTE ? SizeOfCode DWORD ? SizeOfInitializedData DWORD ? SizeOfUninitializedData DWORD ? AddressOfEntryPoint DWORD ? BaseOfCode DWORD ? BaseOfData DWORD ? ImageBase DWORD ? SectionAlignment DWORD ? FileAlignment DWORD ? MajorOperatingSystemVersion WORD ? MinorOperatingSystemVersion WORD ? MajorImageVersion WORD ? MinorImageVersion WORD ? MajorSubsystemVersion WORD ? MinorSubsystemVersion WORD ? Win32VersionValue DWORD ? SizeOfImage DWORD ? SizeOfHeaders DWORD ? CheckSum DWORD ? Subsystem WORD ? DllCharacteristics WORD ? SizeOfStackReserve DWORD ? SizeOfStackCommit DWORD ? SizeOfHeapReserve DWORD ? SizeOfHeapCommit DWORD ? LoaderFlags DWORD ? NumberOfRvaAndSizes DWORD ? DataDirectory IMAGE_DATA_DIRECTORY IMAGE_OPTIONAL_HEADER32 ENDS Bastante coisa não? Os nomes das variáveis na maioria dos casos explicam o seu propósito, mas como fiz anteriormente, colocarei aqui uma explicação mais profunda sobre algum desses valores. Veja a imagem abaixo, que ilustra o cabeçalho WIN ( PE Header ) dentro do editor Hexadecimal: ![]() Uma outra forma de visualizar o cabeçalho do arquivo PE é utilizando algum programa específico para isso, como o caso do PEiD, LordPE ou até mesmo um debugger com a opção de desmembrar o cabeçalho (como é o caso do OllyDbg). Para finalizar o cabeçalho Windows, precisamos falar sobre o IMAGE_DATA_DIRECTORY. Como mencionado logo acima, ele compõe os últimos 128 bytes do PE Header, sendo uma estrutura importante contendo o endereço (RVA) e o tamanho dos diretórios do executável. Segue abaixo a estrutura do IMAGE_DATA_DIRECTORY: IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress DWORD ? ISize DWORD ? IMAGE_DATA_DIRECTORY ENDS Um tanto quanto simples. Podemos ver que se tratam apenas de dois valores DWORD (cada um com 4 bytes, totalizando 8 por estrutura). Essa estrutura é utilizada pelos 16 diretórios de dados, que são listados a seguir: IMAGE_DIRECTORY_ENTRY_EXPORT equ 0
IMAGE_DIRECTORY_ENTRY_IMPORT equ 1 IMAGE_DIRECTORY_ENTRY_RESOURCE equ 2 IMAGE_DIRECTORY_ENTRY_EXCEPTION equ 3 IMAGE_DIRECTORY_ENTRY_SECURITY equ 4 IMAGE_DIRECTORY_ENTRY_BASERELOC equ 5 IMAGE_DIRECTORY_ENTRY_DEBUG equ 6 IMAGE_DIRECTORY_ENTRY_COPYRIGHT equ 7 IMAGE_DIRECTORY_ENTRY_GLOBALPTR equ 8 IMAGE_DIRECTORY_ENTRY_TLS equ 9 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG equ 10 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT equ 11 IMAGE_DIRECTORY_ENTRY_IAT equ 12 Para cada uma dessas entradas há uma estrutura do tipo IMAGE_DATA_DIRECTORY. Temos 16 entradas de diretórios, e cada uma delas possui oito bytes, totalizando os 128 bytes finais do cabeçalho WIN. Veja a marcação em amarelo na imagem anterior, na qual existem 128 bytes marcados em amarelo. Podemos agora sair do cabeçalho WIN e partir para a tabela de seções
Tabela de Seções
A tabela de seções funciona de forma semelhante ao IMAGE_DATA_DIRECTORY. Nessa tabela estão contidas diversas informações referentes a cada uma das seções presente no executável (como o tamanho, endereços e características). A quantidade de itens na tabela de vai variar dependendo do número de seções contidos no aplicativo (essa informação pode ser obtida na entrada NumberOfSections do cabeçalho WIN). IMAGE_SECTION_HEADER STRUCT
Name1 BYTE IMAGE_SIZEOF_SHORT_NAME dup(?) union Misc PhysicalAddress DWORD ? VirtualSize DWORD ? ends VirtualAddress DWORD ? SizeOfRawData DWORD ? PointerToRawData DWORD ? PointerToRelocations DWORD ? PointerToLinenumbers DWORD ? NumberOfRelocations WORD ? NumberOfLinenumbers WORD ? Characteristics DWORD ? IMAGE_SECTION_HEADER ENDS IMAGE_SIZEOF_SHORT_NAME equ 8 Na introdução do tutorial vimos que existem diversas seções dentro de um arquivo PE, sendo algumas delas “oficiais”. O nosso aplicativo de teste é composto por apenas 4 seções: Podemos então analisar a tabela dessas 4 seções dentro do WinHex: ![]()
As seções
Como foi dito anteriormente, o arquivo PE pode conter infinitas seções, sendo que algumas delas são oficiais e estão presentes na maioria dos executáveis. Abaixo vamos descrever qual a função de cada uma:
Apêndice: Endereçamento virtual
Entendendo o endereçamento virtual
O Windows trabalha com uma forma de endereçamento virtual de memória. Isso quer dizer basicamente que os aplicativos não trabalham com endereços absolutos baseados no arquivo, mas sim na memória. Podemos citar três formas de endereçamento: Offset, VA e RVA. Tendo essas diferenciações, podemos formar pequenas equações que talvez esclareçam um pouco as coisas: RVA = VA – ImageBase Vamos para um exemplo. Suponha que um aplicativo qualquer possua um ImageBase com valor 400000h. O VA relativo ao início do espaço de memória destinado ao aplicativo passa a ser 400000h. Suponho agora que o aplicativo inicie sua execução no RVA 1000h. Pela formula acima, podemos descobrir que o VA do início da execução do programa está no endereço 401000h (RVA + ImageBase = 400000h + 1000h). Não é tão complicado quanto parece, só é preciso cautela para não confundir as nomenclaturas.
Conversão entre Offset e VA
Para fazer a conversão de um Offset para um VA, é necessário conhecer alguns dados do aplicativo. Primeiramente devemos saber em qual seção o nosso offset está localizado. Para isso basta comparar o offset que você possui com o PointerToRawData e o SizeOfRawData de cada uma das seções. Fica mais fácil de entender através de um exemplo (vou utilizar o nosso aplicativo de testes). Digamos que eu queira descobrir o VA do offset 00000900h. Abaixo está uma tabela com o RawOffset e o RawSize de cada seção (retirado da tabela de seções):
Analisando a tabela, podemos notar que o offset está contido dentro da seção de recursos, pois ela vai de 00000800h até 0000A00h (800h + 200h) e o nosso offset aponta no meio dela (00000900h). Como observamos anteriormente, as seções são copiadas para a memória da mesma forma que elas estão no arquivo em disco, portanto, o VA que queremos descobrir também está 100h bytes a frente do VirtualOffset da seção de recursos. Então basta somar o 100h ao VirtualOffset da seção e incluir o ImageBase. VA = RawOffset – RawOffset da seção + VirtualOffset da seção + ImageBase No nosso exemplo, tendo a ImageBase como 00400000h, esses cálculos seriam: VA = 00000900h – 00000800h + 00004000h + 00400000h VA = 00404100h Analogamente, também é possível descobrir o Offset através de um VA: RawOffset = VA – VirtualOffset da seção – ImageBase + RawOffset da seção Fazendo o processo inverso para o nosso exemplo, tendo o VA 00404100h e querendo saber o RawOffset: RawOffset = 00404100h – 00004000h – 00400000h + 00000800h RawOfsset = 00000900h
Conclusão
Espero que esse tutorial tenha atingido o seu objetivo, que era de dar uma visão geral sobre o formato dos executáveis, assim como colocar informações úteis para programadores que pretendem se aventurar nesse ramo. Deixei de lado algumas informações, como a seção de relocação, pois é dela aparecer. Fiquei satisfeito com o resultado e devo dizer que ao mesmo tempo em que escrevia este artigo, aprendi algumas coisas novas sobre o formato, que passaram despercebidas quando eu comecei a me interessar pelo assunto. A vantagem de entender e dominar esse tipo de arquivo é que você passa a ter a possibilidade de “customizar” o executável, seja para modificar ou proteger seu software, alterando um pouco a disposição e os endereços padrões estabelecidos. Para quem um dia pensa em fazer um compilador, editor de recursos ou simplesmente um visualizador de arquivos PE, creio que este tutorial possa ajudar. Gostaria de deixar um agradecimento especial ao fórum Guia do Hardware, por ceder um espaço onde eu possa publicar estes artigos, assim como receber críticas e sugestões do mesmo. Obrigado e até a próxima! Por Fernando Birck - www.fergonez.net
Referências
|
||||||||||||||||||||||||||||||||||||||||