Parte 1. QInst: es mejor perder un día, luego volar en cinco minutos (escribir instrumentos es trivial)

En la parte anterior, describí aproximadamente cómo puede cargar funciones eBPF desde un archivo ELF. Ahora es el momento de pasar de los dibujos animados de fantasía a los soviéticos, y siguiendo sabios consejos, después de gastar una cierta cantidad de esfuerzo una vez, hacer una herramienta de instrumentación universal (o, en resumen, UII !!!) . Al hacerlo, aprovecharé el diseño antipatrón Golden Hammer y construiré una herramienta de la relativamente familiar QEMU . Como beneficio adicional para esto, obtenemos instrumentación de arquitectura cruzada, así como instrumentación a nivel de toda la computadora virtual. La instrumentación tendrá la forma de "un pequeño archivo nativo + un pequeño archivo .o con eBPF". En este caso, las funciones de eBPF se sustituirán antes que las instrucciones correspondientes de la representación interna de QEMU antes de la optimización y la generación de código.


Como resultado, la instrumentación en sí, que se agrega durante la generación del código (es decir, sin contar un par de kilobytes de tiempo de ejecución normal del sistema), se ve así, y este no es un 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; } 

Bueno, es hora de cargar a nuestro elfo en Matrix. Bueno, como descargar, más bien golpe spray.


Como ya se mencionó en el artículo sobre QEMU.js , uno de los modos de operación de QEMU es la generación JIT de código de máquina host desde el invitado (potencialmente, para una arquitectura completamente diferente). Si la última vez que implementé mi backend de generación de código, esta vez voy a procesar la representación interna encajando justo en frente del optimizador. ¿Es esta una decisión arbitraria? No Existe la esperanza de que el optimizador elimine el exceso de esquinas, arroje variables innecesarias, etc. Según tengo entendido, él, de hecho, hace cosas simples y rápidamente factibles: empujar constantes, tirar expresiones como "x: = x + 0" y eliminar el código inalcanzable. Y podemos obtener una cantidad decente.


Configuración de script de ensamblaje


Primero, agreguemos nuestros archivos fuente: tcg/bpf-loader.c y tcg/instrument.c a los Makefiles. En términos generales, existe el deseo de algún día llevar esto a la corriente ascendente, por lo que deberá hacerlo sabiamente al final, pero por ahora agregaré incondicionalmente estos archivos al ensamblado. Y tomaré los parámetros en las mejores tradiciones de AFL - a través de variables de entorno. Por cierto, probaré esto nuevamente en la instrumentación para AFL.


Simplemente busque la mención del "vecino": el archivo optimize.c con grep -R y no encontraremos nada. Debido a que era necesario buscar 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 

Así que aquí estás, metaprogramando en C ...


