Openbsd vmexit contrôlé
Sommaire
- Introduction
- Théorie
- Pratique
Contexte
Cet article, décrit comment générer un vm-exit contrôlé dans OpenBSD. Pour parvenir à ce résultat, le code source, coté noyau devra être patcher notamment la partie qui est chargé de la gestion des vm-exit, il sera également nécessaire de coder un petit programme pour déclencher la raison du vm-exit ciblé.
Durant des missions nécessitant l’utilisation d’un fuzzer noyau, l’implémentation actuelle du fuzzer ne semblait pas très efficace, notamment car la partie couverture de code était manquante. C’est pour cela que cette approche servira notamment à implémenter un fuzzer guidé par couverture de code dans notre nouveau fuzzer noyau développé en interne et destiné à une utilisation dans un environnement virtualisé. Cette approche permettra de récupérer les traces d’exécution de chaque input transmit afin d’adapter nos inputs mutés.
Le fuzzing guidé par couverture de code (coverage-guided) (également connu sous le nom de fuzzing boite grise) utilise l’instrumentation du programme pour retracer la couverture de code atteinte par chaque entrée transmise à une cible de fuzzing. Les moteurs de fuzzing utilisent ces informations pour prendre des décisions éclairées sur les entrées à muter afin de maximiser la couverture.
Qu’est-ce qu’un vm-exit et un vm-exit contrôlé ?
Un vm-exit permet au VMM (Virtual Machine Monitor) ou autrement dit, à l’hyperviseur de reprendre le contrôle de VM (Virtual Machine) lorsqu’un événement spécifique se produit. Le vm-exit passe du mode VMX non-root au mode VMX root en transférant le contrôle de la VM au VMM, permettant au VMM de prendre les mesures appropriées selon l’événement qui a déclenché le vm-exit.
Un vm-exit contrôlé est un vm-exit où l’événement déclencheur est contrôlé, c’est-à-dire que l’événement est choisi et sa gestion modifiée pour permettre l’exécution de code spécifique. Ainsi, lorsque le VMM prend le contrôle pour gérer cet événement, il exécute simultanément le code implémentant la gestion de cet événement.
Il est recommandé d’avoir au préalable une base sur la structure d’un hyperviseur pour pouvoir appréhender au mieux le potentiel de cette Preuve de concept.
Méthodologie
Voici la méthodologie suivie :
- Hyperviseur : Acquérir une compréhension de base de l’architecture des hyperviseurs.
- Vm-exit : Comprendre la notion d’un vm-exit.
- Analyse : Analyse des fichiers concernant la virtualisation afin d’identifier où est implémentée la gestion des raisons d’un vm-exit.
- POC : Cette partie décrira l’étape de la POC (Proof of Concept) qui permet de démontrer qu’un vm-exit contrôlé est possible dans OpenBSD.
VMX
Qu’est-ce que sont les Virtual Machine Extensions (VMX) ?
Les extensions de machine virtuelle, définissent la prise en charge au niveau du processeur pour les machines virtuelles basées sur les processeurs IA-32.
Deux principales classes de logiciel sont supportées :
- Virtual Machine Monitors (VMM) : Un VMM contrôle entièrement le(s) processeur(s) ainsi que les autres matériels de la plateforme de l’hôte. Un VMM fournit au logiciel invité une abstraction d’un processeur virtuel et lui permet de s’exécuter directement sur un processeur logique. Le VMM est capable de conserver un contrôle sélectif des ressources du processeur, de la mémoire physique, de la gestion des interruptions et des I/O.
- Guest software : Chaque machine virtuelle (VM) est un environnement logiciel invité (Guest software) qui prend en charge une pile composée d’un système d’exploitation (OS) et d’un logiciel d’application. Chaque machine virtuelle fonctionne indépendamment des autres machines virtuelles mais utilise la même interface avec le(s) processeur(s), la mémoire, le stockage, les graphiques et les I/O fournis par la plateforme physique de l’hôte. La pile logicielle agit comme si elle fonctionnait sur une plateforme sans VMM. Les logiciels s’exécutant dans une machine virtuelle doivent fonctionner avec des privilèges restreints/réduits afin que VMM puisse conserver le contrôle des ressources de la plateforme.
operation VMX
La prise en charge de la virtualisation par le processeur est assurée par une forme d’opération du processeur appelée VMX operation.
Il existe deux types d’opérations VMX :
- VMX root operation : Un VMM s’exécute en mode VMX root.
- VMX non-root operation : Le logiciel invité s’exécute en mode VMX non-root.
Les transitions entre le fonctionnement en mode VMX root et VMX non-root sont appelées transitions VMX.
Il existe deux types de transitions VMX :
- VM-entry : Transition entre une opération en mode VMX root et une opération en mode VMX non-root.
- VM-exits : Transition entre une opération en mode VMX non-root et une opération en mode VMX root.
Lorsque le processeur fonctionne en mode VMX root operation, il active un ensemble de nouvelles instructions, connues sous le nom d’instructions VMX. Ce mode permet également de charger des valeurs dans certains registres de contrôle, qui sont strictement limités.
En mode VMX non-root operation, le fonctionnement du processeur est modifié et restreint pour faciliter la virtualisation. Cela signifie que certaines instructions et certains événements déclenchent des vm-exits. Ces vm-exits interrompent le comportement normal du processeur, réduisant ainsi la capacité du logiciel à fonctionner en mode VMX non-root operation. Cette limitation est intentionnelle et permet au VMM de maintenir le contrôle strict sur les ressources du processeur.
Cycle VMM
Le schéma ci-dessous illustre le cycle d’un VMM et de son logiciel invité, ainsi que les interactions entre eux :
- VMM entre en fonctionnement VMX en exécutant une instruction VMX appelée VMXON.
- L’utilisation de VM-entry permet le passage de VMM vers le logiciel invité qui est une machine virtuelle. Les instructions VMX VMLAUNCH et VMRESUME permettent le VM-entry. VMM reprend le contrôle à l’aide d’un VM-exit.
- L’utilisation de VM-exit permet de transférer le contrôle à un point d’entrée spécifié par VMM, l’état de la VM est sauvegardé dans sa VMCS (Voir paragraphe suivant). VMM peut prendre les mesures appropriées quant à la cause du VM-exit et peut ensuite revenir à la VM en utilisant VM-entry tout en préservant son état lors du VM-exit.
- VMM peut décider de s’arrêter et de quitter le fonctionnement VMX operation en exécutant l’instruction VMX VMXOFF.
Virtual Machine Control Structure
Les VMX non-root operation et les transitions VMX sont contrôlées par une Virtual Machine Control Structure (VMCS).
La gestion de l’accès à la VMCS (Virtual Machine Control Structure) est assurée par un élément de l’état du processeur connu sous le nom de VMCS Pointer, celui-ci est unique pour chaque processeur logique. Ce pointeur a pour valeur, l’adresse 64-bits de la VMCS. Les opérations de lecture et d’écriture sur ce pointeur sont effectuées à l’aide d’instructions VMX spécifiques, telles que VMPTRST pour sauvegarder l’état actuel du pointeur et VMPTRLD pour charger un nouvel état dans le pointeur. Le VMM utilise des instructions telles que VMREAD qui est utilisée pour lire la valeur d’un champ spécifique de la VMCS et la stocker dans un registre, VMWRITE est utilisée pour écrire une valeur dans un champ spécifique de la VMCS à partir d’un registre et VMCLEAR qui permet d’effacer la VMCS spécifié pour configurer la VMCS.
Le VMM attribue une VMCS distincte à chaque VM qu’il gère. Dans le cas d’une VM qui utilise plusieurs processeur logiques, le VMM peut allouer une VMCS distincte à chaque processeur.
Lors d’un VM-exit, l’état de la VM est sauvegardé dans sa VMCS. VMM peut ensuite accéder à cette sauvegarde pour comprendre l’état de la VM au moment du VM-exit. Cela permet au VMM de prendre les mesures appropriées en fonction de la cause du VM-exit, puis de revenir à la VM en utilisant VM-entry tout en préservant son état précédent le VM-exit.
Schéma
Direct Execution of User Request
Direct Execution est une méthode dans laquelle les instructions ou les demandes de l’utilisateur sont exécutées directement sur le matériel physique sans aucune modification. Cette méthode est généralement utilisée pour les instructions qui sont supportées par le matériel sous-jacent, sans nécessiter de traduction. L’exécution directe peut améliorer les performances en réduisant le surcoût associé à la traduction des instructions.
Binary Translation of Guest OS Request
Binary Translation est quant à elle, une méthode utilisée pour exécuter des instructions ou des demandes du système d’exploitation invité (Guest OS) qui ne sont pas supportées par le matériel physique. Dans ce cas, les instructions du Guest OS sont traduites en instructions qui sont compatibles avec le matériel physique. Cette traduction peut être nécessaire pour des raisons de compatibilité, de sécurité ou pour permettre l’exécution de logiciels qui ne sont pas conçus pour fonctionner directement sur le matériel physique.
Hypercall
Un Hypercall est une demande spécifique faite par le système d’exploitation invité au VMM pour effectuer une opération qui nécessite une intervention du VMM. Les hypercalls sont souvent utilisés pour accéder à des ressources matérielles, pour gérer des interruptions, ou pour exécuter des instructions qui ne sont pas supportées dans le mode de virtualisation. Binary Translation est souvent utilisée pour gérer ces hypercalls, en traduisant les instructions du Guest OS en instructions qui peuvent être exécutées par le VMM ou le matériel physique.
Résumé
Direct Execution est utilisée pour exécuter directement les instructions supportées par le matériel physique, tandis que Binary Translation est utilisée pour traduire des instructions qui ne sont pas directement supportées. Les Hypercalls sont des demandes spécifiques faites par le Guest OS au VMM, qui peuvent nécessiter une traduction binaire pour être exécutées.
Analyse
Analyse du répertoire /sys d’OpenBSD
Après avoir récupéré le répertoire, Les fichiers intéressants, à savoir celle qui définit les constantes avec la valeur de la raison du vm-exit et ensuite trouver où est implémentée la fonction qui s’occupe de cette gestion. Les fichiers intéressants sont vmmvar.h et vmm_machdep.c.
Dans /usr/src/sys/arch/amd64/include/vmmvar.h, les constantes des raisons d’un vm-exit sont définit :
1
2
3
4
5
6
7
8
9
10
11
12
/* VMX: Basic Exit Reasons */
#define VMX_EXIT_NMI 0
#define VMX_EXIT_EXTINT 1
#define VMX_EXIT_TRIPLE_FAULT 2
#define VMX_EXIT_INIT 3
#define VMX_EXIT_SIPI 4
...
#define VMX_EXIT_INVPCID 58
#define VMX_EXIT_VMFUNC 59
#define VMX_EXIT_RDSEED 61
#define VMX_EXIT_XSAVES 63
#define VMX_EXIT_XRSTORS 64
La fonction qui implémente la gestion des raisons d’un vm-exit est présente dans le fichier /usr/src/sys/arch/amd64/amd64/vmm_machdep.c, elle se nomme vmx_handle_exit(). On peut la reconnaître grâce à son switch case sur les constantes dont il était question précédemment, avec comme entrée la valeur de la raison du vm-exit. Ci-dessous, un extrait du code de la fonction :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int
vmx_handle_exit(struct vcpu *vcpu)
{
uint64_t exit_reason, rflags, istate;
int update_rip, ret = 0;
update_rip = 0;
exit_reason = vcpu->vc_gueststate.vg_exit_reason;
rflags = vcpu->vc_gueststate.vg_rflags;
switch (exit_reason) {
case VMX_EXIT_INT_WINDOW:
if (!(rflags & PSL_I)) {
DPRINTF("%s: impossible interrupt window exit "
"config\n", __func__);
ret = EINVAL;
break;
}
ret = EAGAIN;
update_rip = 0;
break;
case VMX_EXIT_EPT_VIOLATION:
ret = vmx_handle_np_fault(vcpu);
break;
case VMX_EXIT_CPUID:
ret = vmm_handle_cpuid(vcpu);
update_rip = 1;
break;
...
case VMX_EXIT_MWAIT:
...
Désormais, toutes les informations nécessaires pour tenter de générer un vm-exit contrôlé sont récoltées.
POC
Concernant la partie POC, elle sera décomposée en plusieurs sous-parties allant de la création de la VM test jusqu’à la POC.
Création de la VM test dans OpenBSD
Créer un espace disque :
1
# vmctl create -s <TAILLE>G <NOM_DISQUE>.img
Ensuite il suffit de démarrer la VM à partir du fichier iso de l’OS souhaité :
1
# vmctl start -m <TAILLE_MEMOIRE>G -L -i 1 -r <CHEMIN_ISO> -d <DISQUE_CHEMIN> <NOM_VM>
Pour afficher l’état des VMs :
1
# vmctl show
Maintenant pour s’attacher à la console de la VM :
1
# vmctl console <NOM_VM>
Pour arrêter la VM :
1
# vmctl stop <NOM_VM>
Choix de la raison du vm-exit
Le choix de la raison CPUID semble être la plus approprié à déclencher, car elle est relativement simple à mettre en place :
1
2
3
4
5
6
7
#include <sys/syslog.h>
case VMX_EXIT_CPUID:
ret = vmm_handle_cpuid(vcpu);
update_rip = 1;
log(LOG_INFO, "Its won in CPUID vm exit reason !!!!");
break;
Reconstruisons maintenant le noyau et redémarrons pour le mettre à jour.
Reconstruire le noyau patcher
Pour modifier le nom du noyau :
1
2
3
# cd /usr/src/sys/arch/$(machine)/conf
# cp GENERIC.MP <NOM_NOYAU>.MP
# config <NOM_NOYAU>.MP
Enfin, pour reconstruire et installer :
1
2
3
4
# cd ../compile/<NOM_NOYAU>.MP
# make obj
# make config
# make && make install
Une fois la reconstruction et et l’installation terminés, il suffit de redémarrer la machine pour que le noyau se mette à jour. L’ancien noyau sera copié dans /obsd et le nouveau dans /bsd.
Coder un programme dans la VM test pour déclencher la raison du vm-exit CPUID
Dans ce petit programme, la dépendance cpuid.h est utilisé pour pouvoir la fonction __cpuid et déclencher la raison du vm-exit CPUID :
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <cpuid.h>
int
main()
{
uint32_t eax, ebx, ecx, edx;
printf("Setting up CPUID\n");
__cpuid(0, eax, ebx, ecx, edx);
printf("CPUID instruction successfully executed\n");
return 0;
}
L’output montre que l’instruction a bien été exécutée :
Maintenant, il ne reste plus qu’à vérifier dans /var/log/messages pour vérifier que le vm-exit a bien été effectué :
Conclusion
En conclusion, l’article a démontré la possibilité de générer un vm-exit contrôlé dans OpenBSD, une avancée significative pour l’optimisation des fuzzers noyau. La compréhension approfondie de la structure d’un hyperviseur et la capacité à modifier le code source du noyau ont permis de créer une Preuve de Concept (POC) pour déclencher un vm-exit spécifique.
L’analyse détaillée des fichiers de configuration et du code source du noyau a révélé les points clés de gestion des vm-exit, offrant ainsi une base solide pour l’implémentation d’un fuzzer noyau plus efficace. L’approche a été un succès en utilisant l’instruction CPUID.
Enfin, l’article met en lumière l’opportunité d’appliquer des techniques de fuzzing par couverture de code dans un environnement virtualisé, ce qui peut conduire à des améliorations significatives dans la détection des vulnérabilités et la qualité générale du logiciel.
Bibliographie
Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4 OpenBSD - Virtualization OpenBSD - Anonymous CVS Man OpenBSD - log Man OpenBSD - release Coverage-guided fuzzing