Teil 1. QInst: Es ist besser, einen Tag zu verlieren und dann in fünf Minuten zu fliegen (Schreibgeräte sind trivial)

Im vorherigen Teil habe ich grob beschrieben, wie Sie eBPF-Funktionen aus einer ELF-Datei laden können. Jetzt ist es an der Zeit, von der Fantasie zu sowjetischen Cartoons überzugehen und nach einem gewissen Aufwand ein universelles Instrumentierungswerkzeug zu befolgen (oder kurz UII !!!) . Dabei werde ich das Golden Hammer Antipattern-Design nutzen und ein Tool aus der relativ bekannten QEMU erstellen. Als Bonus erhalten wir architekturübergreifende Instrumente sowie Instrumente auf der Ebene des gesamten virtuellen Computers. Die Instrumentierung hat die Form „eine kleine native So-Datei + eine kleine O-Datei mit eBPF“. In diesem Fall werden eBPF-Funktionen vor den entsprechenden Anweisungen der internen Darstellung von QEMU vor der Optimierung und Codegenerierung ersetzt.


Infolgedessen sieht die Instrumentierung selbst, die während der Codegenerierung hinzugefügt wird (dh ohne ein paar Kilobyte der normalen Systemlaufzeit), folgendermaßen aus, und dies ist kein Pseudocode:


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

Nun, es ist Zeit, unseren Elfen in die Matrix zu laden. Nun, eher wie man herunterlädt klatschen sprühen.


Wie bereits im Artikel über QEMU.js erwähnt , ist einer der QEMU-Betriebsmodi die JIT-Generierung von Host-Maschinencode aus dem Gast (möglicherweise für eine völlig andere Architektur). Wenn ich das letzte Mal mein Codegenerierungs-Backend implementiert habe, werde ich dieses Mal die interne Darstellung verarbeiten, indem ich direkt vor dem Optimierer klemme. Ist das eine willkürliche Entscheidung? Nein. Es besteht die Hoffnung, dass der Optimierer überschüssige Ecken abschneidet, unnötige Variablen wegwirft usw. Soweit ich weiß, macht er tatsächlich einfache und schnell umsetzbare Dinge: Konstanten verschieben, Ausdrücke wie „x: = x + 0“ ausgeben und nicht erreichbaren Code löschen. Und wir können eine anständige Menge davon bekommen.


Konfiguration des Assemblerskripts


tcg/bpf-loader.c unsere Quelldateien hinzu: tcg/bpf-loader.c und tcg/instrument.c zu den Makefiles. Im Allgemeinen besteht der Wunsch, dies eines Tages in den Upstream zu verschieben, sodass Sie dies am Ende mit Bedacht tun müssen, aber im Moment werde ich diese Dateien nur bedingungslos zur Assembly hinzufügen. Und ich werde die Parameter in den besten Traditionen der AFL übernehmen - durch Umgebungsvariablen. Übrigens werde ich dies noch einmal an der Instrumentierung für AFL testen.


Suchen Sie einfach nach der Erwähnung des "Nachbarn" - der Datei optimize.c mit grep -R und wir werden nichts finden. Weil es notwendig war, nach optimize.o zu suchen:


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

Also hier sind Sie, Metaprogrammierung in C ...


bpf-loader.c aus der letzten Serie mit Code hinzu, der Einstiegspunkte für QEMU-Operationen herauszieht. Und die mysteriöse Datei tcg-opc.h wird uns dabei helfen. Es sieht so aus:


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

Was für ein Unsinn? Und die Sache ist einfach, dass es nicht im Quellheader verbunden ist - Sie müssen das DEF Makro definieren, diese Datei einschließen und das Makro sofort löschen. Sehen Sie, er hat nicht einmal Wache.


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

Als Ergebnis erhalten wir ein ordentliches Array von Zielfunktionsnamen, die durch Opcodes indiziert sind und mit NULL enden, die wir für jedes Zeichen in der Datei ausführen können. Ich verstehe, dass dies nicht effektiv ist. Aber es ist einfach, was angesichts des einmaligen Charakters dieser Operation wichtig ist. Als nächstes überspringen wir einfach alle Zeichen, für die


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

Der Rest wird mit der Liste verglichen.


Wir sind an einen Ausführungsfluss gebunden


Jetzt müssen Sie irgendwo im Ablauf des Codegenerierungsmechanismus aufstehen und warten, bis die interessierende Anweisung vergeht. Aber zuerst müssen Sie Ihre Funktionen instrumentation_init , tcg_instrument und instrumentation_shutdown in der tcg/tcg.h und ihre Aufrufe notieren: Initialisierung - nachdem das Backend initialisiert wurde, instrumentation - unmittelbar vor dem Aufruf von tcg_optimize . Es scheint, dass instrumentation_shutdown in instrumentation_init auf atexit und nicht in die Höhe atexit werden kann. Ich dachte auch, und höchstwahrscheinlich wird es im vollständigen _exit funktionieren, aber im Usermode-Emulationsmodus übersetzt QEMU die exit_group und beendet manchmal den Funktionsaufruf _exit , der alle diese atexit-Handler ignoriert. Daher werden wir in linux-user/syscall.c und linux-user/syscall.c Aufruf in unseren Code davor linux-user/syscall.c .


Bytecode interpretieren


Es ist also Zeit zu lesen, was der Compiler für uns generiert hat. Dies geschieht bequemerweise mit llvm-objdump mit der Option -x oder besser sofort mit -d -t -r .


Ausgabebeispiel
 $ ./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 

