Parte 1. QInst: é melhor perder um dia e depois voar em cinco minutos (instrumentos de escrita são triviais)

Na parte anterior, descrevi mais ou menos como você pode carregar funções eBPF a partir de um arquivo ELF. Agora é hora de passar da fantasia para os desenhos animados soviéticos, e seguindo conselhos sábios, depois de gastar uma certa quantidade de esforço uma vez, faça uma ferramenta de instrumentação universal (ou, em suma, UII !!!) . Ao fazer isso, aproveitarei o design antipadrão do Golden Hammer e construirei uma ferramenta a partir do relativamente familiar QEMU . Como um bônus para isso, obtemos instrumentação entre arquiteturas e instrumentação no nível de todo o computador virtual. A instrumentação terá a forma “um pequeno arquivo-so nativo + um pequeno arquivo -o com eBPF”. Nesse caso, as funções do eBPF serão substituídas antes das instruções correspondentes da representação interna do QEMU antes da otimização e geração de código.


Como resultado, a própria instrumentação, que é adicionada durante a geração do código (ou seja, sem contar alguns kilobytes de tempo de execução normal do sistema), fica assim, e esse não é um pseudocódigo:


#include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_qemu_brcond_i64(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } void inst_qemu_brcond_i32(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } 

Bem, é hora de carregar nosso elfo na Matrix. Bem, como baixar, em vez bater spray.


Como já mencionado no artigo sobre o QEMU.js , um dos modos de operação do QEMU é a geração JIT do código da máquina host do convidado (potencialmente, para uma arquitetura completamente diferente). Se na última vez em que implementei meu back-end de geração de código, desta vez processarei a representação interna agindo diretamente na frente do otimizador. Esta é uma decisão arbitrária? Não. Há uma esperança de que o otimizador corte cantos em excesso, jogue variáveis ​​desnecessárias etc. Tanto quanto eu entendo, ele, de fato, faz coisas simples e rapidamente viáveis: empurrar constantes, lançar expressões como “x: = x + 0” e excluir código inacessível. E podemos obter uma quantidade decente disso.


Configuração de script de montagem


Primeiro, vamos adicionar nossos arquivos de origem: tcg/bpf-loader.c e tcg/instrument.c aos Makefiles. De um modo geral, há um desejo de empurrar isso para a montante, então você precisará fazer isso no final com sabedoria, mas por enquanto apenas adicionarei esses arquivos incondicionalmente ao assembly. E tomarei os parâmetros nas melhores tradições da AFL - através de variáveis ​​de ambiente. A propósito, testarei isso novamente na instrumentação para AFL.


Basta procurar pela menção do "vizinho" - o arquivo optimize.c com grep -R e não encontraremos nada. Porque era necessário procurar optimize.o :


 --- a/Makefile.target +++ b/Makefile.target @@ -110,7 +110,7 @@ obj-y += trace/ obj-y += exec.o obj-y += accel/ obj-$(CONFIG_TCG) += tcg/tcg.o tcg/tcg-op.o tcg/tcg-op-vec.o tcg/tcg-op-gvec.o -obj-$(CONFIG_TCG) += tcg/tcg-common.o tcg/optimize.o +obj-$(CONFIG_TCG) += tcg/tcg-common.o tcg/optimize.o tcg/instrument.o tcg/bpf-loader.o obj-$(CONFIG_TCG_INTERPRETER) += tcg/tci.o obj-$(CONFIG_TCG_INTERPRETER) += disas/tci.o obj-$(CONFIG_TCG) += fpu/softfloat.o 

Então aqui está você, metaprogramação em C ...


Primeiro, vamos adicionar o bpf-loader.c da última série com o código que extrai os pontos de entrada correspondentes às operações do QEMU. E o misterioso arquivo tcg-opc.h nos ajudará com isso. É assim:


 /* * DEF(name, oargs, iargs, cargs, flags) */ /* predefined ops */ DEF(discard, 1, 0, 0, TCG_OPF_NOT_PRESENT) DEF(set_label, 0, 0, 1, TCG_OPF_BB_END | TCG_OPF_NOT_PRESENT) /* variable number of parameters */ DEF(call, 0, 0, 3, TCG_OPF_CALL_CLOBBER | TCG_OPF_NOT_PRESENT) DEF(br, 0, 0, 1, TCG_OPF_BB_END) // ... 

