Como espiar arquivos binários na linha de comando do Linux

Um terminal Linux estilizado com linhas de texto verde em um laptop.
fatmawati achmad zaenuri / Shutterstock

Tem um arquivo misterioso? O filecomando do Linux lhe dirá rapidamente que tipo de arquivo é. Se for um arquivo binário, você pode descobrir ainda mais sobre ele. filetem toda uma jangada de companheiros de estábulo que o ajudarão a analisá-lo. Mostraremos como usar algumas dessas ferramentas.

Identificando Tipos de Arquivo

Os arquivos geralmente têm características que permitem que os pacotes de software identifiquem o tipo de arquivo que eles representam, bem como os dados que eles 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 ID.

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 de um aspecto distinto da organização interna dos próprios dados, conhecido como arquitetura do 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 uma prova e olha dentro do arquivo para encontrá-la.

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 filecomando .

Usando o arquivo Command

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 lscomando nos mostrará o que há no diretório, e a opção -hl(tamanhos legíveis, lista longa) nos mostrará o tamanho de cada arquivo:

ls -hl

ls -hl em uma janela de terminal.

Vamos experimentar filealguns deles e ver o que temos:

arquivo build_instructions.odt
arquivo build_instructions.pdf
arquivo COBOL_Report_Apr60.djvu

arquivo build_instructions.odt em uma janela de terminal.

Os três formatos de arquivo são identificados corretamente. Sempre que possível, filedê-nos um pouco mais de informação. O arquivo PDF está no formato da  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 Filesnavegador de arquivos quanto na linha de comando usando file.

Arquivo OpenDocument identificado corretamente no navegador de arquivos Arquivos, embora sua extensão seja XYZ.

No Filesnavegador de arquivos, é fornecido o ícone correto. Na linha de comando,  fileignora a extensão e olha dentro do arquivo para determinar seu tipo:

arquivo build_instructions.xyz

arquivo build_instructions.xyz em uma janela de terminal.

O uso fileem mídia, como arquivos de imagem e música, geralmente produz informações sobre seu formato, codificação, resolução e assim por diante:

arquivo screenshot.png
arquivo screenshot.jpg
arquivo Pachelbel_Canon_In_D.mp3

arquivo screenshot.png em uma janela de terminal.

Curiosamente, mesmo com arquivos de texto simples, filenã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 o código-fonte,  file não o confunda com um arquivo de código-fonte C genuíno :

função de arquivo + cabeçalhos.h
arquivo makefile
arquivo hello.c

file function + headers.h em uma janela de terminal.

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.

Recomendado:  Como tirar uma captura de tela tocando na parte de trás do seu telefone Android

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, entretanto, são um desafio maior.

Por exemplo, os arquivos “hello” e “wd” são executáveis ​​binários. Eles são programas. O arquivo denominado “wd.o” é um arquivo objeto. Quando o código-fonte é compilado por um compilador, um ou mais arquivos-objeto são criados. Eles contêm o código de máquina que o computador executará eventualmente quando o programa concluído for executado, junto com informações para o vinculador. O vinculador verifica cada arquivo de objeto para chamadas de função para bibliotecas. Ele os vincula a qualquer biblioteca usada pelo programa. O resultado desse processo é um arquivo executável.

O arquivo “watch.exe” é um executável binário que foi compilado para ser executado no Windows:

arquivo wd
arquivo wd.o
arquivo olá
arquivo watch.exe

arquivo wd em uma janela de terminal.

Pegando o último primeiro, filediz-nos que o arquivo “watch.exe” é um executável PE32 +, programa de console, para a família x86 de processadores 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 identificados como arquivos Executable and Linkable Format (ELF). Este é um padrão para arquivos executáveis ​​e arquivos de objetos compartilhados, como bibliotecas. Veremos o 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  objetos compartilhados Linux Standard Base (LSB), 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 objeto são relocá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 sistema Address Space Layout Randomization   (ASMR) carregue os executáveis ​​na memória em endereços de sua escolha. Os executáveis ​​padrão têm um endereço de carregamento codificado em seus cabeçalhos, que determinam 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 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) posicionados em um endereço aleatório superam essa suscetibilidade.

