Tem um arquivo misterioso? O comando de arquivo do Linux informará rapidamente que tipo de arquivo é. Se for um arquivo binário, porém, você pode descobrir ainda mais sobre ele. arquivo tem toda uma série de stablemates que irão ajudá-lo a analisá-lo. Mostraremos como usar algumas dessas ferramentas.
últimas postagens
Identificando tipos de arquivo
Os arquivos geralmente têm características que permitem que os pacotes de software identifiquem que tipo de arquivo é, bem como o que os dados dentro dele representam. Não faria sentido tentar abrir um arquivo PNG em um reprodutor de música MP3, então é útil e pragmático que um arquivo carregue consigo alguma forma de identificação.
Isso pode ser alguns bytes de assinatura no início do arquivo. Isso permite que um arquivo seja explícito sobre seu formato e conteúdo. Às vezes, o tipo de arquivo é inferido a partir de um aspecto distinto da própria organização interna dos dados, conhecida como arquitetura de arquivo.
Alguns sistemas operacionais, como o Windows, são totalmente guiados pela extensão de um arquivo. Você pode chamá-lo de crédulo ou confiável, mas o Windows assume que qualquer arquivo com a extensão DOCX é realmente um arquivo de processamento de texto DOCX. O Linux não é assim, como você verá em breve. Ele quer provas e procura dentro do arquivo para encontrá-lo.
As ferramentas descritas aqui já foram instaladas nas distribuições Manjaro 20, Fedora 21 e Ubuntu 20.04 que usamos para pesquisar este artigo. Vamos começar nossa investigação usando o comando do arquivo.
Usando o comando de arquivo
Temos uma coleção de diferentes tipos de arquivos em nosso diretório atual. Eles são uma mistura de documentos, código-fonte, executáveis e arquivos de texto.
O comando ls nos mostrará o que está no diretório, e a opção -hl (tamanhos legíveis por humanos, listagem longa) nos mostrará o tamanho de cada arquivo:
ls -hl
Vamos tentar arquivar alguns deles e ver o que obtemos:
file build_instructions.odt
file build_instructions.pdf
file COBOL_Report_Apr60.djvu
Os três formatos de arquivo estão identificados corretamente. Sempre que possível, o arquivo nos dá um pouco mais de informação. O arquivo PDF é relatado para estar no formato versão 1.5.
Mesmo se renomearmos o arquivo ODT para ter uma extensão com o valor arbitrário de XYZ, o arquivo ainda será identificado corretamente, tanto no navegador de arquivos Arquivos quanto na linha de comando usando file.
No navegador de arquivos Arquivos, é fornecido o ícone correto. Na linha de comando, o arquivo ignora a extensão e procura dentro do arquivo para determinar seu tipo:
file build_instructions.xyz
O uso de arquivo em mídia, como arquivos de imagem e música, geralmente produz informações sobre seu formato, codificação, resolução e assim por diante:
file screenshot.png
file screenshot.jpg
file Pachelbel_Canon_In_D.mp3
Curiosamente, mesmo com arquivos de texto simples, o arquivo não julga o arquivo por sua extensão. Por exemplo, se você tiver um arquivo com a extensão “.c”, contendo texto simples padrão, mas não código-fonte, o arquivo não o confundirá com um C genuíno arquivo de código fonte:
file function+headers.h
file makefile
file hello.c
file identifica corretamente o arquivo de cabeçalho (“.h”) como parte de uma coleção de arquivos de código-fonte C e sabe que o makefile é um script.
Usando arquivo com arquivos binários
Arquivos binários são mais uma “caixa preta” do que outros. Arquivos de imagem podem ser visualizados, arquivos de som podem ser reproduzidos e arquivos de documentos podem ser abertos pelo pacote de software apropriado. Arquivos binários, no entanto, são mais um desafio.
Por exemplo, os arquivos “hello” e “wd” são executáveis binários. São programas. O arquivo chamado “wd.o” é um arquivo objeto. Quando o código-fonte é compilado por um compilador, um ou mais arquivos de objeto são criados. Eles contêm o código de máquina que o computador eventualmente executará quando o programa finalizado for executado, juntamente com informações para o vinculador. O vinculador verifica cada arquivo de objeto para chamadas de função para bibliotecas. Ele os vincula a quaisquer bibliotecas que o programa use. O resultado deste processo é um arquivo executável.
O arquivo “watch.exe” é um executável binário que foi compilado para rodar no Windows:
file wd
file wd.o
file hello
file watch.exe
Tomando o último primeiro, o arquivo nos diz que o arquivo “watch.exe” é um programa de console executável PE32+ para a família de processadores x86 no Microsoft Windows. PE significa formato executável portátil, que tem versões de 32 e 64 bits. O PE32 é a versão de 32 bits e o PE32+ é a versão de 64 bits.
Os outros três arquivos são todos identificados como Formato executável e vinculável (ELF). Este é um padrão para arquivos executáveis e arquivos de objetos compartilhados, como bibliotecas. Daremos uma olhada no formato do cabeçalho ELF em breve.
O que pode chamar sua atenção é que os dois executáveis (“wd” e “hello”) são identificados como Base Padrão Linux (LSB) objetos compartilhados, e o arquivo de objeto “wd.o” é identificado como um LSB relocável. A palavra executável é óbvia em sua ausência.
Os arquivos de objetos são realocáveis, o que significa que o código dentro deles pode ser carregado na memória em qualquer local. Os executáveis são listados como objetos compartilhados porque foram criados pelo vinculador a partir dos arquivos de objeto de forma que herdam esse recurso.
Isso permite que o Randomização do layout do espaço de endereço (ASMR) para carregar os executáveis na memória em endereços de sua escolha. Executáveis padrão têm um endereço de carregamento codificado em seus cabeçalhos, que ditam onde eles são carregados na memória.
ASMR é uma técnica de segurança. Carregar executáveis na memória em endereços previsíveis os torna suscetíveis a ataques. Isso ocorre porque seus pontos de entrada e os locais de suas funções sempre serão conhecidos pelos invasores. Executáveis Independentes de Posição (PIE) posicionado em um endereço aleatório superam essa suscetibilidade.
Se nós compilar nosso programa com o compilador gcc e fornecer a opção -no-pie, geraremos um executável convencional.
A opção -o (arquivo de saída) nos permite fornecer um nome para nosso executável:
gcc -o hello -no-pie hello.c
Usaremos o arquivo no novo executável e veremos o que mudou:
file hello
O tamanho do executável é o mesmo de antes (17 KB):
ls -hl hello
O binário agora é identificado como um executável padrão. Estamos fazendo isso apenas para fins de demonstração. Se você compilar aplicativos dessa maneira, perderá todas as vantagens do ASMR.
Por que um executável é tão grande?
Nosso exemplo de programa hello tem 17 KB, então dificilmente poderia ser chamado de grande, mas tudo é relativo. O código fonte é de 120 bytes:
cat hello.c
O que está aumentando o binário se tudo o que ele faz é imprimir uma string na janela do terminal? Sabemos que existe um cabeçalho ELF, mas tem apenas 64 bytes para um binário de 64 bits. Claramente, deve ser outra coisa:
ls -hl hello
Vamos escaneie o binário com o strings como um primeiro passo simples para descobrir o que está dentro dele. Vamos canalizá-lo em menos:
strings hello | less
Existem muitas strings dentro do binário, além do “Hello, Geek world!” do nosso código-fonte. A maioria deles são rótulos para regiões dentro do binário e os nomes e informações de vinculação de objetos compartilhados. Isso inclui as bibliotecas e funções dentro dessas bibliotecas, das quais o binário depende.
O comando ldd nos mostra as dependências de objetos compartilhados de um binário:
ldd hello
Existem três entradas na saída e duas delas incluem um caminho de diretório (o primeiro não):
linux-vdso.so: Objeto Compartilhado Dinâmico Virtual (VDSO) é um mecanismo do kernel que permite que um conjunto de rotinas do espaço do kernel seja acessado por um binário do espaço do usuário. Isto evita a sobrecarga de uma troca de contexto do modo kernel do usuário. Os objetos compartilhados VDSO aderem ao formato Executable and Linkable Format (ELF), permitindo que sejam vinculados dinamicamente ao binário em tempo de execução. O VDSO é alocado dinamicamente e aproveita o ASMR. A capacidade VDSO é fornecida pelo padrão Biblioteca GNU C se o kernel suporta o esquema ASMR.
libc.so.6: O Biblioteca GNU C objeto compartilhado.
/lib64/ld-linux-x86-64.so.2: Este é o vinculador dinâmico que o binário deseja usar. O vinculador dinâmico interroga o binário para descobrir quais dependências ele possui. Ele lança esses objetos compartilhados na memória. Ele prepara o binário para ser executado e capaz de localizar e acessar as dependências na memória. Em seguida, ele inicia o programa.
O cabeçalho ELF
Podemos examine e decodifique o cabeçalho ELF usando o utilitário readelf e a opção -h (cabeçalho do arquivo):
readelf -h hello
O cabeçalho é interpretado para nós.
O primeiro byte de todos os binários ELF é definido para o valor hexadecimal 0x7F. Os próximos três bytes são definidos como 0x45, 0x4C e 0x46. O primeiro byte é um sinalizador que identifica o arquivo como um binário ELF. Para deixar isso claro, os próximos três bytes soletram “ELF” em ASCII:
Classe: Indica se o 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 a maneira como os números multibyte são armazenados. Na codificação big-endian, um número é armazenado com seus bits mais significativos primeiro. Na codificação little-endian, o número é armazenado primeiro com seus bits menos significativos.
Versão: A versão do ELF (atualmente, é 1).
OS/ABI: Representa o tipo de interface binária do aplicativo em uso. Isso define a interface entre dois módulos binários, como um programa e uma biblioteca compartilhada.
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 arquivo de 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. Isso indica a plataforma de destino para a qual o 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 no qual a execução começa.
As outras entradas são tamanhos e números de regiões e seções dentro do binário para que suas localizações possam ser calculadas.
Uma rápida olhada nos primeiros oito bytes do binário com hexdump mostrará o byte de assinatura e a string “ELF” nos primeiros quatro bytes do arquivo. A opção -C (canônico) nos dá a representação ASCII dos bytes ao lado de seus valores hexadecimais, e a opção -n (número) nos permite especificar quantos bytes queremos ver:
hexdump -C -n 8 hello
objdump e a visualização granular
Se você quiser ver os detalhes, você pode usar o comando objdump com a opção -d (desmontar):
objdump -d hello | less
Isso desmonta o código de máquina executável e o exibe em bytes hexadecimais ao lado do equivalente em linguagem assembly. A localização do endereço do primeiro bye em cada linha é mostrada na extrema esquerda.
Isso só é útil se você puder ler linguagem assembly ou estiver curioso sobre o que acontece por trás da cortina. Há uma grande quantidade de saída, então canalizamos em menos.
Compilando e Vinculando
Há muitas maneiras de compilar um binário. Por exemplo, o desenvolvedor escolhe se deseja incluir informações de depuração. A maneira como o binário está vinculado também desempenha um papel em seu conteúdo e tamanho. Se as referências binárias compartilharem objetos como dependências externas, será menor do que um ao qual as dependências se vinculam estaticamente.
A maioria dos desenvolvedores já conhece os comandos que abordamos aqui. Para outros, porém, eles oferecem algumas maneiras fáceis de vasculhar e ver o que está dentro da caixa preta binária.