Wenn Sie versuchen, nach einer Beschreibung der eBPF-Opcodes zu suchen, werden Sie feststellen, dass an offensichtlichen Stellen (Quell- und Manpages des Linux-Kernels) Beschreibungen zu deren Verwendung, Kompilierung usw. vorhanden sind. Dann stoßen Sie auf die Seite des iovisor-Toolteams mit einer praktischen inoffiziellen eBPF-Referenz.


Der Befehl belegt ein 64-Bit-Wort (etwa zwei) und hat die Form


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

Diejenigen, die zwei Wörter belegen, bestehen einfach aus der ersten Anweisung mit der gesamten Logik und einem „Trailer“ mit 32 weiteren Bits von unmittelbarem Wert und sind auf dem objdump-Disassembler sehr deutlich sichtbar.


Die Opcodes selbst haben ebenfalls eine reguläre Struktur: Die unteren drei Bits sind die Operationsklasse: 32-Bit-ALU, 64-Bit-ALU, Laden / Speichern, bedingte Verzweigung. Daher ist es sehr praktisch, sie in Makros in den besten Traditionen von QEMU zu implementieren. Ich werde keine detaillierten Anweisungen auf der Codebasis durchführen Wir sind nicht auf Code-Überprüfung Ich erzähle Ihnen besser von den Fallstricken.


Mein erstes Problem war, dass ich einen faulen eBPF-Registerzuweiser in Form von QEMU- local_temp und den Aufruf dieser Funktion gedankenlos an das Makro übertragen habe. Es stellte sich wie in einem berühmten Mem heraus: "Wir haben eine Abstraktion in eine Abstraktion eingefügt, damit Sie eine Anweisung generieren können, während Sie eine Anweisung generieren." Post factum verstehe ich schon nicht sehr gut, was damals kaputt war, aber mit der Reihenfolge der generierten Anweisungen passierte anscheinend etwas Seltsames. Danach habe ich Analoga der Funktionen tcg_gen_... , um neue Anweisungen in die Mitte der Liste zu verschieben, wobei Operanden als Argumente für die Funktion verwendet wurden, und die Reihenfolge wurde automatisch so, wie sie sollte (da die Argumente genau einmal vor dem Aufruf vollständig berechnet wurden).


Das zweite Problem bestand darin, die TCG-Konstante als Operanden einer beliebigen Anweisung zu verschieben, wenn der unmittelbare Operand in eBPF betrachtet wurde. Bei der Aufforderung zur bereits erwähnten tcg-opc.h ist die Zusammensetzung der Argumentliste der Operation streng festgelegt: n Eingabeargumente, m Ausgabe und k Konstante. Übrigens hilft es beim Debuggen eines solchen Codes, QEMU das Befehlszeilenargument -d op,op_opt oder sogar -d op,op_opt,out_asm .


Mögliche Argumente
 $ ./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. 

Wiederholen Sie meine Fehler nicht: Der Disassembler für interne Anweisungen ist ziemlich weit fortgeschritten. Wenn Sie so etwas wie add_i64 loc15,loc15,$554412123213 , ist dieses Ding nach dem Dollarzeichen kein Zeiger. Genauer gesagt ist dies natürlich ein Zeiger, der jedoch möglicherweise mit Flags und in der Rolle des Literalwerts des Operanden und nicht des Zeigers aufgehängt ist. All dies gilt natürlich, wenn Sie wissen, dass es eine bestimmte Zahl wie $0 oder $ff , müssen Sie sich überhaupt nicht vor Zeigern fürchten. :) Wie man damit movi - Sie müssen nur eine Funktion erstellen, die eine neue movi , in die durch movi die gewünschte Konstante gesetzt wird.


Übrigens, wenn Sie #define USE_TCG_OPTIMIZATIONS im tcg/tcg.c #define USE_TCG_OPTIMIZATIONS tcg/tcg.c , wird die Optimierung plötzlich tcg/tcg.c und es ist einfacher, tcg/tcg.c zu analysieren.


Für sim werde ich einen Leser schicken, der daran interessiert ist, QEMU in die Dokumentation aufzunehmen , sogar die offizielle! Im Übrigen werde ich die versprochene Instrumentierung für AFL demonstrieren.


Das gleiche und das Kaninchen


Für den vollständigen Text der Laufzeit werde ich den Leser erneut an das Repository senden, da er (der Text) nicht von künstlerischem Wert ist und ehrlich aus qemu_mode aus der AFL-Lieferung gehärtet wird und im Allgemeinen ein regulärer Teil des C-Codes ist. Aber so sieht die Instrumentierung selbst aus ::


 #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 ist wichtig, dass Hook-Funktionen so viele Argumente wie iargs für die entsprechende QEMU-Operation haben. Zwei extern im Header werden während des Verschiebungsprozesses mit der Laufzeit verknüpft. Im Prinzip könnte prev genau hier definiert werden, aber dann muss es als static definiert werden, sonst fällt es in den GEMEINSAMEN Abschnitt, den ich nicht unterstütze. Eigentlich haben wir den Pseudocode einfach aus der Dokumentation umgeschrieben, aber hier ist er maschinenlesbar!


Erstellen Sie zur bug.c Datei 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; } 

Und auch - forksrv Datei, die bequem ist, um AFL zu füttern:


 #!/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 

Und laufen Sie Fuzzing:


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

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

Bisher ist die Geschwindigkeit nicht so hoch, aber als Entschuldigung qemu_mode ich sagen, dass hier ( qemu_mode ) ein wichtiges Merkmal des ursprünglichen qemu_mode nicht verwendet wird: das Senden von Adressen mit ausführbarem Code an den Fork-Server. Aber es gibt jetzt nichts AFL in der QEMU-Codebasis, und es besteht die Hoffnung, dass diese verallgemeinerte Instrumentierung eines Tages in den Upstream gesteckt wird.


GitHub-Projekt

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


All Articles