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 :

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

  1. 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).

  2. Protection mémoire fine : La MMU permet de définir des permissions précises pour chaque page (lisible, écrivable, exécutable, ou interdite).

  3. Allocation flexible : Un processus peut demander un grand bloc de mémoire virtuelle sans que celui-ci soit physiquement contigu en RAM.

  4. 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

Le processus de traduction (page walk) :

  1. Extraction des champs depuis l'adresse virtuelle.
  2. Lecture du PDE depuis l'adresse pointee par CR3.
  3. Lecture du PTE depuis l'adresse indiquee par le PDE.
  4. 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 :

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 :

Cette séparation joue un rôle clé dans la simulation du bit NX.

2.3. Impact sur les performances

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 :

3.3. Manipulation des TLB

  1. 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).
  2. 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

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.