Virtualisation et sécurité

Virtualisation et machines virtuelles

Un ordinateur est un système complexe comprenant notamment un ou plusieurs processeurs, de la mémoire, des disques, un clavier, une souris, un écran, et beaucoup d’autres systèmes… Si les programmeurs d’applications devaient comprendre en détail le fonctionnement de tous ces dispositifs, il serait quasiment impossible d’écrire du code, et a fortiori du code utilisant de manière optimisée ces composants. Pour cette raison, les ordinateurs offrent une couche d’abstraction permettant d’utiliser de manière simplifiée ces dispositifs : les systèmes d’exploitation [116]. Les systèmes d’exploitation permettent donc aux processus d’accéder de manière simple et optimisée à ces ressources. Au sein des systèmes d’exploitations, plusieurs processus sont généralement exécutés simultanément. Ces différents logiciels ont besoin d’accéder aux mêmes ressources. Afin de permettre à ces processus de s’exécuter indépendamment les uns des autres, les systèmes d’exploitations mettent en œuvre des mécanismes d’isolation. Grâce à ces techniques, plusieurs processus peuvent utiliser simultanément ou chacun leur tour les mêmes ressources, indépendamment des autres. Cela simplifie largement la coopération de tels processus. La virtualisation [14] est une technique permettant de créer des abstractions « virtuelles » d’une ressource (mémoire, isolation d’exécution, …). En ajoutant un niveau d’abstraction entre la ressource physique et la ressource virtuelle, il est possible de présenter à l’application une version abstraite de cette ressource qui sera utilisable indépendamment de l’implémentation réelle de la ressource. Ainsi, le domaine virtualisé peut agir de manière isolée des autres domaines virtualisés ou non. Le virtualiseur a le contrôle sur les éléments à présenter et à cacher à l’interface virtuelle. Puisque la virtualisation permet de ne montrer que des sous-ensembles de la ressource virtualisée, il est possible de créer une vue spécifique au domaine virtualisé et de l’isoler du reste du système. Cette isolation est aussi due au fait qu’il est impossible pour la machine virtuelle d’accéder directement aux ressources physiques : elle doit toujours accéder aux ressources virtualisées, spécifiques à cette unique machine virtuelle.

La virtualisation se base sur trois techniques : 1) le multiplexage, permettant de présenter plusieurs instances virtuelles d’une ressource indépendantes entre elles et basées sur la même ressource sous-jacente afin de partager cette ressource entre les différentes instances virtualisées, 2) l’agrégation, permettant de présenter plusieurs ressources physiques sous forme d’une seule ressource virtuelle et 3) l’émulation, permettant de « simuler » une ressource physique à partir d’une autre ressource compatible. La ressource émulée est virtuelle .

La virtualisation est une approche utilisée dans de très nombreux domaines. Il est ainsi possible de virtualiser à la fois les ressources systèmes (processeur, mémoire vive, carte réseau, dispositif de stockage…) et l’environnement d’exécution (isolation entre les logiciels, isolation de l’espace utilisateur pour créer des conteneurs, isolation de ressources systèmes pour créer une machine virtuelle complète). Dans les années 60 le terme de virtualisation désignait les techniques permettant l’isolation mémoire des processus entre eux [2]. Aujourd’hui, le terme de virtualisation désigne généralement la création de machines virtuelles au sens large, c’est à dire d’environnements d’exécution qui s’appuient sur des ressources virtuelles fournies par l’hôte (via l’hyperviseur, le moteur de conteneurisation, …) similaires aux ressources physiques de l’hôte. Dans ce chapitre, nous mettons en italique le terme de machine virtuelle au sens large pour ne pas le confondre avec les domaines virtualisés liés à la virtualisation matérielle et à l’émulation. Dans ce mémoire, nous nommons ’virtualisation lourde’ les formes de virtualisation appartenant à l’une de ces deux techniques.

Des machines virtuelles peuvent être utilisées à des fins très différentes. Les types de machines virtuelles les plus courants sont :
— Machines virtuelles de langage de programmation Certains langages, comme le Java ou le C#, utilisent une machine virtuelle (respectivement la JVM (Java Virtual Machine) [120] et le MSCLR (MicroSoft Common Language Runtime) [48]) pour exécuter les programmes compilés sous forme de langage intermédiaire. Ces machines virtuelles particulières ne visent qu’à exécuter une seule application et sont donc en dehors du champ d’intérêt de cette thèse.
— Machines virtuelles lourdes La virtualisation complète [14], aussi appelée virtualisation lourde permet d’exécuter des machines virtuelles qui se comportent comme des ordinateurs complets. Les ressources d’exécution exposées aux machines virtuelles sont identiques à celles du système physique. Il est donc possible d’y d’installer des systèmes d’exploitation et d’y lancer des programmes comme sur la machine physique. Les machines virtuelles sont totalement isolées entre elles et vis-à-vis de l’hôte, ainsi sauf attaque très spécifique (attaque par canal auxiliaire [107], exploitation d’une faille de l’hyperviseur [92]) il est impossible pour une machine virtuelle de voir l’extérieur de son domaine virtualisé.
— Machines virtuelles légères Les machines virtuelles légères sont des machines virtuelles pour lesquelles seule une partie du système d’exploitation est virtualisée et une partie du système appartient à l’hôte. Typiquement, l’espace utilisateur est virtualisé alors que le noyau appartient au système hôte. On parle alors de virtualisation de niveau système d’exploitation [16]. Les conteneurs [90] et les sandboxes appartiennent à cette catégorie. La différence entre virtualisation légère et lourde est donc le placement de la frontière de virtualisation [119].

