Informática Numaboa - Referências
O formato PE
Sab 11 Abr 2009 11:01 |
- Detalhes
- Categoria: Formatos padrão
- Atualização: Sábado, 11 Abril 2009 23:44
- Autor: vovó Vicki
- Acessos: 14446
Cabeçalho do arquivo
Cabeçalho MZ do DOS |
Fragmento (stub) do DOS |
Cabeçalho do Arquivo |
Cabeçalho Opcional Diretório de Dados |
Cabeçalhos das Seções |
Seção 1 |
Seção 2 |
... |
Seção n |
Para obter o IMAGE_FILE_HEADER é preciso validar o "MZ" do cabeçalho do DOS (os primeiros 2 bytes), depois encontrar o membro 'e_lfanew' do cabeçalho do fragmento do DOS (offset 3C) e avançar o número indicado de bytes a partir do início do arquivo. Resumindo: na posição 003C encontra-se o 'e_lfanew' cujo valor, de 32 bits, nos indica a posição do início do cabeçalho do arquivo, ou seja, a IMAGE_NT_SIGNATURE, cujo valor é sempre 00004550 (veja na página anterior se tiver dúvidas).
Os componentes do cabeçalho do arquivo são os seguintes:
- Assinatura do cabeçalho PE
- Tipo de máquina previsto para rodar o executável
- Número de seções
- TimeDateStamp
- Ponteiro para Tabela de Símbolos e Número de Símbolos
- Tamanho do Cabeçalho Opcional
- Características
Assinatura PE e tipo de máquina
O cabeçalho do arquivo, uma estrutura do tipo IMAGE_FILE_HEADER, tem como primeiro componente a assinatura PE e, logo a seguir, um dos seguintes elementos:
Nome | #define em hexa | Significado |
IMAGE_FILE_MACHINE_I386 | 014C | processador Intel 80386 ou melhor |
014D | processador Intel 80486 ou melhor | |
014E | processador Intel Pentium ou melhor | |
0160 | E3000 (MIPS), big endian | |
IMAGE_FILE_MACHINE_R3000 | 0162 | R3000 (MIPS), little endian |
IMAGE_FILE_MACHINE_R4000 | 0166 | R4000 (MIPS), little endian |
IMAGE_FILE_MACHINE_R10000 | 0168 | R10000 (MIPS), little endian |
IMAGE_FILE_MACHINE_ALPHA | 0184 | DEC Alpha AXP |
IMAGE_FILE_MACHINE_POWERPC | 01F0 | IBM Power PC, little endian |
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
... | ||||||||||||||||
0000 00C0 | 50 | 45 | 00 | 00 | 4C | 01 |
No nosso exemplo ficou determinado que a assinatura PE se encontra no offset 00C0, ocupando 4 bytes. Os dois bytes seguintes, nas posições 00C4 e 00C5, indicam que o executável foi previsto para rodar num processador Intel 80386 ou melhor: IMAGE_FILE_MACHINE_I386 de valor 014C (não esqueça de inverter os bytes, conforme explicado anteriormente).
Número de seções
O terceiro componente do cabeçalho do arquivo é um valor de 16 bits que indica o número de seções após o cabeçalho. As seções serão discutidas em detalhe adiante. No nosso exemplo, são 4 (04 00 invertido = 00 04).
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
... | ||||||||||||||||
0000 00C0 | 50 | 45 | 00 | 00 | 4C | 01 | 04 | 00 |
Carimbo de data e hora
Logo após o número de seções encontra-se um valor de 32 bits, o "TimeDateStamp" (carimbo de data e hora), referente ao momento da criação do arquivo. Pode-se distinguir as diversas versões de um mesmo arquivo através deste valor, mesmo que o valor "oficial" da versão não tenha sido alterado. O formato deste carimbo não está documentado, exceto de que deveria ser único entre as versões do mesmo arquivo. Aparentemente corresponde ao número de segundos decorridos a partir de 1 de Janeiro de 1970 00:00:00, em UTC - o formato utilizado pela maioria dos compiladores C para time_t. Este carimbo é utilizado para a união de diretórios de importação, os quais serão abordados adiante.
alguns linkers costumam atribuir valores absurdos ao carimbo, fora dos padrões time_t descritos acima.
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
... | ||||||||||||||||
0000 00C0 | 50 | 45 | 00 | 00 | 4C | 01 | 04 | 00 | A3 | 77 | 55 | 3C |
Invertendo os bytes, o TimeDateStamp do nosso executável exemplo mostra um valor hexadecimal de 3C55 77A3 que corresponde a 1.012.234.147 decimal. Sabendo que este valor é o número de segundos decorridos a partir de 01.01.70 e que cada dia possui 86.400 segundos, podemos calcular que o binário em questão foi criado 11.716 dias (um pouquinho menos) após o início de 1970. Isto corresponde a mais ou menos 32 anos ... UAU! O executável foi criado no ano de 2002 (1970 + 32). Realmente, se você observar a data do arquivo exemplo (o tutNB03.exe), verá que o executável foi criado em 28 de Janeiro de 2002 às 14:12 horas.
PointerToSymbolTable e NumberOfSymbols
Os componentes "PointerToSymbolTable" (ponteiro para a tabela de símbolos) e "NumberOfSymbols" (número de símbolos), ambos de 32 bits, são utilizados para fins de debug. Não sei como decifrá-los e, geralmente, estão zerados.
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
... | ||||||||||||||||
0000 00C0 | 50 | 45 | 00 | 00 | 4C | 01 | 04 | 00 | A3 | 77 | 55 | 3C | 00 | 00 | 00 | 00 |
0000 00D0 | 00 | 00 | 00 | 00 |
SizeOfOptionalHeader
O "SizeOfOptionalHeader" (tamanho do cabeçalho opcional), de 16 bits, é simplesmente o tamanho do IMAGE_OPTIONAL_HEADER. Pode ser utilizado para verificar se a estrutura do arquivo PE está correta. No nosso exemplo, o tamanho do cabeçalho opcional é 224 (E000 invertido = 00E0 hexa = 224 decimal).
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
... | ||||||||||||||||
0000 00C0 | 50 | 45 | 00 | 00 | 4C | 01 | 04 | 00 | A3 | 77 | 55 | 3C | 00 | 00 | 00 | 00 |
0000 00D0 | 00 | 00 | 00 | 00 | E0 | 00 |
Características
"Characteristics" (características) tem 16 bits e consiste num conjunto de flags, a maioria válida apenas para arquivos objeto e bibliotecas:
Bit | Nome | Setado (valor 1) |
0 | IMAGE_FILE_RELOCS_STRIPPED | Se não houver informações de remanejamento no arquivo. Isto se refere a informações de remanejamento por seção nas próprias seções. Não é utilizado em executáveis, os quais possuem informações de remanejamento no diretório "base relocation" descrito abaixo. |
1 | IMAGE_FILE_EXECUTABLE_IMAGE | Se o arquivo for um executável, isto é, não é nem um arquivo objeto nem uma biblioteca. Esta flag também pode estar setada se a tentativa do linker em criar um executável tenha falhado por algum motivo - a imagem é mantida para facilitar uma linkagem incremental numa próxima tentativa. |
2 | IMAGE_FILE_LINE_NUMS_STRIPPED | Se a informação do número de linha tiver sido eliminada. Não é usado em executáveis. |
3 | IMAGE_FILE_LOCAL_SYMS_STRIPPED | Se não houver informação sobre símbolos locais. Não é usado em executáveis. |
4 | IMAGE_FILE_AGGRESIVE_WS_TRIM | Se o sistema operacional deve cortar agressivamente o conjunto de trabalho do processo em andamento (a quantidade de RAM que o processo utiliza) através de um 'paging out'. Este bit deve ser setado apenas em aplicativos tipo demon que, na maior parte do tempo, ficam em estado de espera. |
7 | IMAGE_FILE_BYTES_REVERSED_LO | Assim como seu par, o bit 15, está setado se o endian do arquivo não corresponder ao esperado pela máquina, de modo que precisa haver uma troca de bytes antes que uma leitura seja efetuada. Esta flag não é confiável para arquivos executáveis - o sistema operacional conta com os bytes devidamente ordenados nos executáveis. |
8 | IMAGE_FILE_32BIT_MACHINE | Se for para rodar numa máquina de 32 bits. |
9 | IMAGE_FILE_DEBUG_STRIPPED | Se não houver informação de debug no arquivo. Não é utilizado para executáveis. De acordo com outras informações ([6]), este bit é denominado de "fixo" e é setado se a imagem só puder rodar se estiver mapeada no endereço preferido (ou seja, não permite remanejamento). |
10 | IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | Se o aplicativo não puder rodar a partir de um meio removível, como um disquete ou CD-ROM. Neste caso, o sistema operacional é informado para copiar o arquivo para um arquivo temporário ("swapfile") e executá-lo a partir da cópia. |
11 | IMAGE_FILE_NET_RUN_FROM_SWAP | Se o aplicativo não puder ser executado em rede. Neste caso, o sistema operacional é informado para copiar o arquivo para um arquivo temporário local ("swapfile") e executá-lo a partir da cópia. |
12 | IMAGE_FILE_SYSTEM | Se o arquivo for um arquivo de sistema, por exemplo, um driver. Não é utilizado em executáveis. Também não é usado em todos os drivers NT. |
13 | IMAGE_FILE_DLL | Se o arquivo for uma DLL. |
14 | IMAGE_FILE_UP_SYSTEM_ONLY | Se o arquivo não tiver sido projetado para rodar em sistemas multiprocessados, ou seja, travará porque depende, de algum modo, de um único processador. |
15 | IMAGE_FILE_BYTES_REVERSED_HI | Veja Bit 7. |
Agora a coisa complicou um pouquinho. Precisamos "abrir" o hexadecimal de 16 bits destacado em rosa para sua notação binária e analisar cada um dos bits:
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
... | ||||||||||||||||
0000 00C0 | 50 | 45 | 00 | 00 | 4C | 01 | 04 | 00 | A3 | 77 | 55 | 3C | 00 | 00 | 00 | 00 |
0000 00D0 | 00 | 00 | 00 | 00 | E0 | 00 | 0F | 01 |
Transformando os valores hexadecimais em binários, temos o que precisamos. Observe que tanto os bytes quanto a numeração dos bits, para variar, precisa ser considerada no sentido inverso.
Hexa | 01 | 0F | ||||||||||||||
Binário | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
Bits | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Agora, de posse dos bits, podemos analisar a informação transmitida por cada um deles:
bit | binário | característica |
0 | 1 | Há informações de remanejamento |
1 | 1 | É um arquivo executável |
2 | 1 | A numeração de linhas foi eliminada |
3 | 1 | Há informações sobre símbolos locais |
4 | 0 | |
5 | 0 | |
6 | 0 | |
7 | 0 | O endian corresponde |
8 | 1 | É para rodar numa máquina de 32 bits |
9 | 0 | |
10 | 0 | |
11 | 0 | |
12 | 0 | |
13 | 0 | |
14 | 0 | |
15 | 0 | O endian corresponde |
Algumas considerações sobre RVA
RVA é o endereço virtual relativo. O formato PE faz uso intensivo dos assim chamados RVAs. Um RVA ("relative virtual address") é utilizado para definir um endereço de MEMÓRIA caso se desconheça o endereço base ("base address"). É o valor que, adicionado ao endereço base, fornece o endereço linear.
O endereço base é o endereço a partir do qual a imagem PE é carregada na memória e pode mudar de uma execução para outra. Por exemplo, rodando duas instâncias de um mesmo executável, cada um deles terá um endereço base diferente, ou seja, foram mapeados para localizações diferentes na memória. Outro exemplo: imagine um executável mapeado para o endereço 0x400000 e que a execução se inicie no RVA 0x1560. O endereço de memória que contém o início efetivo será 0x401560. Se o executável tivesse sido mapeado a partir de 0x100000, o início de execução estaria em 0x101560.
As coisas começam a ficar um pouco mais complexas porque algumas partes do arquivo PE (as seções) não estão necessariamente alinhadas da mesma forma que a imagem mapeada. Por exemplo, as seções do arquivo em disco geralmente estão alinhadas em limites de 512 bytes, enquanto que a imagem mapeada na memória provavelmente esteja alinhada em limites de 4096 bytes. Veja 'SectionAlignment' e 'FileAlignment' adiante.
Deste modo, para localizar um bloco de informações num arquivo PE para um RVA específico, há a necessidade de se calcular os deslocamentos (offsets) como se o arquivo estivesse mapeado, porém saltar de acordo com os deslocamentos do arquivo. Como exemplo, suponha que você saiba que a execução se inicia no RVA 0x1560 e pretende desassemblar o código a partir deste ponto. Para achar o endereço no arquivo (disco), você precisará descobrir que as seções na RAM estão alinhadas em 4096 bytes, que a seção ".code" começa no RVA 0x1000 da RAM e tem o comprimento de 16384 bytes. Só então você pode determinar que o RVA 0x1560 está deslocado (offset) 0x560 nesta seção. Agora, verificando também que as seções do arquivo em disco estão alinhadas no limite de 512 bytes e que a seção ".code" começa no deslocamento 0x800, com alguns cálculos, torna-se possível localizar o início da execução em disco: 0x800 + 0x560 = 0xD60.
Desassemblando, você encontra uma variável no endereço linear 0x1051D0. O endereço linear será remanejado após o mapeamento do executável e, supostamente, o endereço preferencial será usado. Você consegue determinar que o endereço preferencial é 0x100000, portanto, o RVA é 0x51D0. Isto ocorre na seção de dados que começa no RVA 0x5000 e tem 2048 bytes de comprimento. Ela tem seu início no deslocamento 0x4800 do arquivo em disco, portanto a variável pode ser encontrada no deslocamento 0x4800 + 0x51D0 - 0x5000 = 0x49D0.
:anota: Exercícios propostos
Para este módulo, tente o seguinte:
- Continue fuçando os executáveis que você encontrar no seu HD. Qualquer um serve, contanto que esteja no formato PE. Localize o cabeçalho do arquivo e analise principalmente o campo número de seções. Vai ser útil logo adiante.
- Faça alguns exercícios de transformação de hexa para binário. Se estiver com preguiça, use a calculadora do windows na opção "científica" - as trasnformações estão a um clique de mouse. Caso você queira se aprofundar mais no assunto, dê uma olhada em Sistemas de Notação.
- Tente desenhar um mapa de memória e um mapa de disco para um executável imaginário. Tente entender o que esse negócio de RVA tem a ver com isso