Cuidado: contém programação do sistema. Sim, em essência, ele não contém mais nada.
Vamos imaginar que você recebeu a tarefa de escrever um jogo de fantasia e fantasia. Bem, há sobre os elfos. E sobre a realidade virtual. Desde a infância, você sonhava em escrever algo assim e, sem hesitar, concorda. Logo você percebe que conhece a maior parte do mundo dos elfos a partir de piadas do velho bashorgh e de outras fontes díspares. Opa, um problema. Bem, onde a nossa não desapareceu ... Ensinado por uma rica experiência em programação, você acessa o Google, insere a "especificação Elf" e segue os links. Oh! Este leva a algum tipo de PDF ... então o que temos aqui ... algum tipo de Elf32_Sword
- elven swords - parece o que você precisa. 32 é aparentemente o nível do personagem, e os dois quatros nas colunas a seguir provavelmente são danos. Exatamente o que você precisa, e além de como sistematizado! ..
Conforme declarado em uma tarefa de programação da Olimpíada, após alguns parágrafos de um texto detalhado sobre o tema do Japão, samurai e gueixa: "Como você já entendeu, a tarefa não será sobre isso". Ah, sim, o concurso durou, é claro, por um tempo. Em geral, declaro encerrados cinco minutos de tenacidade.
Hoje vou tentar falar sobre a análise de um arquivo no formato ELF de 64 bits. Em princípio, o que eles simplesmente não armazenam são programas nativos, bibliotecas estáticas, bibliotecas dinâmicas, todas as implementações específicas, como crashdumps ... É usado, por exemplo, no Linux e em muitos outros sistemas semelhantes ao Unix, sim, dizem, mesmo em telefones seu suporte estava ativamente empacotado em firmware corrigido antes. Parece que o suporte ao formato para armazenar programas de sistemas operacionais sérios deve ser difícil. Então eu pensei. Sim, provavelmente é. Mas daremos suporte a um caso de uso muito específico: carregar o bytecode do eBPF a partir de arquivos .o
. Porque Apenas para experimentos futuros, precisarei de um bytecode sério de plataforma cruzada (que não seja na altura do joelho ), que pode ser obtido de C em vez de escrito manualmente, para que o eBPF seja simples e exista um back-end do LLVM. E eu só preciso analisar o ELF como um contêiner no qual esse bytecode é colocado pelo compilador.
Só para esclarecer, o artigo é de programação exploratória e não pretende ser um guia completo. O objetivo final é criar um gerenciador de inicialização que permita a leitura de programas C compilados no eBPF usando o Clang - os que eu tenho - em um volume suficiente para continuar os experimentos.
Manchete
A partir do deslocamento zero no ELF está o cabeçalho. Ele contém as próprias letras E, L, F, que podem ser vistas se você tentar abri-lo com um editor de texto e algumas variáveis globais. Na verdade, o cabeçalho é a única estrutura no arquivo localizada em um deslocamento fixo e contém informações para encontrar o restante da estrutura. (A seguir, sou guiado pela documentação para o formato de 32 bits e elf.h
, que conhece 64 bits. Portanto, se você perceber erros, sinta-se à vontade para corrigi-los)
A primeira coisa que nos encontra no arquivo é o unsigned char e_ident[16]
. Lembra-se desses artigos divertidos da série "todas as declarações a seguir são falsas"? Aqui está o mesmo: o ELF pode conter código de 32 ou 64 bits, Little ou Big Endian e até uma dúzia de arquiteturas de processador. Você o lerá como Elf64 em Little endian - bem, boa sorte ... Esse conjunto de bytes é um tipo de assinatura do que está dentro e de como analisá-lo.
Com os primeiros quatro bytes, tudo é simples - é [0x7f, 'E', 'L', 'F']
. Se eles não combinam, há motivos para acreditar que são algum tipo de abelha errada. O próximo byte contém a classe personagem Arquivo: ELFCLASS32
ou ELFCLASS64
- profundidade de bits. Para simplificar, trabalharemos apenas com arquivos de 64 bits (existe um eBPF de 32 bits?). Se a classe acabou sendo ELFCLASS32
, simplesmente saímos com um erro: mesmo assim, as estruturas “flutuam” e a verificação de sanidade não prejudica. O último byte de interesse para nós nessa estrutura indica a persistência do arquivo - trabalharemos apenas com a ordem de bytes nativa do nosso processador.
Apenas esclarecendo: ao trabalhar com o formato ELF em C, você não deve subtrair todos os int pelo deslocamento calculado de maneira inteligente - elf.h
contém as estruturas necessárias e até números de bytes no e_ident
: EI_MAG0
, EI_MAG1
, EI_MAG2
, EI_MAG3
, EI_CLASS
, EI_DATA
... ponteiro para os dados lidos ou mapeados na memória do arquivo para o ponteiro da estrutura e leitura.
Além do e_ident
cabeçalho contém outros campos, alguns apenas verificaremos e outros serão usados para análises posteriores, mas posteriormente. Ou seja, verificamos se e_machine == EM_BPF
(ou seja, está "sob a arquitetura do processador eBPF"), e_type == ET_REL
, e_shoff != 0
. A última verificação tem o seguinte significado: um arquivo pode conter informações para vincular (tabela e seções de seção), para iniciar (tabela e segmentos de programa) ou ambos. Com as duas últimas verificações, verificamos se as informações de que precisamos (como se fossem links) estão no arquivo. Verifique também se a versão do formato é EV_CURRENT
.
Faça uma reserva imediatamente, não irei verificar a validade do arquivo, assumindo que, se o carregarmos em nosso processo, confiaremos nele. No código do kernel ou de outros programas que trabalham com arquivos não confiáveis, é naturalmente impossível fazer isso em qualquer caso .
Tabela de seção
Como eu disse, estamos interessados na visualização de vinculação do arquivo, ou seja, na tabela de seções e nas próprias seções. As informações sobre onde procurar a tabela de seção estão no cabeçalho. Seu tamanho também é indicado lá, assim como o tamanho de um elemento - pode ser maior que sizeof(Elf64_Shdr)
(pois afetará o número da versão do formato, sinceramente não sei). Alguns números de seção principais são reservados e, na verdade, não estão presentes na tabela. Referenciá-los tem um significado especial. Aparentemente, estamos interessados apenas em SHN_UNDEF
(zero também é reservado - a seção que falta); a propósito, como você sabe, o título na tabela ainda está lá) SHN_ABS
. O caractere "definido na seção SHN_UNDEF
" é realmente indefinido e, em SHN_ABS
ele realmente possui um valor absoluto e não é realocado. No entanto, SHN_ABS
também não parece ser SHN_ABS
mim.
Tabela de linhas
Aqui nos deparamos pela primeira vez com tabelas de strings - tabelas de strings usadas em um arquivo. De fato, se const char *strtab
for uma tabela de strings, o nome sh_name
será apenas strtab + sh_name
. Sim, é apenas uma linha que começa com um determinado índice e continua a zero byte. As linhas podem se cruzar (mais precisamente, uma pode ser o sufixo da outra). As seções podem ter nomes; no cabeçalho ELF, o campo e_shstrndx
apontará para uma seção da tabela de linhas (aquela para nomes de seções, se houver várias) e o campo sh_name
no cabeçalho da seção para uma linha específica.
O primeiro (zero) e o último bytes da tabela de linhas contêm caracteres nulos. O último é compreensível porque: valor-hora, termina a última linha. Mas o deslocamento zero especifica um nome ausente ou vazio - dependendo do contexto.
Carregando seções
Existem dois endereços no cabeçalho de cada seção: um, sh_addr
é o endereço de carregamento (onde a seção será colocada na memória), o outro, sh_offset
é o deslocamento no arquivo em que esta seção está lá. Não sei como são os dois, mas cada um desses valores individualmente pode ser 0: em um caso, a seção "permanece no disco", porque há algum tipo de informação de serviço. Em outra, a seção não é carregada do disco , por exemplo, você só precisa selecioná-la e .bss
la com zeros ( .bss
). Honestamente, embora eu não tenha que processar o endereço de download - onde foi carregado, lá foi carregado :) No entanto, também temos programas específicos.
Realocação
E agora o interessante: de acordo com as medidas de segurança, como você sabe, elas não vão para a Matrix sem que um operador permaneça na base. E como ainda temos fantasia aqui, a conexão com o operador será telepática. Ah, sim, anunciei cinco minutos de tenacidade concluídos. Em geral, discutiremos brevemente o processo de vinculação.
Para o meu experimento, preciso de um pedaço de código compilado em uma inicialização normal, carregada com libdl
comum. Aqui nem vou descrever em detalhes - basta abrir o dlopen
, retirar os caracteres via dlsym
e fechá-lo com dlclose
quando o programa dlclose
. No entanto, mesmo esses são detalhes de implementação que não estão relacionados ao nosso carregador de arquivos ELF. Há simplesmente algum contexto : a capacidade de obter um ponteiro pelo nome.
Em geral, o conjunto de instruções do eBPF é um triunfo do código de máquina alinhado: uma instrução sempre leva 8 bytes e possui uma estrutura
struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; };
Além disso, muitos campos em cada instrução específica não podem ser usados - economizar espaço para um código de "máquina" não é sobre nós.
De fato, a primeira instrução pode seguir imediatamente a segunda, que não contém nenhum código de operação, mas simplesmente estende o campo imediato de 32 para 64 bits. Aqui está um patch para uma instrução composta chamada R_BPF_64_64
.
Para realizar a realocação, mais uma vez, examinaremos a tabela de seção sh_type == SHT_REL
. O campo sh_info
do cabeçalho indicará qual seção estamos sh_link
e sh_link
- a partir de qual tabela obter uma descrição dos caracteres.
typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; } Elf64_Rel;
Na verdade, existem dois tipos de seções de realocação: REL
e RELA
- a segunda contém explicitamente um termo adicional, mas eu ainda não o vi, portanto, apenas adicionamos asserção ao fato de que ele não atende e vamos processá-lo. Em seguida, adicionarei ao valor que está escrito nas instruções o endereço do símbolo. E onde conseguir? Aqui, como já sabemos, são possíveis opções:
- O símbolo refere-se à seção
SHN_ABS
. Então pegue st_value
- O caractere refere-se à seção `SHN_UNDEF. Em seguida, puxe o símbolo externo
- Em outros casos, basta corrigir o link para outra seção do mesmo arquivo`
Como tentar você mesmo
Primeiro, o que ler? Além da especificação já especificada , faz sentido ler este arquivo , no qual a equipe do iovisor coleta informações extraídas do kernel do Linux via eBPF.
Em segundo lugar, como, na verdade, todos deveriam trabalhar com isso? Primeiro você precisa obter o arquivo ELF de algum lugar. Conforme declarado no StackOverfow , a equipe nos ajudará.
clang -O2 -emit-llvm -c bpf.c -o - | llc -march=bpf -filetype=obj -o bpf.o
Em segundo lugar, você precisa de alguma forma obter uma análise de referência do arquivo em pedaços. Em uma situação normal, o comando objdump
nos ajudaria:
$ objdump : objdump <> <()> <()>. : -a, --archive-headers Display archive header information -f, --file-headers Display the contents of the overall file header -p, --private-headers Display object format specific file header contents -P, --private=OPT,OPT... Display object format specific contents -h, --[section-]headers Display the contents of the section headers -x, --all-headers Display the contents of all headers -d, --disassemble Display assembler contents of executable sections -D, --disassemble-all Display assembler contents of all sections --disassemble=<sym> Display assembler contents from <sym> -S, --source Intermix source code with disassembly -s, --full-contents Display the full contents of all sections requested -g, --debugging Display debug information in object file -e, --debugging-tags Display debug information using ctags style -G, --stabs Display (in raw form) any STABS info in the file -W[lLiaprmfFsoRtUuTgAckK] or --dwarf[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames, =frames-interp,=str,=loc,=Ranges,=pubtypes, =gdb_index,=trace_info,=trace_abbrev,=trace_aranges, =addr,=cu_index,=links,=follow-links] Display DWARF info in the file -t, --syms Display the contents of the symbol table(s) -T, --dynamic-syms Display the contents of the dynamic symbol table -r, --reloc Display the relocation entries in the file -R, --dynamic-reloc Display the dynamic relocation entries in the file @<file> Read options from <file> -v, --version Display this program's version number -i, --info List object formats and architectures supported -H, --help Display this information
Mas neste caso, é impotente:
$ objdump -d test-bpf.o test-bpf.o: elf64-little objdump: UNKNOWN!
Mais precisamente, ele mostrará seções, mas desmontar é um problema. Aqui, lembramos o que coletamos usando o LLVM. O LLVM possui seus próprios análogos estendidos de utilitários do binutils, com nomes no formato llvm-< >
. Eles, por exemplo, entendem o código de bits LLVM. E eles também entendem o eBPF - com certeza depende das opções de compilação, mas, como compilado, provavelmente deve sempre ser analisado. Portanto, por conveniência, recomendo criar um script:
vim test-bpf.c
Então, para essa fonte:
#include <stdint.h> extern uint64_t z; uint64_t func(uint64_t x, uint64_t y) { return x + y + z; }
Haverá esse resultado:
$ ./compile-bpf.sh test-bpf.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 func: 0: bf 20 00 00 00 00 00 00 r0 = r2 1: 0f 10 00 00 00 00 00 00 r0 += r1 2: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 0000000000000010: R_BPF_64_64 z 4: 79 11 00 00 00 00 00 00 r1 = *(u64 *)(r1 + 0) 5: 0f 10 00 00 00 00 00 00 r0 += r1 6: 95 00 00 00 00 00 00 00 exit SYMBOL TABLE: 0000000000000000 l df *ABS* 00000000 test-bpf.c 0000000000000000 ld .text 00000000 .text 0000000000000000 g F .text 00000038 func 0000000000000000 *UND* 00000000 z
Código
Parte 1. QInst: é melhor perder um dia e depois voar em cinco minutos (instrumentos de escrita são triviais)