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
:
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(discard, 1, 0, 0, TCG_OPF_NOT_PRESENT) DEF(set_label, 0, 0, 1, TCG_OPF_BB_END | TCG_OPF_NOT_PRESENT) 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:
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