Primero, agreguemos bpf-loader.c de la última serie con un código que extraiga los puntos de entrada correspondientes a las operaciones QEMU. Y el misterioso archivo tcg-opc.h nos ayudará con esto. Se ve así:


 /* * 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) // ... 

¿Qué tontería? Y la cuestión es simplemente que no está conectado en el encabezado de origen: debe definir la macro DEF , incluir este archivo e inmediatamente eliminar la macro. Mira, él ni siquiera tiene guardia.


 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, obtenemos una ordenada matriz de nombres de funciones de destino, indexados por códigos de operación y terminando con NULL, que podemos ejecutar para cada carácter en el archivo. Entiendo que esto no es efectivo. Pero es simple, lo cual es importante, dada la naturaleza única de esta operación. A continuación, omitimos todos los caracteres para los que


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

El resto se compara con la lista.


Estamos unidos a un flujo de ejecución


Ahora necesita levantarse en algún lugar del flujo del mecanismo de generación de código y esperar hasta que pasen las instrucciones de interés. Pero primero debe definir sus funciones instrumentation_init , tcg_instrument y instrumentation_shutdown en el tcg/tcg.h y anotar sus llamadas: inicialización - después de que se inicializa el backend, instrumentación - justo antes de la llamada tcg_optimize . Parece que instrumentation_shutdown puede colgarse en instrumentation_init en atexit y no atexit . Yo también lo pensé, y lo más probable es que funcione en el modo de emulación de sistema completo, pero en el modo de emulación de modo de usuario, QEMU traduce las exit_group sistema exit_group y, a veces, exit a la _exit función _exit , que ignora todos estos manejadores de atexit, por lo tanto, lo buscaremos en linux-user/syscall.c y linux-user/syscall.c llamada a nuestro código delante de él.


Interpretar Bytecode


Así que es hora de leer lo que el compilador generó para nosotros. Esto se hace convenientemente usando llvm-objdump con la opción -x , o mejor, inmediatamente -d -t -r .


Ejemplo de salida
 $ ./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 

Si intenta buscar una descripción de los códigos de operación eBPF, resulta que en lugares obvios (fuente y páginas de manual del kernel de Linux) hay descripciones de cómo usarlo, cómo compilar, etc. Luego se encuentra con la página del equipo de la herramienta iovisor con una conveniente referencia no oficial de eBPF.


La instrucción ocupa una palabra de 64 bits (algunas dos) y tiene la forma


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

Aquellos que ocupan dos palabras simplemente consisten en la primera instrucción con toda la lógica y un "trailer" con 32 bits más de valor inmediato y son muy claramente visibles en el desensamblador de objdump.


Los propios códigos de operación también tienen una estructura regular: los tres bits inferiores son la clase de operación: ALU de 32 bits, ALU de 64 bits, carga / almacenamiento, ramificación condicional. Por lo tanto, es muy conveniente implementarlos en macros en las mejores tradiciones de QEMU. No conduciré instrucciones detalladas sobre la base del código no estamos en revisión de código Será mejor que te cuente sobre las trampas.


Mi primer problema fue que hice un asignador perezoso de registros eBPF en forma de QEMU- local_temp , y comencé a transferir sin local_temp la llamada de esta función a la macro. Resultó como en un famoso meme: "Insertamos una abstracción en una abstracción para que pueda generar una instrucción mientras genera una instrucción". Después del hecho, ya no entiendo muy bien qué se rompió en ese momento, pero aparentemente algo extraño estaba sucediendo con el orden de las instrucciones generadas. Después de eso, hice análogos de las funciones tcg_gen_... para insertar nuevas instrucciones en el medio de la lista, tomando los operandos como argumentos de la función, y el orden se convirtió automáticamente como debería (ya que los argumentos se calculan completamente exactamente una vez antes de la llamada).


El segundo problema fue tratar de empujar la constante TCG como el operando de una instrucción arbitraria al mirar el operando inmediato en eBPF. Solicitando el tcg-opc.h ya mencionado, la composición de la lista de argumentos de la operación es estrictamente fija: n argumentos de entrada, m salida y k constante. Por cierto, al depurar dicho código, ayuda a pasar QEMU el argumento de la línea de comando -d op,op_opt o incluso -d op,op_opt,out_asm .


Posibles 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. 

Bueno, no repita mis errores: el desensamblador de instrucciones internas está bastante avanzado, y si ve algo como add_i64 loc15,loc15,$554412123213 , entonces esto después del signo de dólar no es un puntero. Más precisamente, esto, por supuesto, es un puntero, pero tal vez colgado de banderas y en el papel del valor literal del operando, y no el puntero. Todo esto se aplica, por supuesto, si sabe que debería haber un número específico, como $0 o $ff , no tiene que tener miedo de los punteros. :) Cómo movi con esto: solo necesita crear una función que devuelva una temp nueva, en la que a través de movi la constante deseada.


Por cierto, si comenta #define USE_TCG_OPTIMIZATIONS en el #define USE_TCG_OPTIMIZATIONS tcg/tcg.c , entonces, de repente, la optimización se desactivará y será más fácil analizar las transformaciones de código.


Para sim, enviaré un lector interesado en elegir QEMU en la documentación , ¡incluso el oficial! Por lo demás, demostraré la instrumentación prometida para AFL.


Lo mismo y el conejo


Para el texto completo del tiempo de ejecución, nuevamente enviaré al lector al repositorio, ya que (el texto) no tiene valor artístico y honestamente está endurecido de qemu_mode de la entrega de AFL, y en general, es una pieza regular de código C. Pero así es como se ve la instrumentación en sí :


 #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; } 

Es importante que las funciones de iargs tengan tantos argumentos como iargs para la operación QEMU correspondiente. Dos extern en el encabezado se vincularán al tiempo de ejecución durante el proceso de reubicación. En principio, prev podría definirse aquí mismo, pero luego debe definirse como static , de lo contrario caerá en la sección COMÚN que no soporto. En realidad, nosotros, de hecho, simplemente reescribimos el pseudocódigo de la documentación, ¡pero aquí es legible por máquina!


Para verificar, cree el archivo 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; } 

Y también - archivo forksrv , que es conveniente para alimentar 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 

Y corre 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   

Hasta ahora, la velocidad no es tan alta, pero como excusa diré que aquí (por ahora) una característica importante del qemu_mode original no se usa: enviar direcciones de código ejecutable al servidor fork. Pero ahora no hay nada AFL en la base de código QEMU, y hay esperanzas de que esta instrumentación generalizada algún día se acumule en sentido ascendente.


Proyecto GitHub

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


All Articles