Comprendre la Mémoire Virtuelle et la Simulation du Bit NX : Voyage au Coeur du x86 32 bits
La mémoire virtuelle est au coeur des systèmes d'exploitation modernes, offrant à chaque programme un espace mémoire isolé et flexible. Orchestrée par la MMU et les tables de pages, elle garantit efficacité et sécurité. Cet article explore la mémoire paginée sur x86 32 bits, le rôle des TLB, et une technique clé : la simulation logicielle du bit NX via W^X, une solution astucieuse aux limitations matérielles pour contrer les malwares.
1. Mémoire virtuelle, MMU et pagination
La mémoire virtuelle est un mécanisme matériel et logiciel permettant à chaque processus de bénéficier d'un espace mémoire logique (virtuel) indépendant, souvent bien plus vaste que la mémoire physique disponible. Ce système repose sur une interaction complexe entre le matériel, via la MMU (Memory Management Unit), et le logiciel, via les structures de gestion des pages.
1.1. Fondamentaux et rôle de la MMU
La mémoire virtuelle permet à chaque processus de fonctionner comme s'il disposait d'un espace d'adressage continu et exclusif, isolé des autres processus. Cet espace virtuel est mappé sur la mémoire physique (DRAM) ou, si nécessaire, sur un espace de stockage disque (swap), par un composant clé : la MMU.
La MMU est un composant matériel situé entre le processeur et la mémoire physique. Elle intervient à chaque accès mémoire pour traduire à la volée une adresse virtuelle en adresse physique. La MMU gère également :
- Les droits d'accès : Permissions de lecture (R), écriture (W), ou exécution (X).
- La signalisation des erreurs : En cas d'accès à une page absente ou interdite, la MMU génère un défaut de page (page fault).
- Les bits d'état : Les bits Accessed (A) et Dirty (D) permettent au système d'exploitation de suivre l'utilisation des pages.
Sur les architectures x86 32 bits, la traduction repose sur un schéma de pagination à deux niveaux : un Page Directory et des Page Tables.
1.2. Avantages de la mémoire virtuelle
Isolation des processus : Chaque programme dispose de son propre espace d'adressage virtuel. L'isolation est garantie matériellement par la MMU, qui utilise des tables de pages distinctes pour chaque processus (pointées par CR3 sur x86).
Protection mémoire fine : La MMU permet de définir des permissions précises pour chaque page (lisible, écrivable, exécutable, ou interdite).
Allocation flexible : Un processus peut demander un grand bloc de mémoire virtuelle sans que celui-ci soit physiquement contigu en RAM.
Optimisation de l'utilisation mémoire : Les pages rarement utilisées peuvent être déchargées sur un espace disque (swap).
1.3. Traduction et découpage des adresses
Dans les systèmes 32 bits, la taille standard d'une page mémoire est de 4 Kio (4096 octets). Une adresse virtuelle 32 bits est divisée comme suit :
[31........22][21........12][11........0] 10 bits 10 bits 12 bits PDE index PTE index Offset
- PDE index (bits 31-22) : Index dans le Page Directory, sélectionnant une des 1024 entrées.
- PTE index (bits 21-12) : Index dans la Page Table, sélectionnant une des 1024 entrées.
- Offset (bits 11-0) : Position à l'intérieur de la page physique (0 à 4095 octets).
Le processus de traduction (page walk) :
- Extraction des champs depuis l'adresse virtuelle.
- Lecture du PDE depuis l'adresse pointee par CR3.
- Lecture du PTE depuis l'adresse indiquee par le PDE.
- Calcul de l'adresse physique : Page Frame + Offset.
Le page walk nécessite au minimum deux accès mémoire supplémentaires (PDE + PTE), ce qui peut représenter 100-200 cycles si les structures ne sont pas en cache.
2. Les Translation Lookaside Buffers (TLB)
Les TLB sont des caches matériels spécialisés intégrés dans la MMU. Leur rôle est d'accélérer la traduction des adresses virtuelles en adresses physiques, en évitant le coût élevé des accès répétés aux tables de pages.
2.1. Fonctionnement
Lorsqu'une adresse virtuelle est générée, la MMU consulte le TLB :
- TLB hit : La traduction est immédiate, sans accès aux tables de pages.
- TLB miss : La MMU effectue un page walk et stocke le résultat dans le TLB pour les accès futurs.
2.2. Structure
Les TLB sont des caches de petite taille (quelques dizaines à quelques centaines d'entrées). Chaque entrée contient : adresse virtuelle, adresse physique, permissions et bits d'état.
Sur les architectures x86 32 bits, les TLB sont souvent séparés en :
- i-TLB (instruction TLB) : Pour les accès liés à l'exécution des instructions.
- d-TLB (data TLB) : Pour les accès de lecture/écriture de données.
Cette séparation joue un rôle clé dans la simulation du bit NX.
2.3. Impact sur les performances
- TLB Hit : Quelques cycles.
- TLB Miss : Peut coûter jusqu'à trois accès DRAM (PDE, PTE, donnée), soit plusieurs centaines de cycles.
Pour minimiser les TLB misses, les systèmes modernes utilisent des TLB multi-niveaux, des grandes pages (2 Mio ou 4 Gio), et optimisent la localité de référence.
3. Simulation du bit NX : la politique W^X
Dans les processeurs x86 32 bits des années 1990 et début 2000, l'absence d'un mécanisme matériel pour empêcher l'exécution de code dans des pages de données exposait les systèmes à des attaques par injection de code. Pour remédier à cette limitation, une solution logicielle basée sur la politique W^X (Write XOR Execute) a été développée, exploitant la séparation des TLB.
3.1. Le problème
Sur x86 32 bits, toutes les pages présentes en mémoire étaient implicitement exécutables. Cette limitation permettait aux attaquants d'injecter du code malveillant dans des pages de données (via un débordement de tampon par exemple) et de l'exécuter.
Le bit NX (No-eXecute) a été introduit plus tard par AMD dans l'architecture x86-64. Sur RISC-V Sv32, un bit X (Executable) explicite existe dans chaque entrée de table de pages.
3.2. Le principe de W^X
La politique W^X impose qu'une page ne peut être à la fois écrivable et exécutable :
- Une page de données est marquée écrivable dans le d-TLB mais non exécutable dans l'i-TLB.
- Une page de code est marquée exécutable dans l'i-TLB mais non écrivable dans le d-TLB.
- Toute violation déclenche un défaut de page, capturé via le registre CR2.
3.3. Manipulation des TLB
- Pour une écriture : Le système charge la page dans le d-TLB avec
{ R=1, W=1 }et s'assure que l'i-TLB n'a pas d'entrée pour cette page (flush sélectif). - Pour une exécution : Le système charge la page dans l'i-TLB et vide l'entrée correspondante dans le d-TLB.
Pseudo-code simplifié :
gestion_defaut_page(adresse_virtuelle, type_operation) { pte = obtenir_PTE(adresse_virtuelle); if (type_operation == ECRITURE) { dTLB_ajouter(adresse_virtuelle, pte.adresse_physique, R=1, W=1); flush_iTLB(adresse_virtuelle); } else if (type_operation == EXECUTION) { iTLB_ajouter(adresse_virtuelle, pte.adresse_physique); flush_dTLB(adresse_virtuelle); } }
3.4. Mon expérience chez LANDesk
J'ai travaillé sur cette technique chez LANDesk (devenu Ivanti), où le système de prévention d'intrusion utilisait W^X pour protéger des millions de machines Windows. Un défi majeur : sur les premiers Pentiums, la TLB ne faisait que 8 entrées. Une instruction comme push [esi] pouvait nécessiter jusqu'à onze accès mémoire si l'adresse n'était pas alignée ou chevauchait deux pages.
La solution que j'ai conçue : un désassembleur pour identifier ces cas critiques, puis un émulateur pour contourner le fait que les premiers processeurs Pentium n'avaient pas assez d'entrées dans les TLBs. Le tout déployé sur 10 millions de machines sans le moindre problème.
3.5. Limites
- Performance : Chaque défaut de page déclenche un traitement logiciel. Les flushs fréquents des TLB dégradent les performances.
- Compatibilité : Certaines applications légitimes (compilateurs JIT) nécessitent des pages à la fois écrivables et exécutables.
- Complexité : La gestion des TLB séparés requiert une synchronisation fine entre le matériel et le logiciel.
Conclusion
Bien que W^X ait été une solution ingénieuse pour pallier l'absence du bit NX sur x86 32 bits, elle restait coûteuse en performances et complexe à implémenter. L'introduction du bit NX matériel dans x86-64, suivie par son adoption généralisée dans les architectures modernes (ARM et RISC-V), a rendu cette simulation obsolète, offrant une protection native plus efficace contre les attaques par injection de code.