Parte 0. Requer que um elfo trabalhe na Matrix. É possível realocar

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 #     clang -Oz -emit-llvm -c test-bpf.c -o - | llc -march=bpf -filetype=obj -o test-bpf.o llvm-objdump -d -t -r test-bpf.o 

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)

Source: https://habr.com/ru/post/pt452592/


All Articles