Não há dúvida de que você já se deparou com diversos arquivos, mas será que já pensou numa definição para eles? Defino arquivo como uma sequência de bytes armazenada numa mídia digital somados a uma entrada, um registro, no sistema de arquivos (filesystem) que os referencie. Vou tentar provar minha definição para você. Faça o seguinte teste: abra o Bloco de Notas, escreva "mentebinaria.com.br" (sem aspas) e salve num arquivo chamadoarquivo.txt
.
Se nosso estudo sobre strings estiver correto, este arquivo deve possuir 19 bytes de tamanho.
Agora vamos verificar o conteúdo deste arquivo. Abra-o num editor hexadecimal. O conteúdo deve consistir apenas dos seguintes bytes:
O conteúdo exibido é exatamente a string "mentebinaria.com.br" em ASCII. Conferindo com Python, temos:
Ou seja, se o arquivo tem apenas 19 bytes, que são os codepoints referentes aos caracteres da string, onde ficam armazenados seu nome, extensão, permissões, data e hora de criação, e todos os outros dados que não são o conteúdo, ou seja, os metadados do arquivo? Só pode ser em outro lugar no filesystem né?
De fato, nos sistemas de arquivos modernos, os arquivos só armazenam seu conteúdo. Na prática, as referências a eles é que definem onde começam e onde terminam um arquivo.
A pergunta mais interessante para nós é, no entanto, em relação ao tipo de arquivo. Criamo o arquivo.txt
com a extensão .txt
, mas é bom lembrar que uma extensão de arquivo nada mais é que parte de seu nome e não mantém nenhuma relação com seu tipo real. A única forma de saber um tipo de arquivo é inferindo este tipo através de seu conteúdo. Ao olhar para o arquivo no editor hexadecimal, vimos que todos os bytes do arquivo.txt
pertencem à faixa de codepoints da tabela ASCII, por isso podemos inferir que este é um arquivo de texto ASCII.
Claro que há maneiras mais práticas de se identificar o tipo de arquivo do que inspecionando seus bytes um a um. No Windows, podemos utilizar softwares como o DIE. Ele possui uma base de assinaturas para reconhecer os bytes de um arquivo e inferir seu tipo. Outros exemplos incluem o TrID (Windows) e o file/libmagic (GNU/Linux).
Veremos agora como trabalhar com arquivos mais complexos que os arquivos de texto.
Os arquivos de texto, independentemente da codificação (ASCII, ISO-8859-1, UTF-8, etc) são tipos de arquivos bem simples. Começaremos agora a avaliar tipos de arquivos mais complexos. Acontece que para um programa salvar ou abrir um determinado tipo de arquivo, é essencial que este conheça seu formato. Tomemos por exemplo o formato GIF. Para inspecionar seu conteúdo, utilizaremos, no Windows, um editor hexadecimal gráfico chamado HxD.
Analise o seguinte arquivo GIF aberto no HxD:
O HxD é um editor hexadecimal. Para entender estes editores funcionam, vamos começar pelas colunas onde os bytes estão organizados.
Também chamado de offset no arquivo ou offset em disco, esta coluna exibe a posição do conteúdo no arquivo, em bytes. Na imagem, o primeiro byte (no offset 0) é o 0x47. O segundo é o 0x49 e assim por diante. Já o byte no offset 0x10 é o 0x06.
Identifique na imagem as afirmações a seguir:
O byte no offset 0x53 é o 0xc8.
O byte no offset 0x7c é 0xc0.
O byte no offset 0x90 é 0x6c.
A próxima coluna exibe os bytes em si. Sem segredos. Por serem editores hexadecimais, programas como o HxD exibem o conteúdo do arquivo em bytes hexadecimais separados por espaços, mas é importante lembrar que o conteúdo de um arquivo é uma sequência de bits em disco ou em outro dispositivo de armazenamento que, quando aberto num editor, tem seu conteúdo copiado para a memória. A maneira como estes bytes serão visualizados fica a cargo do programa que o abre. Por exemplo, se o editor exibisse os bytes em decimal, os primeiros dois bytes (0x47 e 0x49) seriam 71 e 73. Se fosse em binário, seria 1000111 e 1001001.
A esta altura o leitor já deve ter percebido que um número pode ser expresso de várias maneiras, no entanto, o sistema hexadecimal é bem otimizado para fins de engenheiria reversa.
Na terceira coluna o HxD nos apresenta a interpretação textual em ASCII de cada byte exibido na segunda coluna, ou seja, se o byte em questão estiver dentro da faixa de caracteres ASCII imprimíveis (de 0x20 à 0x7e), sua representação é exibida. Para valores fora desta faixa, o HxD imprime um ponto.
Há dezenas de outros editores hexadecimais disponíveis, inclusive alguns visualizadores de linha de comando. Vale consultar o apêndice Ferramentas e testar alguns. Se você tiver curiosidade de saber como funciona um visualizador hexadecimal, recomendo olhar o código do hdump, disponível em https://github.com/merces/hdump, um visualizador para linha de comando que implementei em C para funcionar em Windows, Linux e macOS.
De volta ao formato, é importante ressaltar que tanto quem programa o software que salva um determinado tipo de arquivo quanto quem programa o software que visualiza tal formato precisa conhecê-lo bem. Vejamos então como o formato GIF é definido.
Em geral, os formatos são definidos por campos (faixas de bytes) de tamanho fixo ou variável, que podem assumir determinados valores. Para entendê-los, precisamos da documentação deste formato (no caso, do GIF) que está disponível livremente na internet. Conforme sua especificação, o formato GIF segue, dentre outras, as seguintes regras:
Seguindo esta tabela fornecida por quem desenhou o formato GIF e olhando o conteúdo do arquivo de exemplo na imagem anterior, podemos verificar que o primeiro campo, de 6 bytes, casa exatamente com o que está definido no padrão. Os bytes são a sequência 0x47, 0x49, 0x46, 0x38, 0x39 e 0x61 que representam a sequência em ASCII GIF89a. É bem comum ao definir formatos de arquivo que o primeiro campo, normalmente chamado de cabeçalho (header) ou número mágico (magic number) admita como valor uma representação ASCII que dê alguma indicação de que tipo de arquivo se trata. Por exemplo, os tipos de arquivo ZIP possuem o magic number equivalente ao texto PK. Já o tipo de arquivo RAR começa com os bytes equivalentes ao texto Rar!. Não é uma regra, mas é comum.
No exemplo do formato GIF o tamanho do primeiro campo é de 6 bytes, mas nem todo magic number possui este tamanho. Na verdade, não há regra.
Logo após o primeiro campo, temos o segundo campo, que define a largura em pixels da imagem GIF, segundo sua documentação. Este campo possui 2 bytes e, na imagem de exemplo, são os bytes 0x30 e 0x00. Aqui cabe explicar um conceito valioso que é o endianess. Acontece que na arquitetura Intel os bytes de um número inteiro são armazenados de trás para frente (chamado de little endian). Sendo assim a leitura correta da largura em pixels deste GIF é 0x0030, ou simplesmente 0x30 (já que zero à esquerda não conta), que é 48 em decimal.
O próximo campo, também de 2 bytes, diz respeito a altura em pixels da imagem GIF e também possui o valor 0x30 (já lendo os bytes de trás para frente conforme explicado). É correto dizer então que esta é uma imagem de 48 x 48 pixels.
É por isso que alguns sistemas operacionais, com o GNU/Linux, não consideram a extensão de arquivo como sendo algo importante para definir seu tipo. Na verdade, o conteúdo do arquivo o define.
Não seguiremos com toda a interpretação do formato GIF pois este foge ao escopo de estudo de engenharia reversa, mas vamos seguir a mesma lógica para entender o formato de arquivos executáveis do sistema Windows, objeto de estudo do próximo capítulo.
Byte offset (posição no arquivo) | Tamanho do campo em bytes | Valor em hexadecimal | Descrição |
---|---|---|---|
0
6
47 49 46 38 39 61
Cabeçalho
6
2
<variável>
Largura em pixels
8
2
<variável>
Altura em pixels