Recomendado:  Como ver se seus amigos do Facebook estão seguros durante uma emergência

Se compilarmos nosso programa com o gcccompilador e fornecermos a -no-pieopção, geraremos um executável convencional.

A opção -o(arquivo de saída) nos permite fornecer um nome para o nosso executável:

gcc -o hello -no-pie hello.c

Usaremos  fileno novo executável e ver o que mudou:

arquivo olá

O tamanho do executável é o mesmo de antes (17 KB):

ls -hl olá

gcc -o hello -no-pie hello.c em uma janela de terminal.

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 helloprograma de exemplo  tem 17 KB, então dificilmente poderia ser chamado de grande, mas tudo é relativo. O código-fonte tem 120 bytes:

gato olá.c

O que está aumentando o binário se tudo o que ele faz é imprimir uma string na janela do terminal? Sabemos que há um cabeçalho ELF, mas ele tem apenas 64 bytes para um binário de 64 bits. Claramente, deve ser outra coisa:

ls -hl olá

cat hello.c em uma janela de terminal.

Vamos examinar o binário com o strings comando como uma primeira etapa simples para descobrir o que está dentro dele. Vamos canalizá-lo para less:

cordas olá | Menos

cordas olá |  menos em uma janela de terminal.

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 ligação de objetos compartilhados. Isso inclui as bibliotecas e funções dentro dessas bibliotecas, das quais o binário depende.

O lddcomando nos mostra as dependências de objetos compartilhados de um binário:

ldd olá

ldd hello em uma janela de terminal.

Existem três entradas na saída e duas delas incluem um caminho de diretório (a primeira não):

  • linux-vdso.so: Virtual Dynamic Shared Object (VDSO) é um mecanismo de 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. Isso evita a sobrecarga de uma mudança de contexto do modo kernel do usuário. Os objetos VDSO compartilhados aderem ao formato Executable and Linkable Format (ELF), permitindo que sejam vinculados dinamicamente ao binário no tempo de execução. O VDSO é alocado dinamicamente e tira proveito do ASMR. O recurso VDSO é fornecido pela Biblioteca GNU C padrão se o kernel suportar o esquema ASMR.
  • libc.so.6: O objeto compartilhado da Biblioteca GNU C.
  • /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 ser capaz de localizar e acessar as dependências na memória. Em seguida, ele inicia o programa.

O cabeçalho ELF

Podemos examinar e decodificar o cabeçalho ELF usando o readelfutilitário e a opção -h(cabeçalho do arquivo):

readelf -h olá

readelf -h oi em uma janela de terminal.

O cabeçalho é interpretado para nós.

Recomendado:  Qual Chromecast devo comprar (e devo atualizar meu antigo)?

Saída de readelf -hello em uma janela de terminal.

O primeiro byte de todos os binários ELF é definido com 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 bem 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 com seus bits menos significativos primeiro.
  • 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 da ABI.
  • Tipo: o tipo de binário ELF. Os valores comuns são ET_RELpara um recurso relocável (como um arquivo de objeto), ET_EXECpara um executável compilado com o -no-piesinalizador e ET_DYNpara 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 da 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 olhada rápida 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 -Copção (canônica) nos dá a representação ASCII dos bytes ao lado de seus valores hexadecimais, e a -nopção (número) nos permite especificar quantos bytes queremos ver:

hexdump -C -n 8 ola

hexdump -C -n 8 olá em uma janela de terminal.

objdump e a visão granular

Se você quiser ver os detalhes essenciais, você pode usar o  objdumpcomando com a -dopção (desmontar):

objdump -d hello | Menos

objdump -d hello |  menos em uma janela de terminal.

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 extremidade esquerda.

Isso só é útil se você conseguir ler a linguagem assembly ou se estiver curioso para saber o que se passa por trás da cortina. Há uma grande quantidade de resultados, então nós os direcionamos less.

Putput de objdump -d hello |  menos em uma janela de terminal.

Compilando e vinculando

Existem muitas maneiras de compilar um binário. Por exemplo, o desenvolvedor escolhe se deseja incluir informações de depuração. A forma como o binário é 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 aquele 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.