Que bobagem? E o problema é simplesmente que ele não está conectado no cabeçalho de origem - você precisa definir a macro DEF , incluir esse arquivo e excluir imediatamente a macro. Veja, ele nem tem guarda.


 static const char *inst_function_names[] = { #define DEF(name, a, b, c, d) stringify(inst_qemu_##name), #include "tcg-opc.h" #undef DEF NULL }; 

Como resultado, obtemos uma matriz clara de nomes de funções de destino, indexados por opcodes e terminando com NULL, que podemos executar para cada caractere no arquivo. Eu entendo que isso não é eficaz. Mas é simples, o que é importante, dada a natureza única dessa operação. Em seguida, pulamos todos os caracteres para os quais


 ELF64_ST_BIND(sym->st_info) == STB_LOCAL || ELF64_ST_TYPE(sym->st_info) != STT_FUNC 

O restante é verificado na lista.


Estamos ligados a um fluxo de execução


Agora você precisa subir em algum lugar no encadeamento do mecanismo de geração de código e aguardar até que a instrução de interesse passe. Mas primeiro você precisa definir suas funções instrumentation_init , tcg_instrument e instrumentation_shutdown no tcg/tcg.h e anote as chamadas: inicialização - após a inicialização do backend, instrumentação - logo antes da chamada tcg_optimize . Parece que instrumentation_shutdown pode ser pendurado em instrumentation_init no atexit e não ser elevado. Eu também pensava assim, e provavelmente funcionará no modo de emulação de sistema completo, mas no modo de emulação de modo de usuário, o QEMU traduz as chamadas do sistema exit_group e às vezes exit na chamada de função _exit , que ignora todos esses manipuladores atexit, portanto, vamos encontrá-lo em linux-user/syscall.c e linux-user/syscall.c chamada para o nosso código na frente dele.


Interpretando Bytecode


Então é hora de ler o que o compilador gerou para nós. Isso é feito convenientemente usando llvm-objdump com a opção -x , ou melhor, imediatamente -d -t -r .


Exemplo de saída
 $ ./compile-bpf.sh test-bpf.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 inst_brcond_i64: 0: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll 0000000000000000: R_BPF_64_64 prev 2: 79 23 00 00 00 00 00 00 r3 = *(u64 *)(r2 + 0) 3: 77 03 00 00 01 00 00 00 r3 >>= 1 4: 7b 32 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r3 5: af 13 00 00 00 00 00 00 r3 ^= r1 6: 57 03 00 00 ff ff 00 00 r3 &= 65535 7: 18 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r4 = 0 ll 0000000000000038: R_BPF_64_64 __afl_area_ptr 9: 79 44 00 00 00 00 00 00 r4 = *(u64 *)(r4 + 0) 10: 0f 34 00 00 00 00 00 00 r4 += r3 11: 71 43 00 00 00 00 00 00 r3 = *(u8 *)(r4 + 0) 12: 07 03 00 00 01 00 00 00 r3 += 1 13: 73 34 00 00 00 00 00 00 *(u8 *)(r4 + 0) = r3 14: 7b 12 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r1 15: 95 00 00 00 00 00 00 00 exit 0000000000000080 inst_brcond_i32: 16: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll 0000000000000080: R_BPF_64_64 prev 18: 79 23 00 00 00 00 00 00 r3 = *(u64 *)(r2 + 0) 19: 77 03 00 00 01 00 00 00 r3 >>= 1 20: 7b 32 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r3 21: af 13 00 00 00 00 00 00 r3 ^= r1 22: 57 03 00 00 ff ff 00 00 r3 &= 65535 23: 18 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r4 = 0 ll 00000000000000b8: R_BPF_64_64 __afl_area_ptr 25: 79 44 00 00 00 00 00 00 r4 = *(u64 *)(r4 + 0) 26: 0f 34 00 00 00 00 00 00 r4 += r3 27: 71 43 00 00 00 00 00 00 r3 = *(u8 *)(r4 + 0) 28: 07 03 00 00 01 00 00 00 r3 += 1 29: 73 34 00 00 00 00 00 00 *(u8 *)(r4 + 0) = r3 30: 7b 12 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r1 31: 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 *UND* 00000000 __afl_area_ptr 0000000000000080 g F .text 00000080 inst_brcond_i32 0000000000000000 g F .text 00000080 inst_brcond_i64 0000000000000008 g O *COM* 00000008 prev 

Se você tentar procurar uma descrição dos códigos de operação do eBPF, descobrirá que em locais óbvios (páginas de origem e manual do kernel do Linux) há descrições de como usá-lo, como compilar etc. Em seguida, você encontra a página da equipe de ferramentas do iovisor com uma referência conveniente e não oficial do eBPF.


A instrução ocupa uma palavra de 64 bits (algumas duas) e tem o formato


 struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; }; 

Aqueles que ocupam duas palavras consistem simplesmente na primeira instrução com toda a lógica e em um "trailer" com mais 32 bits de valor imediato e são claramente visíveis no desmontador de objdump.


Os próprios códigos de operação também têm uma estrutura regular: os três bits inferiores são a classe de operação: ALU de 32 bits, ALU de 64 bits, carga / armazenamento, ramificação condicional. Portanto, é muito conveniente implementá-los em macros nas melhores tradições do QEMU. Não conduzirei instruções detalhadas na base de código não estamos na revisão de código É melhor falar sobre as armadilhas.