Toutes les formes de virtualisation système évoquées ici permettent effectivement d’exécuter du code de manière isolée du reste du système. L’élément de différenciation entre ces différentes techniques est le placement de la frontière de virtualisation, qui impacte directement l’architecture du domaine virtualisé et la manière avec laquelle il communique avec le système hôte. Il est possible de placer la frontière de virtualisation à de nombreux endroits différents comme détaillé dans le reste de ce chapitre. Le placement de la frontière de virtualisation est un enjeu très important pour les raisons suivantes :

— Performances Le placement de la frontière de virtualisation a un impact sur le temps d’exécution des machines virtuelles puisqu’elle implique plus ou moins d’opérations pour implémenter la virtualisation de l’environnement. Cela a aussi un impact important sur la taille des entités virtualisées puisque cela implique que les machines virtuelles doivent intégrer plus ou moins d’éléments système dans la zone virtualisée pour fonctionner correctement. Par exemple, dans le cadre de la virtualisation lourde, la machine virtuelle contient un noyau complet. Ce n’est pas le cas des conteneurs pour lequel seul l’espace utilisateur est virtualisé et le noyau est partagé avec l’hôte. Des comparaisons de performances entre les conteneurs et les machines virtuelles lourdes sont réalisées dans [34] et plus récemment dans [94]. Ces études montrent que les performances des conteneurs se rapprochent de celles de processus natifs dans la plupart des benchmarks, mais que l’utilisation de machines virtuelles lourdes implique généralement une perte de performances non négligeable.
— Sécurité Puisque les abstractions permettant la communication entre les domaines virtualisés et l’hôte diffèrent selon la forme de virtualisation utilisée, les différentes formes de virtualisation possèdent des surfaces d’attaque différentes. Ainsi, un conteneur, qui utilise directement le noyau de l’hôte, utilisera pour communiquer avec celui-ci une grande API (Application Programming Interface) et aura donc une surface d’attaque plus importante qu’une machine virtuelle qui ne partage que des ressources d’exécution virtuelles, ce qui représente une ABI (Application Binary Interface) largement plus restreinte. Une étude comparative de la sécurité des différentes formes de virtualisation est réalisée dans [22]. Cette étude montre que les différents types de virtualisation sont plus ou moins vulnérables aux attaques selon leur type et que les attaques peuvent venir de beaucoup de vecteurs différents. Ces études présentent également les contremesures pouvant être mises en place pour améliorer la sécurité de ces abstractions.
— Facilité de déploiement Le niveau de la frontière de virtualisation influe sur la facilité de déploiement de la machine virtuelle. Ainsi, dans le cadre des machines virtuelles lourdes il est nécessaire d’installer complètement l’OS pour mettre en place le système. Toute modification doit être faite depuis l’intérieur de la machine (sauf via des techniques d’introspection ou des connections ssh), ce qui peut s’avérer complexe. Au contraire, puisque les conteneurs partagent le noyau avec l’hôte, il est possible de configurer le conteneur finement, y compris en mettant des protections de niveau noyau depuis l’hôte.
— Actions de sécurité pouvant être mises en place depuis l’hôte Pour les conteneurs, il est possible de réaliser des vérifications de sécurité depuis l’hôte. Ces protections visent à garantir que le système s’exécute comme prévu. Ces vérifications incluent notamment l’ajout de contrôles d’accès basés sur des règles (RBAC : Rule Based Access Control), la réalisation de contrôle d’intégrité et la mise en place de limites d’utilisation de certaines ressources. Au contraire, puisque le noyau du guest (machine virtuelle) n’est pas censé être accessible/modifiable depuis le kernel hôte (machine physique), ces protections ne sont pas possibles pour des machines virtuelles lourdes depuis le noyau hôte sauf via des techniques d’introspection, imparfaites et coûteuses en performances.

