Encontrou um ficheiro de origem desconhecida? O comando `file` do Linux pode rapidamente identificar o tipo de ficheiro que tem em mãos. No entanto, se for um ficheiro binário, é possível descobrir ainda mais informações. O comando `file` dispõe de várias ferramentas auxiliares que ajudam na análise desses ficheiros. Vamos explorar algumas destas ferramentas.
Identificação de Tipos de Ficheiro
Normalmente, os ficheiros possuem características que permitem que o software identifique o tipo de ficheiro e o tipo de dados que contém. Seria inútil tentar abrir um ficheiro PNG num leitor de música MP3, por isso é essencial que um ficheiro contenha algum tipo de identificação.
Esta identificação pode ser uma assinatura de alguns bytes no início do ficheiro. Isso permite que o ficheiro declare explicitamente o seu formato e conteúdo. Por vezes, o tipo de ficheiro é deduzido através de um aspeto distintivo da sua organização interna, também conhecida como arquitetura de ficheiro.
Alguns sistemas operativos, como o Windows, baseiam-se principalmente na extensão de um ficheiro. O Windows assume que um ficheiro com a extensão DOCX é, de facto, um ficheiro de processamento de texto DOCX. O Linux opera de forma diferente. Ele procura provas dentro do ficheiro para determinar o seu tipo.
As ferramentas descritas aqui já estão instaladas nas distribuições Manjaro 20, Fedora 21 e Ubuntu 20.04, que utilizamos para a redação deste artigo. Vamos começar a nossa análise com o comando `file`.
Utilizando o Comando `file`
No diretório atual, temos vários tipos de ficheiros, como documentos, código-fonte, executáveis e ficheiros de texto.
O comando `ls` mostra o conteúdo do diretório, e a opção `-hl` (tamanhos legíveis por humanos, listagem longa) mostra o tamanho de cada ficheiro:
ls -hl
Vamos usar o comando `file` em alguns deles e verificar o resultado:
file build_instructions.odt
file build_instructions.pdf
file COBOL_Report_Apr60.djvu
Os três formatos de ficheiro foram identificados corretamente. Sempre que possível, o comando `file` fornece informações adicionais. O ficheiro PDF é apresentado como estando no formato versão 1.5.
Mesmo que renomeemos o ficheiro ODT para ter uma extensão com o valor arbitrário de XYZ, o comando `file` continua a identificá-lo corretamente, tal como o navegador de ficheiros “Ficheiros”.
O navegador “Ficheiros” apresenta o ícone correto. Na linha de comando, o comando `file` ignora a extensão e procura dentro do ficheiro para determinar o seu tipo:
file build_instructions.xyz
O comando `file` consegue extrair informações sobre o formato, codificação e resolução de ficheiros multimédia como imagens e músicas:
file screenshot.png
file screenshot.jpg
file Pachelbel_Canon_In_D.mp3
De forma interessante, mesmo com ficheiros de texto simples, o comando `file` não se baseia na sua extensão. Por exemplo, se tiver um ficheiro com a extensão “.c” que contém texto simples, mas não código-fonte, o comando `file` não o confundirá com um ficheiro de código-fonte C:
file function+headers.h
file makefile
file hello.c
O comando `file` identifica corretamente o ficheiro de cabeçalho (“.h”) como parte de um conjunto de ficheiros de código-fonte C e sabe que o ficheiro makefile é um script.
Analisando Ficheiros Binários com o Comando `file`
Os ficheiros binários são mais opacos do que outros tipos. Enquanto ficheiros de imagem podem ser visualizados, ficheiros de som podem ser reproduzidos, e ficheiros de documentos podem ser abertos com o software apropriado, os ficheiros binários apresentam um desafio maior.
Por exemplo, os ficheiros “hello” e “wd” são executáveis binários. São programas. O ficheiro “wd.o” é um ficheiro objeto. Quando o código-fonte é compilado, são criados um ou mais ficheiros objeto. Estes ficheiros contêm o código máquina que o computador executará, juntamente com informações para o ligador. O ligador verifica cada ficheiro objeto em busca de chamadas de função para bibliotecas e liga-as às bibliotecas que o programa utiliza. O resultado deste processo é um ficheiro executável.
O ficheiro “watch.exe” é um executável binário compilado para o Windows:
file wd
file wd.o
file hello
file watch.exe
O comando `file` revela que “watch.exe” é um programa de consola executável PE32+ para a família de processadores x86 no Microsoft Windows. PE significa Portable Executable, que tem versões de 32 e 64 bits. PE32 é a versão de 32 bits, e PE32+ é a versão de 64 bits.
Os outros três ficheiros são identificados como Executable and Linkable Format (ELF). Este é um padrão para ficheiros executáveis e ficheiros objeto partilhados, como bibliotecas. Vamos analisar o formato do cabeçalho ELF em breve.
Os dois executáveis (“wd” e “hello”) são identificados como objetos partilhados Linux Standard Base (LSB), enquanto o ficheiro objeto “wd.o” é identificado como um LSB relocável. A palavra “executável” não aparece.
Os ficheiros objeto são realocáveis, o que significa que o código que contêm pode ser carregado na memória em qualquer local. Os executáveis são listados como objetos partilhados porque foram criados pelo ligador de forma a herdarem esta funcionalidade.
Isto permite que o Address Space Layout Randomization (ASMR) carregue executáveis na memória em endereços aleatórios. Executáveis padrão têm um endereço de carregamento codificado no cabeçalho, que indica o local onde são carregados na memória.
ASMR é uma técnica de segurança. Carregar executáveis na memória em endereços previsíveis torna-os suscetíveis a ataques. Isto acontece porque os seus pontos de entrada e localizações de funções serão sempre conhecidos por atacantes. Position Independent Executables (PIE), localizados num endereço aleatório, mitigam esta vulnerabilidade.
Se compilarmos o nosso programa com o compilador gcc e usarmos a opção `-no-pie`, geraremos um executável convencional.
A opção `-o` (ficheiro de saída) permite-nos dar um nome ao nosso executável:
gcc -o hello -no-pie hello.c
Vamos verificar o novo executável com o comando `file` e ver o que mudou:
file hello
O tamanho do executável é o mesmo (17 KB):
ls -hl hello
O ficheiro binário agora é identificado como um executável padrão. Isto é apenas para fins demonstrativos. Se compilar aplicações desta forma, perderá as vantagens do ASMR.
O Porquê do Tamanho de um Executável
O nosso exemplo de programa “hello” tem 17 KB, o que pode não parecer muito, mas tudo é relativo. O código-fonte tem 120 bytes:
cat hello.c
O que torna o ficheiro binário tão grande se tudo o que faz é imprimir uma string na janela do terminal? Sabemos que existe um cabeçalho ELF, mas este tem apenas 64 bytes para um binário de 64 bits. Portanto, deve haver algo mais:
ls -hl hello
Vamos usar o comando `strings` para analisar o ficheiro binário como primeiro passo para descobrir o seu conteúdo. Vamos encaminhar o resultado para o `less`:
strings hello | less
Existem muitas strings dentro do binário, além de “Hello, Geek world!”, que está no nosso código-fonte. A maioria são rótulos para regiões dentro do binário, bem como nomes e informações de ligação de objetos partilhados. Isto inclui as bibliotecas e as funções dentro destas bibliotecas das quais o binário depende.
O comando `ldd` mostra as dependências de objetos partilhados de um ficheiro binário:
ldd hello
Existem três entradas no resultado, duas das quais incluem um caminho de diretório (a primeira não):
`linux-vdso.so`: Virtual Dynamic Shared Object (VDSO) é um mecanismo do kernel que permite o acesso a um conjunto de rotinas do espaço do kernel por um binário do espaço do utilizador. Isto evita o overhead de uma troca de contexto do modo kernel para o modo utilizador. Os objetos partilhados VDSO seguem o formato Executable and Linkable Format (ELF), o que permite que sejam ligados dinamicamente ao binário em tempo de execução. O VDSO é alocado dinamicamente e aproveita o ASMR. A funcionalidade VDSO é fornecida pela GNU C Library se o kernel suportar o esquema ASMR.
`libc.so.6`: O objeto partilhado da GNU C Library.
`/lib64/ld-linux-x86-64.so.2`: Este é o ligador dinâmico que o binário usa. O ligador dinâmico consulta o ficheiro binário para descobrir as suas dependências. Em seguida, carrega esses objetos partilhados na memória. Ele prepara o ficheiro binário para ser executado e para localizar e aceder às suas dependências na memória. Por fim, inicia o programa.
O Cabeçalho ELF
Podemos examinar e decifrar o cabeçalho ELF com o utilitário `readelf` e a opção `-h` (cabeçalho do ficheiro):
readelf -h hello
O cabeçalho é interpretado para nós.
O primeiro byte de todos os binários ELF tem o valor hexadecimal 0x7F. Os três bytes seguintes têm os valores 0x45, 0x4C e 0x46. O primeiro byte é um sinalizador que identifica o ficheiro como um binário ELF. Os três bytes seguintes formam a palavra “ELF” em ASCII.
`Classe`: Indica se o ficheiro binário é um executável de 32 ou 64 bits (1=32, 2=64).
`Dados`: Indica o endianness em uso. A codificação endian define como os números multibyte são armazenados. Na codificação big-endian, um número é armazenado com os bits mais significativos primeiro. Na codificação little-endian, o número é armazenado com os bits menos significativos primeiro.
`Versão`: A versão do ELF (atualmente, é 1).
`OS/ABI`: Representa o tipo de Application Binary Interface (ABI) em uso. A ABI define a interface entre dois módulos binários, como um programa e uma biblioteca partilhada.
`Versão ABI`: A versão do ABI.
`Tipo`: O tipo de binário ELF. Os valores comuns são `ET_REL` para um recurso realocável (como um ficheiro objeto), `ET_EXEC` para um executável compilado com o sinalizador `-no-pie` e `ET_DYN` para um executável compatível com ASMR.
`Máquina`: A arquitetura do conjunto de instruções. Isto indica a plataforma para a qual o ficheiro binário foi criado.
`Versão`: Sempre definido como 1 para esta versão do ELF.
`Endereço do Ponto de Entrada`: O endereço de memória dentro do binário onde a execução começa.
As outras entradas são os tamanhos e números de regiões e secções dentro do ficheiro binário, para que as suas localizações possam ser calculadas.
Uma análise rápida dos primeiros oito bytes do ficheiro binário com o comando `hexdump` mostra o byte de assinatura e a string “ELF” nos primeiros quatro bytes. A opção `-C` (canónica) fornece a representação ASCII dos bytes ao lado dos seus valores hexadecimais, e a opção `-n` (número) permite especificar o número de bytes que queremos ver:
hexdump -C -n 8 hello
`objdump` para uma Visualização Detalhada
Para visualizar os detalhes, pode usar o comando `objdump` com a opção `-d` (desmontar):
objdump -d hello | less
Isto desmonta o código máquina executável e apresenta-o em bytes hexadecimais ao lado do equivalente em linguagem assembly. A localização do endereço do primeiro byte em cada linha é mostrada no extremo esquerdo.
Esta ferramenta é útil para quem consegue ler linguagem assembly ou tem curiosidade sobre o que acontece “por detrás dos panos”. O resultado é extenso, por isso usamos o `less` para facilitar a leitura.
Compilação e Ligação
Há diversas formas de compilar um ficheiro binário. Por exemplo, o programador pode escolher se pretende incluir informações de depuração. A forma como o ficheiro binário é ligado também influencia o seu conteúdo e tamanho. Se o binário tiver dependências externas, será menor do que um que contenha as dependências ligadas estaticamente.
A maioria dos programadores já conhece os comandos que abordamos neste artigo. No entanto, estas ferramentas oferecem formas simples de explorar e compreender o conteúdo de um ficheiro binário.