Dans la partie précédente, j'ai décrit en gros comment vous pouvez charger des fonctions eBPF à partir d'un fichier ELF. Il est maintenant temps de passer de la fantaisie aux dessins animés soviétiques, et en suivant de sages conseils, après avoir dépensé une certaine quantité d'efforts une fois, faites un outil d'instrumentation universel (ou, en bref, UII !!!) . Ce faisant, je profiterai de la conception anti-modèle Golden Hammer et construirai un outil à partir de la QEMU relativement familière. En prime, nous obtenons une instrumentation trans-architecturale, ainsi qu'une instrumentation au niveau de l'ensemble de l'ordinateur virtuel. L'instrumentation sera de la forme «un petit fichier natif + un petit fichier .o avec eBPF». Dans ce cas, les fonctions eBPF seront substituées avant les instructions correspondantes de la représentation interne de QEMU avant optimisation et génération de code.
En conséquence, l'instrumentation elle-même, qui est ajoutée lors de la génération de code (c'est-à-dire sans compter quelques kilo-octets de temps d'exécution système normal), ressemble à ceci, et ce n'est pas un pseudo-code:
#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; }
Eh bien, il est temps de charger notre elfe dans la matrice. Eh bien, comment télécharger, plutôt claquer pulvérisation.
Comme déjà mentionné dans l' article sur QEMU.js , l'un des modes de fonctionnement QEMU est la génération JIT de code machine hôte à partir de l'invité (potentiellement, pour une architecture complètement différente). Si la dernière fois que j'ai implémenté mon backend de génération de code, cette fois, je vais traiter la représentation interne en la plaçant juste devant l'optimiseur. Est-ce une décision arbitraire? Non. Il y a un espoir que l'optimiseur coupe les coins excédentaires, élimine les variables inutiles, etc. Pour autant que je sache, il fait en fait des choses simples et rapidement réalisables: pousser des constantes, lancer des expressions comme «x: = x + 0» et supprimer du code inaccessible. Et nous pouvons en obtenir une quantité décente.
Configuration du script d'assemblage
Tout d'abord, ajoutons nos fichiers sources: tcg/bpf-loader.c
et tcg/instrument.c
aux Makefiles. D'une manière générale, il y a un désir de pousser un jour cela en amont, vous devrez donc le faire à la fin avec sagesse, mais pour l'instant je vais simplement ajouter ces fichiers à l'assemblage sans condition. Et je prendrai les paramètres dans les meilleures traditions de l'AFL - à travers les variables d'environnement. Soit dit en passant, je vais tester à nouveau cela sur l'instrumentation pour AFL.
Cherchez juste la mention du "voisin" - le fichier optimize.c
avec grep -R
et nous ne trouverons rien. Parce qu'il était nécessaire de rechercher optimize.o
:
Tout d'abord, ajoutons bpf-loader.c
de la dernière série avec un code qui extrait les points d'entrée correspondant aux opérations QEMU. Et le mystérieux fichier tcg-opc.h
nous y aidera. Cela ressemble à ceci:
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)
Quelle absurdité? Et le fait est simplement qu'il n'est pas connecté dans l'en-tête source - vous devez définir la macro DEF
, inclure ce fichier et supprimer immédiatement la macro. Vous voyez, il n'a même pas de garde.
static const char *inst_function_names[] = { #define DEF(name, a, b, c, d) stringify(inst_qemu_##name), #include "tcg-opc.h" #undef DEF NULL };
En conséquence, nous obtenons un tableau soigné de noms de fonctions cibles, indexés par des opcodes et se terminant par NULL, que nous pouvons exécuter pour chaque caractère du fichier. Je comprends que ce n'est pas efficace. Mais c'est simple, ce qui est important, étant donné le caractère ponctuel de cette opération. Ensuite, nous sautons juste tous les personnages pour lesquels
ELF64_ST_BIND(sym->st_info) == STB_LOCAL || ELF64_ST_TYPE(sym->st_info) != STT_FUNC
Le reste est vérifié par rapport à la liste.
Nous sommes attachés à un flux d'exécution
Maintenant, vous devez vous lever quelque part sur le flux du mécanisme de génération de code et attendre que l'instruction d'intérêt passe. Mais vous devez d'abord définir vos fonctions instrumentation_init
, tcg_instrument
et instrumentation_shutdown
dans le tcg/tcg.h
et noter leurs appels: initialisation - une fois le backend initialisé, instrumentation - juste avant l'appel tcg_optimize
. Il semblerait que instrumentation_shutdown
puisse être accroché dans instrumentation_init
sur atexit
et ne pas atexit
en flèche. Je le pensais aussi, et très probablement, cela fonctionnera en mode d'émulation système complet, mais en mode d'émulation en mode utilisateur QEMU traduit les exit_group
système exit_group
et parfois _exit
dans l' _exit
fonction _exit
, qui ignore tous ces gestionnaires atexit, par conséquent, nous allons le rechercher dans linux-user/syscall.c
et linux-user/syscall.c
appel à notre code devant lui.
Interpréter le Bytecode
Il est donc temps de lire ce que le compilateur a généré pour nous. Ceci est commodément fait en utilisant llvm-objdump
avec l'option -x
, ou mieux, immédiatement -d -t -r
.
Exemple de sortie $ ./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 vous essayez de chercher une description des opcodes eBPF, il s'avère que dans des endroits évidents (source et pages de manuel du noyau Linux), il y a des descriptions de la façon de l'utiliser, de la compilation, etc. Ensuite, vous tombez sur la page d' équipe de l'outil iovisor avec une référence eBPF non officielle pratique.
L'instruction occupe un mot de 64 bits (environ deux) et a la forme
struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; };
Ceux qui occupent deux mots se composent simplement de la première instruction avec toute la logique et d'une «bande-annonce» avec 32 bits de plus de valeur immédiate et sont très clairement visibles sur le désassembleur objdump.
Les opcodes eux-mêmes ont également une structure régulière: les trois bits inférieurs sont la classe d'opération: ALU 32 bits, ALU 64 bits, chargement / stockage, branchement conditionnel. Par conséquent, il est très pratique de les implémenter sur des macros dans les meilleures traditions de QEMU. Je ne conduirai pas d'instructions détaillées sur la base de code nous ne sommes pas en révision de code Je ferais mieux de vous parler des pièges.
Mon premier problème était que j'ai créé un allocateur de registre eBPF paresseux sous la forme de QEMU- local_temp
, et local_temp
commencé à transférer sans local_temp
l'appel de cette fonction à la macro. Il s'est avéré comme dans un meme célèbre: "Nous avons inséré une abstraction dans une abstraction afin que vous puissiez générer une instruction pendant que vous générez une instruction." Post factum, je ne comprends déjà pas très bien ce qui a été cassé à l'époque, mais quelque chose d'étrange se passait apparemment avec l'ordre des instructions générées. Après cela, j'ai créé des analogues de fonction tcg_gen_...
pour pousser de nouvelles instructions au milieu de la liste, en prenant les opérandes comme arguments de fonction, et l'ordre est automatiquement devenu comme il se doit (car les arguments sont entièrement calculés exactement une fois avant l'appel).
Le deuxième problème essayait de pousser la const TCG comme l'opérande d'une instruction arbitraire en regardant l'opérande immédiat dans eBPF. Invitant le tcg-opc.h
déjà mentionné, la composition de la liste d'arguments de l'opération est strictement fixe: n
arguments d'entrée, m
sortie et k
constante. Par ailleurs, lors du débogage d'un tel code, il est utile de passer à QEMU l'argument de ligne de commande -d op,op_opt
ou même -d op,op_opt,out_asm
.
Arguments possibles $ ./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.
Eh bien, ne répétez pas mes erreurs: le désassembleur d'instructions internes est assez avancé, et si vous voyez quelque chose comme add_i64 loc15,loc15,$554412123213
, alors cette chose après le signe dollar n'est pas un pointeur. Plus précisément, il s'agit bien sûr d'un pointeur, mais peut-être accroché avec des drapeaux et dans le rôle de la valeur littérale de l'opérande, et non du pointeur. Tout cela s'applique, bien sûr, si vous savez qu'il devrait y avoir un certain nombre spécifique, comme $0
$ff
ou $ff
, vous n'avez pas du tout à avoir peur des pointeurs. :) Comment faire movi
à cela - il vous suffit de créer une fonction qui renvoie une nouvelle temp
, dans laquelle, à travers movi
met la constante souhaitée.
Soit dit en passant, si vous commentez #define USE_TCG_OPTIMIZATIONS
dans l'en tcg/tcg.c
#define USE_TCG_OPTIMIZATIONS
tcg/tcg.c
, puis, soudainement, l'optimisation sera désactivée et il sera plus facile d'analyser les transformations de code.
Pour sim, j'enverrai un lecteur intéressé à choisir QEMU dans la documentation , même officielle! Pour le reste, je ferai la démonstration de l'instrumentation promise pour AFL.
Le même et le lapin
Pour le texte intégral de l'exécution, j'enverrai à nouveau le lecteur au référentiel, car il (le texte) n'a pas de valeur artistique et est honnêtement durci à partir de qemu_mode
de la livraison AFL, et en général, est un morceau régulier de code C. Mais voici à quoi ressemble l'instrumentation elle-même :
#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; }
Il est important que les fonctions de hook aient autant d'arguments que d' iargs
pour l'opération QEMU correspondante. Deux extern
dans l'en-tête seront liés à l'exécution pendant le processus de relocalisation. En principe, prev
pourrait être défini ici, mais il doit ensuite être défini comme static
, sinon il tombera dans la section COMMUN que je ne supporte pas. En fait, nous avons en fait simplement réécrit le pseudo-code de la documentation, mais ici il est lisible par machine!
Pour vérifier, créez le fichier 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; }
Et aussi - fichier forksrv
, qui est pratique pour alimenter AFL:
Et lancez le 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
Jusqu'à présent, la vitesse n'est pas si élevée, mais comme excuse, je dirai qu'ici (pour l'instant) une caractéristique importante du qemu_mode
original qemu_mode
pas utilisée: l'envoi d'adresses de code exécutable au serveur fork. Mais il n'y a rien d'AFL dans la base de code QEMU maintenant, et il y a un espoir que cette instrumentation généralisée soit un jour entassée en amont.
Projet GitHub