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
:
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(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)
¿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:
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