Ir para conteúdo

Por dentro do formato ELF - Parte 1


darkcrow

Posts Recomendados

Olá, reversers, esse é meu primeiro post no fórum. Chamo-me Mateus Gualberto, mais conhecido (ainda nem tão conhecido) por darkcrow em competições CTF pelo time RATF e membro do projeto de extensão Residência em Segurança da Informação (RSI) na Universidade Federal do Ceará.

Atenção! Apesar de ser um pequeno resumo de uma parte do formato ELF, é recomendado ao leitor ter conhecimentos prévios de engenharia reversa no geral, assim como termos como virtual address (endereço virtual), offsets, hexdump, arquitetura de computadores, etc.

Hoje venho falar um pouco acerca do Executable and Linkable Format, ou simplesmente formato ELF, muito utilizado por sistemas Linux/BSDs. Para começar, falando um pouco a respeito de extensões-padrão, geralmente o mesmo não utiliza extensões, mas é bem comum de vermos executáveis .elf, .bin e bibliotecas Linux como .so. Mas também venho lembrar-lhes que extensão é só uma parte do nome do arquivo, não influencia a estrutura do mesmo.

O formato ELF é dividido em:

  • Elf Header
  • Program Header Table
  • Seções/Segmentos (não são a mesma coisa, explicarei mais a frente essa diferença)
  • Section Header Table

Uma imagem para esclarecer melhor essa organização:

1200px-Elf-layout--en_svg.thumb.png.38d0517f148d724f23c62b2f4684cc63.png

Então esse é o "molde" que nossos programas para Linux irão se encaixar ao serem compilados com sucesso. Antes de prosseguirmos para uma parte mais pesada, vamos a um resumo básico da função de cada parte de um ELF:

  • Elf Header: É a estrutura inicial do ELF, que inicia no byte 0 de todo binário. Ela é responsável pela identificação do formato por ferramentas como o find, além de prover informações importantes sobre o binário, como arquitetura, offsets da Section Header Table e Program Header Table, tamanhos das mesmas e até mesmo o Entry Point do binário.
  • Program Header Table: Estrutura que estarão contidas outras estruturas, chamadas de Program Headers, que contêm informações necessárias para o carregamento de um executável, como a descrição de segmentos. Logo, é mandatório que um executável ELF contenha essa tabela, enquanto que arquivos-objeto ela é opcional.
  • Seções/Segmentos: Podemos pensar em seções como delimitações das partes dentro do formato ELF com um propósito, como armazenar código, dados ou dados read-only. Já os segmentos contém as seções, ou seja, as seções estão dentro de segmentos, e eles organizam as seções de acordo com o tipo das mesmas. Por exemplo: suponha que eu tenha .data e .data1, seções de dados em um binário. As mesmas ficarão no mesmo tipo de segmento, o segmento de dados. Além disso, segmentos são utilizados na execução de um executável, enquanto que seções por si só, não.
  • Section Header Table: Estrutura que guardará estruturas Section Header, que contém informações acerca das seções do binário analisado. Podemos extrair diversas informações da análise desses cabeçalhos, como nome das seções, tipo, tamanho, offset, entre outros dados.

Agora que você, leitor, já entendeu o básico de cada uma dessas partes, vamos para uma análise mais técnica, começando pelo Elf Header. Porém, não irei explicar cada opção de cada campo de cada estrutura, pois além disso ser maçante para a leitura, iria render um livro (rs).

Como explicado anteriormente, o Elf Header contém informações vitais do nosso binário, como localização das tabelas e do Entry Point. Segundo o manual do ELF (man elf), a struct em C que o forma é:
 

          #define EI_NIDENT 16

           typedef struct {
               unsigned char e_ident[EI_NIDENT]; // Identificação do arquivo e informações de ABI
               uint16_t      e_type; // Tipo do binário, se é um executável, uma lib, um arquivo-objeto
               uint16_t      e_machine; // Arquitetura para qual o binário foi compilado
               uint32_t      e_version; // Versão do arquivo
               ElfN_Addr     e_entry; // Entry Point
               ElfN_Off      e_phoff; // Offset da Program Header Table
               ElfN_Off      e_shoff; // Offset da Section Header Table
               uint32_t      e_flags; // Flags específicas do processador
               uint16_t      e_ehsize; // Tamanho, em bytes, do Elf Header
               uint16_t      e_phentsize; // Tamanho, em bytes, de uma entrada na Program Header Table
               uint16_t      e_phnum; // Número de entradas da Program Header Table
               uint16_t      e_shentsize; // Tamanho, em bytes, de uma entrada na Section Header Table
               uint16_t      e_shnum; // Número  de entradas da Section Header Table
               uint16_t      e_shstrndx; // Contém o indíce da entrada da tabela de nomes de seções na Section Header Table
           } ElfN_Ehdr;

 