Il est à noter que les techniques de virtualisation ne sont pas exclusives et peuvent dans certains cas être cumulées. Ainsi, l’isolation mémoire des processus entre eux est aujourd’hui appliquée dans tous les systèmes d’exploitation et peut par exemple être cumulée avec la conteneurisation pour assurer un niveau d’isolation supérieur. Similairement, on trouve dans certains environnements où la sécurité est un enjeu majeur, par exemple le NFV (Network Function Virtualization) [125], des conteneurs encapsulés dans des machines virtuelles lourdes pour profiter de la simplicité de déploiement des conteneurs tout en bénéficiant des propriétés de sécurité des machines virtuelles lourdes au prix d’une baisse des performances liée à l’addition de couches logicielles de virtualisation.

Isolation des processus

Le concept de virtualisation est apparu dans les années 1960, pour permettre l’exécution de plusieurs applications/processus simultanément sur le même ordinateur (mainframe), permettant ainsi de réduire les coûts d’exploitations très importants de ces machines. Cette virtualisation est réalisée en divisant les ressources systèmes entre les différentes applications. Techniquement, cette isolation repose sur l’utilisation de mémoire virtuelle, permettant l’utilisation par tous les logiciels d’adresses virtuelles et la traduction de ces adresses virtuelles en adresses physiques de la mémoire vive et celle d’espaces d’adressage virtuels.

Ainsi, ces techniques permettent d’interdire à tout processus d’accéder à la mémoire des autres processus. Si un processus se comporte de manière incorrecte ou malveillante, il n’est pas en mesure d’attaquer directement les autres processus par exemple en modifiant la valeur d’une adresse mémoire appartenant aux autres processus, améliorant ainsi le niveau de sécurité des systèmes. Toutefois, même avec l’isolation des processus, tous les logiciels sont en mesure de voir l’intégralité du système (les autres processus exécutés, les systèmes de fichiers, …). Ce niveau d’isolation reste donc faible et ne suffit pas pour isoler les processus entre eux.

Sandboxes

Les sandboxes sont un mécanisme de niveau système d’exploitation permettant d’exécuter un processus ou des ensembles de processus dans un environnement système plus restreint. Un tel mécanisme permet notamment d’exécuter du code qui n’est pas de confiance en limitant au maximum ses possibilités, réduisant ainsi les risques d’attaques. La commande chroot [40] est une première étape de sandboxing, permettant de changer le répertoire racine d’un processus. Ceci est notamment utile pour la création de shells protégés. Sous Linux, il est possible de restreindre les ressources systèmes visibles (et donc accessibles) à des ensembles de processus à l’aide d’espaces de nommages (namespaces)[42]. Chaque namespace isole une ressource de telle manière à ce que les processus dans les namespaces ne voient la ressource protégée que si elle appartient à la même namespace. Par exemple, un processus dans une namespace PID n’est en mesure de voir que les processus appartenant à la même namespace PID. Cela les isole de fait du reste du système.

Virtualisation lourde 

Description technique

La virtualisation lourde ou matérielle est un type de virtualisation où les primitives virtualisées sont les ressources système, c’est à dire le processeur la RAM, les I/O… Dans ce cadre, les machines virtuelles sont des systèmes d’exploitation complets comprenant à la fois l’espace noyau et utilisateur.

Il existe plusieurs types de virtualisation lourde :
— Émulation de machine Les machines sont implémentées en tant qu’application en espace utilisateur classique et émulent totalement la machine à virtualiser. Puisque la machine doit être totalement interprétée, cela induit une perte de performances considérable. Typiquement, la vitesse d’exécution est réduite par un facteur 5 à 1000 [14].
— Hyperviseur Les hyperviseurs permettent une exécution directe sur le CPU pour réduire les pertes de performances. Les hyperviseurs mettent en place un environnement spécifique pour la machine virtuelle pour que celle-ci puisse s’exécuter directement sur le processeur réel. Quand une instruction assembleur doit utiliser une ressource virtuelle, elle est interceptée (trap). L’action à réaliser est alors émulée par l’hyperviseur. Dans les implémentations plus récentes, il est possible de ne trapper que les instructions assembleur sensibles, améliorant ainsi les performances de la machine.

Un hyperviseur peut s’exécuter directement sur des ressources matérielles (bare metal). On parle alors d’hyperviseur de type 1. Au contraire, on peut avoir des hyperviseurs qui s’exécutent au-dessus d’un système d’exploitation. On parle alors d’hyperviseur de type 2. Les processeurs récents fournissent des extensions liées à la virtualisation (Intel VTx [118], AMD-V [3]), permettant ainsi de réaliser matériellement la virtualisation via des instructions assembleur spécifiques (VMCALL, VMLAUNCH, VMWRITE …) pour exécuter directement toutes les instructions de la machine virtuelle sur le processeur sans avoir besoin de trapper et émuler certaines instructions. On parle alors de virtualisation matérielle. La paravirtualisation est une autre technique dans laquelle l’hyperviseur réalise des changements dans le système d’exploitation de la machine virtuelle afin de la rendre compatible avec le matériel sous-jacent. La paravirtualisation peut donc être utilisée même sans support matériel de la virtualisation. Enfin, on parle de virtualisation complète ou logicielle lorsque l’hyperviseur est exécuté sans support matériel sur des OS non modifiés.