Meu primeiro problema foi que criei um alocador de registro lento do eBPF na forma de QEMU- local_temp e comecei a transferir, sem pensar, a chamada dessa função para a macro. Acabou como em um meme famoso: "Inserimos uma abstração em uma abstração para que você possa gerar uma instrução enquanto gera uma instrução". Pós-factum, eu já não entendi muito bem o que estava quebrado, mas algo estranho estava aparentemente acontecendo com a ordem das instruções geradas. Depois disso, fiz análogos das funções tcg_gen_... para tcg_gen_... novas instruções no meio da lista, levando operandos como argumentos para a função e a ordem tornou-se automaticamente como deveria (já que os argumentos são completamente calculados exatamente uma vez antes da chamada).


O segundo problema foi tentar empinar o TCG const como o operando de uma instrução arbitrária ao ver o operando imediato no eBPF. Solicitando o tcg-opc.h já mencionado, a composição da lista de argumentos da operação é estritamente fixa: n argumentos de entrada, m saída e constante k . A propósito, ao depurar esse código, ajuda a transmitir ao QEMU o argumento da linha de comandos -d op,op_opt ou mesmo -d op,op_opt,out_asm .


Possíveis argumentos
 $ ./x86_64-linux-user/qemu-x86_64 -d help Log items (comma separated): out_asm show generated host assembly code for each compiled TB in_asm show target assembly code for each compiled TB op show micro ops for each compiled TB op_opt show micro ops after optimization op_ind show micro ops before indirect lowering int show interrupts/exceptions in short format exec show trace before each executed TB (lots of logs) cpu show CPU registers before entering a TB (lots of logs) fpu include FPU registers in the 'cpu' logging mmu log MMU-related activities pcall x86 only: show protected mode far calls/returns/exceptions cpu_reset show CPU state before CPU resets unimp log unimplemented functionality guest_errors log when the guest OS does something invalid (eg accessing a non-existent register) page dump pages at beginning of user mode emulation nochain do not chain compiled TBs so that "exec" and "cpu" show complete traces trace:PATTERN enable trace events Use "-d trace:help" to get a list of trace events. 

Bem, não repita meus erros: o desmontador de instruções internas é bastante avançado e, se você add_i64 loc15,loc15,$554412123213 algo como add_i64 loc15,loc15,$554412123213 , esse add_i64 loc15,loc15,$554412123213 depois do cifrão não é um ponteiro. Mais precisamente, é claro que isso é um ponteiro, mas talvez pendurado com bandeiras e no papel do valor literal do operando, e não do ponteiro. Tudo isso se aplica, é claro, se você sabe que deve haver um número específico, como $0 ou $ff , não precisa ter medo de indicadores. :) Como movi com isso - você só precisa criar uma função que retorne uma nova temp , na qual, por meio do movi coloque a constante desejada.


A propósito, se você comentar #define USE_TCG_OPTIMIZATIONS no #define USE_TCG_OPTIMIZATIONS tcg/tcg.c , subitamente a otimização será desativada e será mais fácil analisar as transformações de código.


Para sim, enviarei um leitor interessado em escolher o QEMU na documentação , mesmo a oficial! Quanto ao resto, demonstrarei a instrumentação prometida para a AFL.


O mesmo e o coelho


Para o texto completo do tempo de execução, eu, novamente, enviarei o leitor para o repositório, uma vez que (o texto) não tem valor artístico e é honestamente qemu_mode de qemu_mode da entrega da AFL e, em geral, é uma parte regular do código C. Mas aqui está como a própria instrumentação se parece. :


 #include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_qemu_brcond_i64(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } void inst_qemu_brcond_i32(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } 

É importante que as funções de gancho tenham tantos argumentos quanto iargs para a operação QEMU correspondente. Duas extern no cabeçalho serão vinculadas ao tempo de execução durante o processo de realocação. Em princípio, prev pode ser definido aqui, mas precisa ser definido como static ; caso contrário, cairá na seção COMUM que eu não apoio. Na verdade, nós simplesmente reescrevemos o pseudo-código da documentação, mas aqui é legível por máquina!


Para verificar, crie o arquivo bug.c :


 #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char *argv[]) { char buf[16]; int res = read(0, buf, 4); if (buf[0] == 'T' && buf[1] == 'E' && buf[2] == 'S' && buf[3] == 'T') abort(); return res * 0; } 

E também - arquivo forksrv , que é conveniente para alimentar o AFL:


 #!/bin/bash export NATIVE_INST=./instrumentation-examples/afl/afl-native.so export BPF_INST=./instrumentation-examples/afl/afl-bpf.co exec ./x86_64-linux-user/qemu-x86_64 ./instrumentation-examples/afl/bug 

E execute fuzzing:


 AFL_SKIP_BIN_CHECK=1 afl-fuzz -i ../input -o ../output -m none -- ./forksrv 

American Fuzzy Lop
 1234 T234 TE34 TES4 TEST <-     crashes,    2200   

Até agora, a velocidade não é tão alta, mas como desculpa, direi que aqui (por enquanto) um recurso importante do qemu_mode original não qemu_mode usado: o envio de endereços de código executável para o servidor fork. Mas não há nada AFL na base de código QEMU agora, e há esperança de que esta instrumentação generalizada um dia seja amontoada no fluxo de dados.


Projeto GitHub

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


All Articles