Tentei resumir cada entrada para uma melhor leitura e entendimento, mas tenho que explicar algumas coisas:

  • Substitua o N nos campos/estruturas ElfN_ por 32 ou 64, dependendo da arquitetura do binário (isso vem do manual do ELF).
  • Sobre os campos:
    • e_ident: Array de 16 bytes que contém a identificação do arquivo ELF (\x7f E L F) e informações acerca de endianess e sobre a Application Binary Interface (https://en.wikipedia.org/wiki/Application_binary_interface).
    • e_type: define o tipo de arquivo, podendo ser um desses valores:
      • ET_NONE: Tipo desconhecido.
      • ET_REL: Arquivo-objeto, também chamado de relocável
      • ET_EXEC: Arquivo executável.
      • ET_DYN: lib, tamém conhecida como shared object.
      • ET_CORE: core file, arquivo que guarda informações sobre crashes em aplicações.
    • e_machine: Define a arquitetura do arquivo ELF. Valores comuns são:
      • EM_X86_64: amd64
      • EM_386: i386
    • e_entry: Contém o endereço virtual de onde o binário deverá ser executado. Geralmente apontará para o segmento de texto, que contém a seção .text, de onde começa a execução dos códigos.
    • e_phoff e e_shoff contêm offsets dentro do arquivo, ou seja, não são offsets em memória. Isso significa que eles apontam para onde começa suas respectivas tabelas dentro do arquivo, não usando um endereço virtual.
    • e_phentsize e e_shentsize se referem não ao tamanho da tabela, e sim de uma entrada de um header dela (um Program Header ou um Section Header). É necessário o tamanho de apenas um cabeçalho pois todas as entradas têm o mesmo tamanho.
    • e_shstrndx se refere a uma tabela de strings que é referenciada pela Section Header Table. A partir daí, você, leitor, consegue ver a imensidão de possibilidades que o formato ELF nos dá, visto que essa estrutura não foi nem citada anteriormente nem está na figura principal desse post. Assim como essa estrutura existem diversas outras que iremos estudar aos poucos nessa série sobre o formato ELF.

Dado as devidas explicações técnicas, podemos agora analisar um arquivo ELF usando uma das ferramentas mais úteis para análise binária nesse formato de arquivo: readelf, que faz parte do pacote binutils. Caso ele não esteja instalado ainda, e esteja utilizando uma distribuição baseada em Debian, só utilizar o comando:

sudo apt update -y && sudo apt install binutils

 

Vamos à prática! Compilei um binário escrito em C, um simples "Hello World!", compilei-o com o nome hello, com a opção -no-pie no gcc (explicarei em outro post a respeito disso, mas é para o compilador gerar um binário do tipo ET_EXEC) . Vamos ver como o readelf nos mostra o Elf Header:

readelf -h hello

Saída:

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400400
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6376 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

Como podemos ver, o readelf nos traz informações bem mais explícitas que um hexdump nos traria. Mas isso não quer dizer que devamos deixar de lado ferramentas do tipo hexdump, por isso deixarei um breve desafio para os leitores desse post. Tentem encontrar onde está o Elf Header e procurem assimilar as informações descritas acima só que no dump do mesmo arquivo a seguir. Caso sentirem-se à vontade, podem postar as respostas desse desafio aqui abaixo.

image.png.1167bb2c5995612786f632bb24c09d9d.png

 

Enfim, agradeço-os pela leitura e em breve irei continuar essa série. Dúvidas e sugestões podem ser postadas, irei olhar sempre que tiver tempo ?

[OFF] No mais, estou planejando produzir um curso de Engenharia Reversa/Análise de binários focada em Linux e no formato ELF. Estou pensando de postar no youtube para fortalecer a comunidade de engenharia reversa no Brasil. Minha grande inspiração para esse curso e essa grandiosa área foi o Fernando Mercês.

Link para o comentário
Compartilhar em outros sites

  • 2 meses depois...
  • 2 semanas depois...
Em 24/03/2020 em 18:01, unc4nny disse:

Opa! To atrasado aqui mano, mas eu nao sei se eu to viajando, mas qual a diferenca entre o layout da memoria virtual de um executavel e as secoes ELF? Eles parecem ser 2 coisas diferentes, mas parecem ser muito similares. Eu to viajando?

Elf-layout.png

unnamed.png

O endereçamento virtual definirá o layout da memória da imagem (ou processo) do binário quando o mesmo for executado. O range de memória definido irá abrigar os segmentos, como na foto que você postou, mas também conterá outras informações e regiões de memória, como a Stack, utilizada para variáveis locais e dados que tem uma alta volatilidade, e a memória alocável dinâmica. Segmentos definem uma região de memória com um fim específico, com suas próprias flags e permissões. Eles são construídos com base nas informações das seções de um ET_REL, no processo de linkagem de um executável ou shared object. Uma ou mais seções são utilizadas para a contrução um segmento, que definirá uma região de memória dentre o range estabelecido pelo layout de memória virtual. Em resumo: seções são apenas um componente para um segmento; um segmento define, entre outras informações, uma região de memória que estará no range do layout da memória virtual, que engloba vários outros dados.

Link para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...