Les gestionnaires de machines virtuelles (ou VMMs) sont de plus en plus utilisés dans le monde de l'informatique aujourd'hui (notamment, depuis l'engouement pour le Cloud Computing). Ils reposent principalement sur des techniques d'assistance matérielle à la virtualisation proposées par les di érents fondeurs de processeurs. Par ailleurs, il est de plus en plus courant de rencontrer des couches de virtualisation imbriquée ou nested virtualization, également supportée par des techniques matérielles. Cette approche a pour e et d'augmenter le nombre de couches de virtualisation. En partant de ce constat, nous nous sommes penchés sur la conception et le développement d'un hyperviseur récursif , permettant de supporter un nombre quelconque de couches de virtualisation. Nous décrivons son architecture, ainsi qu'un modèle formel décrivant de façon simple comment gérer ces couches de virtualisation. Quelques tests permettant d'évaluer ses performances sont également proposés à la n de cet article.
Les gestionnaires de machines virtuelles (ou VMM pour Virtual Machine Monitor) sont aujourd'hui couramment utilisés dans le monde informatique, en particulier depuis l'engouement pour le Cloud Computing. Leur objectif principal est de contrôler l'exécution de machines virtuelles en pro tant d'un niveau de privilège elevé du processeur et ils comportent de plus en plus de fonctionnalités. La majorité des VMMs du marché pro tent aujourd'hui des techniques matérielles d'assistance à la virtualisation pour exécuter et contrôler des machines virtuelles. L'installation d'un hyperviseur destiné à virtualiser un VMM et utilisant pour cela lui-aussi les techniques matérielles d'assistance à la virtualisation nécessite donc la gestion de la virtualisation imbriquée (ou nested virtualization). Les processeurs récents aujourd'hui fournissent des fonctionnalités pour faciliter la mise en place de la nested virtualization. Mais, par ailleurs, les VMMs du marché utilisent déjà pour beaucoup d'entre eux ces mécanismes de nested virtualization car ils peuvent être amenés eux-mêmes à virtualiser des VMMs. Les VMMs sont donc devenus de plus en plus complexes au l du temps. Cette complexité s'accompagne d'une forte probabilité d'introduction de vulnérabilités. Ils deviennent donc des cibles très intéressantes pour un attaquant. E ectivement, leur compromission assure à l'attaquant une prise de contrôle sur un niveau d'exécution très privilégié du processeur.
Il est donc fondamental aujourd'hui de pouvoir détecter la compromission de VMMs. Cette détection peut se faire notamment en installant un logiciel capable de virtualiser ce VMM. Cette virtualisation permet ainsi d'observer et contrôler les di érentes opérations e ectuées par le VMM, a n de caractériser son comportement normal et détecter les compromissions. Aussi, si l'on désire créer un hyperviseur destiné à virtualiser un VMM, on réalise rapidement qu'il doit être capable de supporter un nombre quelconque de couches de virtualisation. A partir de ce constat, nous nous sommes donc intéressés à la création d'un hyperviseur capable de gérer ce problème de multiples couches de virtualisation. Pour cela, nous avons expérimenté un hyperviseur récursif , au sens où il est capable de se virtualiser un nombre quelconque de fois.
De la même manière que les processeurs gèrent plusieurs anneaux de protection pour séparer les privilèges, la sécurité liée à l'usage de la virtualisation doit également passer par une séparation des privilèges. Cependant aujourd'hui, un seul niveau de privilèges supplémentaire est ajouté aux processeurs récents pour gérer la virtualisation. Il est donc important de penser une architecture logicielle pour gérer cette séparation des privilèges au sein de l'hyperviseur. On pourrait ainsi imaginer l'implémentation de fonctionalités de sécurité plus ou moins critiques dans ces di érents niveaux de privilèges. Pour implémenter cette séparation des privilèges au sein de ce même hyperviseur, la virtualisation imbriquée est une des solutions envisageables, l'hyperviseur de plus bas niveau étant dédié aux fonctions les plus critiques et celui de plus haut niveau étant dédié aux fonctions les moins critiques.
Concevoir et implémenter un tel hyperviseur de sécurité implique donc la maîtrise de la gestion d'un nombre quelconque de niveaux de virtualisation. C'est le but de l'expérimentation réalisée dans cet article.
Nous nous sommes donnés, dans un premier temps, les objectifs suivants. Concevoir et implémenter un hyperviseur :
D'autres articles se sont focalisés sur la virtualisation imbriquée [3, 1, 6, 7]. Mais, à notre connaissance, il n'existe pas d'implémentation d'un hyperviseur minimaliste, récursif, bare-metal et générique sur laquelle des tests de perfomance peuvent être réalisés et des fonctions de sécurité peuvent être instanciés sur di erents niveaux. Cet article est un premier pas vers cette architecture et propose en particulier un modèle, une implémentation préliminaire sur x86 et des tests. Cet hyperviseur est baptisé Abyme.
Le plan de l'article est donc le suivant. La section 2 présente l'architecture de notre hyperviseur tandis que la section 3 détaille le modèle sur lequel est basé le fonctionnement de cet hyperviseur. La section 4 propose quelque détails d'implémentation et la section 5 donne les résultats de quelques tests de performance que nous avons réalisés. Finalement, la section 6 conclut cet article.
Cette section décrit l'architecture globale d'Abyme. Les détails d'implémentation sont abstraits autant que possible. Il est néanmoins important de connaître le fonctionnement de base de l'architecture x86 ainsi que celui des extensions de virtualisation Intel VT-x. Après des rappels technologiques présentés dans la section 2.1, la stratégie de virtualisation classique du matériel est décrite dans la partie 2.2 et la stratégie de virtualisation du jeu d'instruction VMX des extensions de virtualisation Intel est décrite dans la partie 2.3. En n, dans la partie 2.4, les principaux concepts de virtualisation récursive sont introduits.
VT-x [2], pour Virtual Technology, apporte le support matériel pour la virtualisation de l'architecture x86. Pour ce faire, trois éléments sont ajoutés à l'architecture : 1) un jeu d'instructions appelé VMX incluant 13 instructions, 2) un niveau de virtualisation de la mémoire et 3) un mode d'exécution avec deux sous-modes respectivement appelés VMX operation, VMX root operation et VMX non-root operation.
Des machines virtuelles (VM ou guests) sont exécutées sur des c urs virtuels con gurés avec des structures internes du processeur appelées Virtual Machine Control Structures (VMCS). Les VMs sont exécutées en mode VMX non-root operation, ce qui signi e qu'elles sont sous le contrôle de l'entité logicielle appelée le Virtual Machine Monitor (VMM ou host) qui s'exécute en mode VMX root operation. Pour créer et contrôler une VM, le VMM charge une VMCS, avec l'instruction VMX vmptrld. Puis, il prépare l'état du guest en accédant à la VMCS chargée avec les instructions vmread et vmwrite pour renseigner à la fois son propre état, celui de la VM ainsi que des contrôles d'exécution. En n la VM est démarrée avec l'instruction vmlaunch. Le VMM prend le contrôle sur la VM au travers d'interruptions de machines virtuelles appelées VM Exits. Après avoir traité correctement le VM Exit, dans le cas nominal, le VMM redonne la main à la VM grâce à l'instruction VMX vmresume ( gure 1).
Le VMM protège son propre espace mémoire et isole les régions mémoire de la machine virtuelle en utilisant la couche de virtualisation supplémentaire de la MMU appelée Extended Page Tables (EPT). Le principe d'EPT est quasiment identique à celui de la pagination IA-32e classique : découpage d'une adresse en indices utilisés pour naviguer dans une arborescence, sachant que la feuille de l'arbre contient le résultat de la traduction. Avec EPT, les adresses physiques de la VM appelées guest physical addresses sont traduites en host physical addresses, utilisées par le contrôleur mémoire.
Pour plus de détails sur les points techniques, et l'architecture x86, le lecteur peut se référer à [4].
Abyme est un hyperviseur employant la technique dite de full virtualization, mettant en place un environnement d'exécution où le logiciel invité se comporte comme si le matériel n'était pas virtualisé voire n'en est même pas conscient. Pour garder le contrôle sur le matériel, l'hyperviseur doit contrôler, émuler ou exécuter correctement les intructions privilégiées de la VM, sans pour autant altérer son fonctionnement. Pour ce faire, il va masquer aux yeux de la VM son espace mémoire et le matériel qu'il va s'accaparer. Pour éviter de placer le système dans un état incohérent, ce masquage doit être réalisé avant le chargement du système d'exploitation. Aussi, Abyme est un hyperviseur qui se présente sous la forme d'un pilote UEFI de runtime. Sa stratégie de chargement est expliquée dans [4]. Dans l'implémentation actuelle, seule une carte réseau est accaparée par Abyme.
La carte réseau accaparée par Abyme est utilisée pour communiquer avec un client de contrôle et de débogage. Pour cela, il doit protéger son espace de con guration en PIO et MMIO. Les registres des cartes réseaux d'aujourd'hui étant la plupart du temps mappés en mémoire, il doit aussi les protéger directement pour prévenir un accès direct à ces zones mémoire par un logiciel malveillant. Le masquage de l'espace de con guration permet, comme son nom l'indique, de masquer la présence d'un matériel donné (e.g. notre carte réseau), alors que la protection des registres empêche sa recon guration.
La protection PIO s'e ectue grâce au contrôle des deux ports spéci ques 0xcf8 et 0xcfc de l'espace des entrées/sorties. L'utilisation de ces ports s'e ectue en deux étapes. Tout d'abord, le logiciel doit écrire l'adresse du registre PCI à accéder dans le port 0xcf8 via l'instruction out. L'adresse PIO est calculée en fonction de l'adresse PCI bus :device.function du périphérique à protéger. Il va dans un second temps exécuter l'instruction in ou out pour lire ou écrire le contenu du registre passé en paramètre lors de la première phase.
VT-x propose un contrôle d'accès à l'espace des entrées sorties via les I/O bitmaps. Grâce à elles, on peut décider de prendre la main systématiquement sur l'exécution de la VM lors de tentatives d'accès aux ports spéci és dans celles-ci, pour exercer un contrôle adéquat. L'appropriation de la carte réseau se réduit donc au contrôle en lecture et écriture sur le port 0xcfc qui succède à l'écriture de son adresse PIO sur le port 0xcf8. Les écritures sont ignorées. Lors de lectures, on retourne un mot de la bonne taille, avec ses octets à 0xff, pour indiquer l'absence de périphérique à l'adresse de la carte réseau.
Pour la protection MMIO, l'hyperviseur con gure EPT pour indiquer au logiciel invité que la carte réseau n'est pas présente. Connaissant l'adresse de base des accès MMIO donnée par le registre PCI MMCONFIG, ainsi que l'adresse PCI de notre carte, nous pouvons calculer simplement l'adresse MMIO de son espace mémoire a n de le remapper vers une page contenant des octets à 0xff.
EPT est aussi utilisée pour protéger l'espace mémoire de l'hyperviseur. Abyme gère une unique machine virtuelle. Il peut donc simplement con gurer l'espace mémoire physique en identity mapping (sauf MMIO pour la carte réseau), avec tous les droits. Les pages correspondant à son espace mémoire sont par contre interdites d'accès en lecture, écriture et exécution gràce aux attributs des Page Table Entries (PTE).
L'hyperviseur est maintenant capable de protéger son espace mémoire et ses périphériques. Il doit maintenant protéger l'accès à certains registres et à l'exécution par la VM de certaines instructions pouvant remettre en cause son bon fonctionnement. Notamment, les accès aux registres de contrôle cr0 et cr4 doivent être contrôlés car certains bits sont nécessaires pour une exécution correcte lorsque les extensions de virtualisation sont activées (VMX operation). Pour cela, l'hyperviseur est noti é des accès en écriture aux bits protégés grâce aux masques guest host cr0 et guest host cr4 et va écrire les modi cations dans des versions fantômes ou shadow stockées dans la VMCS. Ce sont ces versions shadow qui seront automatiquement lues plus tard par la VM.
En n, d'autres instructions génératrices inconditionnelles de VM Exits seront simplement exécutées sur le processeur car non dangeureuses pour le bon fonctionnement d'Abyme (i.e. cpuid, xsetbv). D'autres instructions sont également génératrices inconditionnelles de VM Exits, celles du jeu d'instruction VMX. Leur gestion par l'hyperviseur est décrite dans la partie suivante.
La virtualisation du jeu d'instruction VMX est un point technique clé de cet article. Dans le cadre de la full virtualization, un hyperviseur virtualisé, ne sachant pas qu'il s'exécute sur un c ur virtuel, va tenter à son tour d'activer les extensions de virtualisation et utiliser les 13 instructions tout au long de son exécution. Dans cette partie, nous décrivons le travail qu'un hyperviseur doit réaliser pour virtualiser ces instructions. Une très grande partie du travail d'implémentation de cette fonctionnalité a constitué à reproduire un comportement aussi proche que possible de celui décrit dans le manuel du développeur Intel [2]. Le fonctionnement et l'utilité des instructions principales sont rappelés avant d'expliquer comment les virtualiser de manière simple. Certains tests matériels très précis ne sont pas décrits ici.
Abyme a pour but de virtualiser les intructions VMX. Cela signi e qu'il doit reproduire leur comportement nominal, mais aussi celui de leur gestion des erreurs. La gestion des erreurs pour ce jeu d'instructions est très simple et se comporte de la manière suivante. Quatre états d'erreur ou de succès sont générés par ces instructions, VMsucceed, VMfail et ses deux sous étatsVMfailValid et VMfailInvalid. À chaque état correspond une combinaison des bits du registre RFLAGS. Dans le cas de VMfailValid, un code d'erreur est inscrit dans le champ VM-instruction error de la VMCS courante. La manipulation du registre RFLAGS et du champ VM-instruction error su sent à injecter un évènement lié à la virtualisation dans un hyperviseur virtualisé. Dans cette partie nous ne traitons pas le cas des véri cations classiques pouvant être e ectuées sur des instructions privilégiées, comme le test du niveau de privilège, du mode du processeur, les exceptions de la gestion des privilèges, etc.
Pour chaque instruction prenant en paramètre un pointeur de VMCS, les véri cations suivantes sont e ectuées : si l'adresse de la VMCS passée en paramètre n'est pas alignée sur 4ko ou si son identi ant de révision est mauvais, nous retournons l'état VMfailInvalid.
La tentative d'exécution d'une des intructions de VMX par le logiciel invité génère systématiquement un VM Exit. Pour chaque VM Exit généré, Abyme émule l'instruction et redonne la main à l'hyperviseur virtualisé avec l'instruction vmresume.
Nous passons à présent en revue chacune des intructions VMX pour indiquer comment nous pouvons les émuler.
vmxon(VMCS *) est la première instruction du jeu VMX utilisée par un hyperviseur pour activer le mode VMX operation. Elle est aussi la seule exécutable par le processeur en dehors de ce mode. Mis à part les véri cations sur le pointeur, il n'y a rien à e ectuer pour cette instruction.
vmclear(VMCS *) permet de passer une VMCS à l'état clear pour pouvoir lancer sa VM associée avec vmlaunch. Pour l'émuler, nous allons charger avec vmprtld la VMCS passée en paramètre, appeler vmclear sur cette VMCS et en n, recharger la VMCS de l'hyperviseur virtualisé.
vmptrld(VMCS *) permet de changer la VMCS courante, qui sera implicitement utilisée par les instructions vmread, vmwrite, vmlaunch, vmresume, etc. Pour émuler cette instruction, il su t simplement de copier le pointeur de VMCS. Nous appellerons ce pointeur le shadow VMCS pointer pointant vers une shadow VMCS, conformément à la documentation Intel. L'hyperviseur n'e ectue immédiatement pas de vmptrld sur ce pointeur car il va rendre la main à la VM ayant exécuté le vmptrld émulé. La shadow VMCS sera chargée plus tard lors de l'émulation d'instructions comme vmread et vmwrite.
vmlaunch permet de démarrer l'exécution d'une VM con gurée par une VMCS dont l'état est clear. Dans le cas de l'émulation, l'hyperviseur virtualisé tente de démarrer l'exécution d'une machine virtuelle. Cette machine virtuelle correspond à la VMCS que l'hyperviseur virtualisé a précédement chargé avec l'instruction vmptrld. Cette VMCS est alors associée au pointeur shadow VMCS pointer. Il su t donc à Abyme de copier cette VMCS dans une VMCS lui appartenant, en exécutant un vmptrld sur la shadow VMCS, une série de vmread, un vmptrld pour charger sa VMCS, et en n une autre série de vmwrite, en faisant attention aux champs compromettant son intégrité. Il termine en exécutant vmlaunch ou vmresume (cf. section 4).
vmresume permet de continuer l'exécution d'une VM con gurée par une VMCS dont l'état est launched. L'hyperviseur virtualisé tente de reprendre l'exécution d'une machine virtuelle qu'il a con guré dans la shadow VMCS. Abyme e ectue la même copie d'état du vmlaunch puis exécute vmresume (cf. section 4).
vmwrite(field, value) permet d'accéder en écriture au champ eld de la VMCS courante. Pour ce faire, l'hyperviseur charge la shadow VMCS, i.e. la VMCS courante de l'hyperviseur virtualisé, exécute le vmwrite(field, value) et recharge sa VMCS.
vmread(field) permet d'accéder en lecture au champ eld de la VMCS courante. La même stratégie que pour vmwrite est appliquée, les valeurs de retours étant copiée dans sa VMCS.
Supporter la virtualisation récursive signi e qu'un hyperviseur doit pouvoir virtualiser son propre code N fois, en ne sachant pas si lui-même ne l'est pas et par combien de couches sous-jacentes. La seule chose qu'il peut déduire étant le nombre de niveaux au dessus de lui.
Dans tous les cas, lors d'un VM Exit généré par une des machines virtuelles de cette pile, le contrôle sera toujours repris par l'hyperviseur chargé en premier et maître du matériel, l0. Or, tous les évènements (VM Exits) générés au niveau x par lx doivent être traités au niveau x