Le rapport de stage ou le pfe est un document d’analyse, de synthèse et d’évaluation de votre apprentissage, c’est pour cela chatpfe.com propose le téléchargement des modèles complet de projet de fin d’étude, rapport de stage, mémoire, pfe, thèse, pour connaître la méthodologie à avoir et savoir comment construire les parties d’un projet de fin d’étude.

Table des matières

Introduction générale
1 Virtualisation et sécurité
1.1 Virtualisation et machines virtuelles
1.2 Isolation des processus
1.3 Sandboxes
1.4 Virtualisation lourde
1.4.1 Description technique
1.4.2 Opportunités et enjeux de sécurité
1.5 Conteneurisation
1.5.1 Espaces de nommages Linux
1.5.2 Cgroups
1.5.3 Capabilities
1.5.4 Modules de Securité Linux
1.5.4.1 SELinux
1.5.4.2 AppArmor
1.5.4.3 IMA
1.5.5 Moteurs de conteneurisation usuels
1.5.6 Moteurs d’orchestration usuels
1.5.7 Opportunités et enjeux de sécurité
1.6 Autres formes de virtualisation
1.6.1 Machines virtuelles légères
1.6.2 Unikernel
1.7 Comparaison des différentes formes de virtualisation
2 Défenses contre les attaques
2.1 Introduction : les grands enjeux de la sécurité
2.1.1 Attaques informatiques : approche historique
2.1.2 Types d’Attaques et conséquences
2.1.3 Divulgation et correction des failles
2.2 Contremesures contre les attaques
2.2.1 Correctif de sécurité
2.2.2 Réduction de privilèges
2.2.3 Mitigation spécifique
2.3 Failles conteneurs
2.3.1 Mauvaise configuration du conteneur
2.3.2 Vulnérabilités logicielles
2.3.3 Failles système
2.4 Taxonomie des défenses conteneur
2.5 Défenses basées sur la configuration
2.5.1 Configuration du moteur de conteneurisation
2.5.2 Vérification d’intégrité
2.5.3 Sécurité Réseau
2.6 Défenses basées sur le code
2.6.1 Landlock LSM version<14
2.6.2 Mécanismes de défenses globaux eBPF
2.6.3 BPFBox et BPFContain
2.6.4 Seccomp-bpf
2.6.5 Falco
2.7 Défenses basées sur des règles de sécurité
2.7.1 Security Namespaces
2.7.2 Landock LSM version≥14
2.7.3 Pledge
2.8 Comparaison des différentes approches et frameworks
2.8.1 Comparaison des approches existantes
2.8.2 Comparaison des frameworks existants
2.9 Programmabilité du noyau et opportunités de sécurité
2.9.1 Considérations générales
2.9.2 Linux Security Modules
2.9.3 extended Berkelet Packet Filter (eBPF)
2.9.3.1 Présentation technique
2.9.3.2 eBPF non privilégié
3 SNAPPY
3.1 Besoin de politiques de sécurité plus flexibles
3.1.1 Contexte
3.1.2 Amélioration de la sécurité des conteneurs
3.1.3 Approche SNAPPY
3.1.4 Contributions
3.2 Design et implémentation
3.2.1 Design Global
3.2.2 Espace de nommage Linux policy_ns
3.2.2.1 Justification du choix technique de la namespace
3.2.2.2 Design et implémentation
3.2.2.3 Adaptation aux moteurs de conteneurisation système
3.2.2.4 Politiques à états
3.2.3 Politiques de sécurité SNAPPY
3.2.3.1 Justification de l’utilisation d’eBPF
3.2.3.2 Mise en place de politiques de sécurité SNAPPY
3.2.4 Helpers dynamiques
3.2.4.1 eBPF et helpers statiques
3.2.4.2 Design des helpers dynamiques
3.2.4.3 Génération et chargement
3.2.4.4 Utilisation des helpers genériques
3.2.5 Analyse de sécurité
3.3 Intégration avec les primitives Linux
3.3.1 Binaires usuels liés à la gestion de namespace
3.3.2 OCI
3.3.3 Dockerfile
3.4 Cas d’usages
3.4.1 Réduction de surface d’attaque
3.4.2 Mitigation dynamique de vulnérabilité
3.5 Comparaison avec l’état de l’art
3.6 Limites de SNAPPY
Conclusion générale

Lire le rapport complet

Télécharger aussi :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *