
Parcourez les tables de pages x86-64 à la main dans qemu et gdb. Décomposez une adresse virtuelle, suivez cr3 à travers tous les niveaux de la mémoire physique, et extrayez un flag des octets bruts.
Vous avez lu sur la pagination. Les diagrammes ont du sens. Quatre niveaux, 9 bits chacun, page frame, offset. Bien sûr. Mais ensuite, vous tombez sur un défi qui exige de parcourir réellement les tables de pages, et vous réalisez que vous ne le savez pas. Vous savez à propos de cela. Une grosse différence.
Ce qui a fonctionné pour moi a été de m'asseoir devant QEMU et gdb et de faire le parcours moi-même : calculer chaque index, lire chaque entrée depuis la mémoire physique, suivre chaque pointeur à la main. Un après-midi de cela peut enseigner plus que des heures de cours.
Ceci est une collection de mes notes de ce processus. Si la partie conceptuelle vous manque encore, regardez d'abord la conférence de Zardus sur la gestion de la mémoire du noyau. C'est la théorie. Ceci est le laboratoire.
L'objectif : prendre une adresse virtuelle et la traquer à travers la mémoire physique brute jusqu'à ce que nous trouvions les données. Pas d'aides du noyau. Pas d'abstractions. Juste une VM QEMU, gdb, et de la mémoire physique brute.
À la fin, la pagination ne sera plus quelque chose que vous avez lu, mais quelque chose que vous connaîtrez parce que vous l'avez fait à la main.
Un noyau pré-construit et un initramfs sont inclus. J'ai exécuté cela sous Fedora, mais tout système d'exploitation qui exécute QEMU et gdb devrait fonctionner. Installez-les avec votre gestionnaire de paquets :```
sudo apt install qemu-system-x86 gdb
sudo dnf install qemu-system-x86 gdb
brew install qemu gdb
### Le binaire du défi
La cible est un programme C trivial qui stocke un flag en mémoire et affiche son adresse virtuelle :```c
#include <stdio.h>
#include <unistd.h>
int main(void)
{
char secret[] = "FLAG{p4g3_t4bl3_w4lk3r}";
printf("secret @ %p\n", (void *)secret);
printf("pid = %d\n", getpid());
printf("Spinning. Walk the page tables to find the flag.\n");
while (1)
{
}
}
La boucle active est intentionnelle. J'utilisais initialement pause(), mais cela met le
processus en sommeil dans un appel système : lorsque gdb interrompt la VM, le CPU est probablement
en train d'exécuter la tâche idle avec un CR3 différent. Une boucle d'attente active maintient le processus
sur le CPU, donc l'arrêt garantit que vous êtes dans son contexte avec les bonnes tables de pages.
Une initramfs pré-construite avec ce binaire est déjà incluse dans
initramfs.cpio.gz. Si vous devez la reconstruire (Linux uniquement, nécessite
busybox et glibc-static), exécutez make dans ce répertoire.
./start.sh
Le script démarre le noyau et l'initramfs fournis sous QEMU avec `-s`
(serveur gdb sur `localhost:1234`) et `nokaslr` afin que les adresses du noyau restent
fixées entre les exécutions.
La VM démarre immédiatement et le binaire du défi s'exécute. Vous verrez l'adresse
virtuelle du flag affichée dans la console.```
secret @ 0x7ffe08985c90
pid = 1
Spinning. Walk the page tables to find the flag.
Écrivez cette adresse virtuelle. C'est votre cible.

La touche d'échappement par défaut de QEMU est
Ctrl-a, mais cela entre en conflit avec mon préfixe tmux, donc le script utilise-echr 0x11pour la remapper surCtrl-q. Si vous utilisezCtrl-qpour autre chose, modifiez la valeur hexadécimale dansstart.shpour l'adapter à votre configuration.
Dans un second terminal :``` gdb -ex "target remote :1234"

---
## Décomposer l'adresse virtuelle
Vous avez une adresse virtuelle. Mais où se trouvent les données, _vraiment_ ?
Les adresses virtuelles sont la fiction polie du système d'exploitation. Chaque processus pense avoir sa propre mémoire privée commençant à zéro. En réalité, les données se trouvent à un emplacement totalement différent dans la RAM physique. La table des pages est la carte entre les deux : une structure arborescente que le CPU parcourt à chaque accès mémoire (ou qu'il consulte dans son cache TLB).
Faisons donc ce que fait le CPU. Manuellement. Pour traduire cette adresse, nous devons la décomposer en les indices que le CPU utilise à chaque niveau.
Une adresse virtuelle x86-64 a une largeur de 48 bits. Ces 48 bits sont répartis en cinq champs :```
63 48 47 39 38 30 29 21 20 12 11 0
┌────────┬────────┬────────┬────────┬────────┬──────────┐
│ sign │ PGD │ PUD │ PMD │ PT │ Offset │
│ extend │ index │ index │ index │ index │ │
│ (16b) │ (9b) │ (9b) │ (9b) │ (9b) │ (12b) │
└────────┴────────┴────────┴────────┴────────┴──────────┘
Chaque index de 9 bits sélectionne une des 512 entrées dans une table des pages à ce niveau. Le décalage de 12 bits sélectionne un octet dans la page finale de 4 Ko (0x1000).
Pour extraire les indices, décaler et masquer :``` PGD index = (VA >> 39) & 0x1FF PUD index = (VA >> 30) & 0x1FF PMD index = (VA >> 21) & 0x1FF PT index = (VA >> 12) & 0x1FF Offset = VA & 0xFFF
Dans gdb, vous pouvez les calculer directement :```
(gdb) p/x (0x7ffe08985c90 >> 39) & 0x1ff
$1 = 0xff
(gdb) p/x (0x7ffe08985c90 >> 30) & 0x1ff
$2 = 0x1f8
(gdb) p/x (0x7ffe08985c90 >> 21) & 0x1ff
$3 = 0x44
(gdb) p/x (0x7ffe08985c90 >> 12) & 0x1ff
$4 = 0x185
(gdb) p/x 0x7ffe08985c90 & 0xfff
$5 = 0xc90
Notez-les. Vous utiliserez chacun à son niveau correspondant.
Vos valeurs seront différentes. L'adresse
0x7ffe08985c90est juste un exemple. Utilisez l'adresse que votre binaire de défi a affichée.
Note sur la pagination à 5 niveaux. Les processeurs et noyaux récents prennent en charge LA57, ce qui ajoute un cinquième niveau (PML5) au-dessus du PGD et étend les adresses virtuelles sur 57 bits. Le parcours suit le même motif : un indice de 9 bits de plus, une consultation de table de plus. La plupart des systèmes utilisent encore la pagination à 4 niveaux. Vous pouvez vérifier le vôtre :
cat /proc/cpuinfo | grep la57. Tout dans cet article suppose une pagination à 4 niveaux.
Chaque arbre a une racine. Pour les tables de pages, cette racine se trouve dans le registre CR3 : il contient l'adresse physique de la table de plus haut niveau, le PGD. Chaque processus a sa propre valeur CR3, le noyau l'échange lors d'un changement de contexte.
C'est notre point d'entrée dans le parcours. Lisez-la depuis gdb :``` (gdb) info registers cr3 cr3 0x66c7000 [ PDBR=26311 PCID=0 ]
La base de la table de pages est `0x66c7000`. Les 12 bits de poids faible sont l'identifiant PCID/drapeaux (nuls ici),
donc l'adresse de base est la valeur telle quelle.
C'est là que commence la marche.
---
## La marche
Voici l'astuce : chaque niveau
suit le même motif. Les drapeaux varient légèrement entre les niveaux, mais le
processus ne change pas. Le motif :
1. **Calculer l'adresse de l'entrée :** `base + index * 8` (chaque entrée fait 8 octets)
2. **Lire l'entrée depuis la mémoire physique** à l'aide de la commande `xp` du moniteur QEMU
3. **Décoder les drapeaux** (voir la référence ci-dessous). Si Present (bit 0) est à 0, la page n'est pas mappée et la marche s'arrête
4. **Extraire la base de la table suivante :** masquer l'entrée avec `& 0x000FFFFFFFFFF000`
5. **Passer au niveau suivant**
Chaque entrée fait 64 bits. Les bits de drapeaux courants :```
Bit Name Meaning when set
0 Present Page/table is mapped
1 Read/Write Writable
2 User/Supervisor Accessible from userspace
3 Write-Through Write-through caching
4 Cache Disable Caching disabled
5 Accessed CPU has read this entry
6 Dirty CPU has written to the page (final level only)
7 Page Size 1 GB page (PUD) or 2 MB page (PMD)
63 NX No-execute
Les bits [51:12] contiennent l'adresse physique de la table suivante (ou du cadre de page au dernier niveau). Les bits 9-11 sont ignorés par le matériel et disponibles pour une utilisation par le système d'exploitation. Linux les utilise pour la tenue de registre (suivi soft-dirty, par exemple). Les bits 52-62 sont réservés. Vous rencontrerez les deux en lisant les PTE dans les descriptions d'exploits.
Gardez ce tableau de flags à portée de main pendant que vous progressez.
Allons-y.
Nous avons la base du PGD depuis CR3 : 0x66c7000.
Notre index PGD est 0xff.
Calculez l'adresse de l'entrée :``` entry = 0x66c7000 + 0xff * 8 = 0x66c77f8
Lisez-le depuis gdb à l'aide de la commande d'examen de la mémoire physique de QEMU :```
(gdb) monitor xp/1gx 0x66c77f8
000000066c77f8: 0x0000000006713067
Entrée : 0x6713067 [Present RW User Accessed Dirty].
Base suivante : 0x6713067 & 0x000FFFFFFFFFF000 = 0x6713000.
La base que nous avons extraite de l'entrée PGD (0x6713000) pointe vers le PUD. Même processus, index suivant : 0x1f8.```
entry = 0x6713000 + 0x1f8 * 8 = 0x6713fc0
.```
(gdb) monitor xp/1gx 0x6713fc0
00000006713fc0: 0x00000000066ac067
Entry: 0x66ac067 [Présent RW Utilisateur Accédé Sale]. Taille de page (bit 7) = 0, pas une page géante de 1 Go.
Base suivante: 0x66ac067 & 0x000FFFFFFFFFF000 = 0x66ac000.
Base: 0x66ac000. Index PMD: 0x44.```
entry = 0x66ac000 + 0x44 * 8 = 0x66ac220
goctopus - détecter CVE-2025-29927 (Contournement du middleware NextJS)```
(gdb) monitor xp/1gx 0x66ac220
000000066ac220: 0x00000000066c4067
Entry: 0x66c4067 [Present RW User Accessed Dirty]. Taille de page (bit 7) = 0, pas une page géante de 2 Mo.
Base suivante : 0x66c4067 & 0x000FFFFFFFFFF000 = 0x66c4000.
Base : 0x66c4000. Indice PT : 0x185.```
entry = 0x66c4000 + 0x185 * 8 = 0x66c4c28
**No input content provided.**```
(gdb) monitor xp/1gx 0x66c4c28
000000066c4c28: 0x80000000037fd867
Entry: 0x80000000037fd867 [Present RW User Accessed Dirty NX]. C'est la dernière PTE.
Physical page frame: 0x80000000037fd867 & 0x000FFFFFFFFFF000 =
0x37fd000.
Voyons ce qui se passe lorsque la marche rencontre une page non mappée. Choisissez une adresse qui n'est presque certainement pas mappée, quelque part au milieu de l'espace d'adressage :``` (gdb) p/x (0x0000414141414000 >> 39) & 0x1ff $1 = 0x82
### Clé d'accès
**Les clés d'accès** se composent d'un ID de clé d'accès et d'une clé d'accès secrète, qui sont utilisées pour signer les requêtes programmatiques que vous envoyez à AWS. Une clé d'accès fonctionne comme un mot de passe pour votre compte AWS, il est donc important de la renouveler régulièrement. Cette méthode d'élévation de privilèges recherche les clés d'accès actives, puis interroge les politiques attachées à l'utilisateur correspondant.```
(gdb) monitor xp/1gx 0x66c7000 + 0x82 * 8
00000000066c7410: 0x0000000000000000
Tous des zéros. Le bit 0 (Présent) est effacé. La marche s'arrête ici. Il n'y a pas de PUD, pas de PMD, pas de PT, pas de cadre de page. Cette adresse ne correspond à aucune mémoire physique.
Si le CPU avait rencontré cela pendant une exécution normale, il aurait déclenché une faute de page (interruption 14). Le gestionnaire de fautes du noyau déciderait alors de la marche à suivre : charger la page depuis le disque (échange), allouer une nouvelle page (allocation à la demande), ou tuer le processus avec un défaut de segmentation.
Le point essentiel : la table des pages n'est pas seulement une structure de traduction. C'est aussi le mécanisme qui rend la mémoire virtuelle virtuelle. Chaque adresse n'a pas besoin d'avoir de la mémoire physique derrière elle. Le CPU le découvre pendant la marche, un niveau à la fois.
Combinez le cadre de page physique avec le décalage de l'adresse virtuelle d'origine :``` Physical address = 0x37fd000 | 0xc90 = 0x37fdc90
Maintenant, lisez-le :```
(gdb) monitor xp/6bx 0x37fdc90
00000000037fdc90: 0x46 0x4c 0x41 0x47 0x7b 0x70
C'est F, L, A, G, {, p: le début de notre drapeau. Lire la suite:```
(gdb) monitor xp/24bx 0x37fdc90
00000000037fdc90: 0x46 0x4c 0x41 0x47 0x7b 0x70 0x34 0x67
00000000037fdc98: 0x33 0x5f 0x74 0x34 0x62 0x6c 0x33 0x5f
00000000037fdca0: 0x77 0x34 0x6c 0x6b 0x33 0x72 0x7d 0x00
[No content provided in the INPUT section. Please provide the Markdown text to translate.]```
FLAG{p4g3_t4bl3_w4lk3r}

La voilà. Vous venez de faire ce que le CPU fait des milliards de fois par seconde, mais vous l'avez fait à la main, en lisant des octets bruts de la mémoire physique. Quatre tables de profondeur, rien caché derrière une abstraction.
Avant, le pagination était un diagramme dans un diaporama. Maintenant, c'est une séquence de lectures que vous pouvez rejouer dans votre tête : base, index, décalage, masque, suivre. Cette différence compte quand vous scrutez une exploitation de noyau et devez raisonner sur ce que fait réellement une écriture dans une PTE.
Vous pouvez vérifier votre résultat avec la commande moniteur gva2gpa (adresse virtuelle invitée vers adresse physique invitée) de QEMU, qui effectue le parcours en interne :```
(qemu) gva2gpa 0x7ffe08985c90
gpa: 0x37fdc90
## Flags et permissions
Nous avons décodé les flags à chaque niveau lors de la marche mais avons passé sous silence ce qu'ils
signifient en termes de sécurité. Regardez le PTE final :```
0x80000000037fd867
Bien que class-dump ne puisse pas importer des bibliothèques Swift directement, vous pouvez toujours analyser les binaires Swift. Toutefois, les noms sont modifiés (mangled) et peuvent être difficiles à lire. Utilisez class-dump --arch arm64 SwiftBinary pour tenter de dumpter les informations Objective-C. Pour Swift, considérez des outils comme SwiftDump ou dsdump.```
Bit 0 (Present) = 1 Page is in physical memory
Bit 1 (Read/Write) = 1 Page is writable
Bit 2 (User/Supervisor)= 1 Accessible from user mode
Bit 3 (Write-Through) = 0 Write-back caching
Bit 4 (Cache Disable) = 0 Caching enabled
Bit 5 (Accessed) = 1 CPU has read this page
Bit 6 (Dirty) = 1 CPU has written to this page
Bit 7 (Page Size) = 0 4 KB page (not huge)
Bit 63 (NX) = 1 No-Execute: cannot run code from this page
C'est logique : le secret est une variable de pile. La pile est lisible, inscriptible et sale (elle a été écrite). Elle est marquée non-exécutable car les systèmes modernes imposent W^X : une page qui est inscriptible ne devrait pas être exécutable.
Les indicateurs à chaque niveau sont combinés par un ET logique par le matériel. Si l'entrée PUD a User=0, rien en dessous n'est accessible par l'utilisateur, peu importe ce que dit la PTE. La permission la plus restrictive l'emporte.
---
## Le TLB : quand le CPU saute la traversée
Quatre lectures mémoire juste pour accéder à un octet. C'est coûteux. Le CPU ne traverse pas réellement la table des pages à chaque accès mémoire. Il met en cache le résultat dans un **Translation Lookaside Buffer (TLB)**.
Après le premier accès à l'adresse virtuelle de notre indicateur, le CPU stocke le mappage `0x7ffe08985c90 -> 0x37fdc90` (approximativement) dans le TLB. Les accès suivants touchent le cache et sautent entièrement la traversée. La table des pages reste intacte dans la RAM.
Cela est transparent pour le code normal. Mais cela devient important dès que vous _modifiez_ une entrée de la table des pages. Si vous écrivez une nouvelle adresse physique dans une PTE, le CPU ne s'en aperçoit pas : le TLB contient toujours l'ancien mappage. Vous devez explicitement le vider.
Le noyau fait cela avec l'instruction `invlpg`, qui invalide l'entrée TLB pour une seule adresse virtuelle. Appelez `mprotect` depuis l'espace utilisateur et c'est ce qui se passe sous le capot : le noyau met à jour les indicateurs de la PTE, puis vide le TLB pour que le CPU prenne en compte les nouvelles permissions.
Cela a des implications directes en matière de sécurité. Dans une exploitation du noyau, si vous parvenez à écrire dans une PTE (par exemple, en effaçant le bit NX pour rendre la pile exécutable), vous avez également besoin que le TLB soit vidé avant que le CPU n'honore le changement. Parfois, le noyau le fait pour vous comme effet secondaire du chemin de code que vous avez déclenché. Parfois, vous devez l'organiser vous-même. Dans les deux cas, vous devez savoir que le TLB existe, sinon votre exploit fonctionne en théorie mais pas en pratique.
---
## Les grandes pages : quand la traversée se termine tôt
Dans la procédure pas à pas ci-dessus, nous avons parcouru les quatre niveaux. Mais la traversée peut se terminer tôt si un bit de taille de page (bit 7) est défini.
**Au niveau 3 (PUD) :** Si le bit 7 est défini, l'entrée mappe directement une page de 1 Go. L'adresse physique est prise à partir de l'entrée, et les bits [29:0] de l'adresse virtuelle deviennent le décalage (30 bits = 1 Go).
**Au niveau 2 (PMD) :** Si le bit 7 est défini, l'entrée mappe une page de 2 Mo. Les bits [20:0] de l'adresse virtuelle deviennent le décalage (21 bits = 2 Mo).
Vous verrez souvent des grandes pages dans les mappages du noyau. La région de mappage direct du noyau (`0xffff888000000000` sur la plupart des noyaux 64 bits) utilise fréquemment des pages de 2 Mo ou 1 Go pour réduire la pression sur le TLB.
Si vous rencontrez une grande page lors de votre traversée, la formule change :```
2 MB page: phys = (PMD_entry & 0x000FFFFFFFE00000) | (VA & 0x1FFFFF)
1 GB page: phys = (PUD_entry & 0x000FFFFFC0000000) | (VA & 0x3FFFFFFF)
Maintenant que nous connaissons le processus, codons-le. pagewalk.py est un
script Python gdb qui effectue la même marche que nous venons de faire. La logique centrale tient en
une seule fonction :```python
ADDR_MASK = 0x000FFFFFFFFFF000
def read_phys(addr): """Read a 64-bit value from guest physical memory via QEMU monitor.""" result = gdb.execute(f"monitor xp/1gx {addr:#x}", to_string=True) return int(result.strip().split(":")[1].strip(), 16)
def pagewalk(va): cr3 = int(gdb.parse_and_eval("$cr3")) pgd_base = cr3 & ADDR_MASK
# Decompose the virtual address
pgd_idx = (va >> 39) & 0x1FF
pud_idx = (va >> 30) & 0x1FF
pmd_idx = (va >> 21) & 0x1FF
pt_idx = (va >> 12) & 0x1FF
offset = va & 0xFFF
# Walk: each level is the same pattern
pgd_entry = read_phys(pgd_base + pgd_idx * 8)
if not (pgd_entry & 1): return None # Not present
pud_base = pgd_entry & ADDR_MASK
pud_entry = read_phys(pud_base + pud_idx * 8)
if not (pud_entry & 1): return None
if pud_entry & (1 << 7): # 1 GB huge page
return (pud_entry & 0x000FFFFFC0000000) | (va & 0x3FFFFFFF)
pmd_base = pud_entry & ADDR_MASK
pmd_entry = read_phys(pmd_base + pmd_idx * 8)
if not (pmd_entry & 1): return None
if pmd_entry & (1 << 7): # 2 MB huge page
return (pmd_entry & 0x000FFFFFFFE00000) | (va & 0x1FFFFF)
pt_base = pmd_entry & ADDR_MASK
pt_entry = read_phys(pt_base + pt_idx * 8)
if not (pt_entry & 1): return None
return (pt_entry & ADDR_MASK) | offset
Le script complet (avec décodage du flag et sortie formatée) se trouve dans `pagewalk.py`.
Utilisez-le pour vérifier votre travail manuel ou pour explorer d'autres adresses :```
(gdb) source ./pagewalk.py
Page walk command loaded. Usage: pagewalk <virtual-address>
(gdb) pagewalk 0x7ffe08985c90
Decoded Virtual Address:
PGD=0x0ff
PUD=0x1f8
PMD=0x044
PT=0x185
Offset=0xc90
CR3: 0x00000000066c7000
PGD[0x0ff]: 0x0000000006713067 [Present RW User Accessed Dirty]
PUD[0x1f8]: 0x00000000066ac067 [Present RW User Accessed Dirty]
PMD[0x044]: 0x00000000066c4067 [Present RW User Accessed Dirty]
PT[0x185]: 0x80000000037fd867 [Present RW User Accessed Dirty NX]
Physical address: 0x00000000037fdc90

Remarquez comment le script vérifie la présence de pages énormes aux niveaux PUD et PMD avant de continuer la marche. C'est la même logique que nous avons discutée dans la section sur les pages énormes : si PageSize (bit 7) est défini, la marche se termine prématurément et le décalage est plus large.
Essayez de parcourir l'adresse d'une fonction : vous verrez que le bit NX est clair (le code doit être exécutable). Essayez une section de données en lecture seule : vous verrez que R/W est clair.
Tout au long de cet exercice, nous avons utilisé monitor xp pour lire directement la mémoire physique. Cela fonctionne car le moniteur de QEMU se trouve en dehors de la VM et peut accéder à l'espace d'adressage physique de l'invité. Dans un véritable exploit, vous n'avez pas ce luxe.
Le noyau résout ce problème pour lui-même avec la région de mappage direct : un mappage virtuel contigu de toute la RAM physique. Sur x86-64, cette région commence conventionnellement à 0xffff888000000000, mais avec KASLR activé, la base est randomisée. Le noyau stocke la base réelle dans un symbole appelé page_offset_base.
Nous avons démarré avec nokaslr, donc la base est à sa valeur par défaut. Confirmons :```
(gdb) x/s 0xffff888000000000 + 0x37fdc90
0xffff888037fdc90: "FLAG{p4g3_t4bl3_w4lk3r}"
Même mémoire physique, accessible via une adresse virtuelle du noyau. C'est ainsi que le noyau lui-même lit une mémoire physique arbitraire: `phys_to_virt()` est juste `page_offset_base + phys_addr`.
C'est aussi pourquoi les exploits du noyau se soucient de fuiter `page_offset_base`. Si KASLR est activé, vous ne savez pas où la cartographie directe commence, donc vous ne pouvez pas convertir des adresses physiques en adresses virtuelles du noyau. Fuitez la base, et vous pouvez lire ou écrire n'importe quelle adresse physique via la cartographie directe, y compris les entrées des tables de pages elles-mêmes.
---
## Finding another process's page tables
Notre configuration garantissait que CR3 pointait vers les tables de pages du binaire du défi lorsque gdb mettait en pause la VM. Mais que faire si vous avez besoin de parcourir les tables de pages d'un processus _différent_?
La valeur CR3 de chaque processus est stockée dans son `task_struct`. Le chemin est:```
task_struct -> mm_struct -> pgd -> physical page
Dans gdb avec les symboles du noyau, vous pouvez trouver le task_struct de init (PID 1) et extraire sa racine de la table de pages :``` (gdb) p/x init_task.mm->pgd $1 = 0xffff8880066c7000
C'est une adresse virtuelle du noyau dans le mappage direct. Enlevez la base pour obtenir l'adresse physique :```
0xffff8880066c7000 - 0xffff888000000000 = 0x66c7000
C'est la même CR3 avec laquelle nous avons commencé, ce qui a du sens : notre binaire du défi est le PID 1 dans ce initramfs minimal.
Pour d'autres processus, vous parcourriez la liste des tâches (liste chaînée init_task.tasks),
trouveriez la cible, et en extrairiez son mm->pgd de la même manière. Chaque processus a son
propre arbre de tables de pages enraciné à sa propre CR3. Le noyau échange la CR3 à chaque
changement de contexte, donnant à chaque processus l'illusion d'une mémoire privée.
Si vous êtes arrivé jusqu'ici en ayant réellement effectué le parcours (pas juste en lisant), vous possédez désormais quelque chose qu'aucun diagramme ne peut vous donner : une intuition pour comprendre comment la mémoire fonctionne réellement au niveau matériel. Voici où cette intuition porte ses fruits :
L'ASLR randomise l'adresse virtuelle, pas le parcours. La structure de la table de pages est toujours la même : quatre niveaux, 512 entrées chacun, même disposition des bits. L'ASLR modifie les indices que vous allez calculer, mais le processus est identique.
W^X est appliqué dans la table de pages. Les bits R/W et NX dans la PTE sont ce qui rend
mprotect fonctionnel. Quand une exploitation tente d'exécuter du shellcode sur la pile,
le CPU vérifie le bit NX lors de la traduction et génère une faute.
SMEP et SMAP vérifient le bit Utilisateur. La Prévention d'Exécution/Accès en Mode Superviseur vérifie le bit Utilisateur/Superviseur à tous les niveaux de la table de pages. Si une entrée marque l'adresse comme mode utilisateur et que du code noyau tente de l'exécuter ou d'y accéder, le CPU génère une faute. C'est pourquoi les exploits noyau modernes ne peuvent pas simplement sauter vers du shellcode en espace utilisateur.
Les exploits noyau ciblent souvent directement les tables de pages. Si vous pouvez écrire dans une PTE, vous pouvez changer la mémoire physique à laquelle une adresse virtuelle est mappée, changer les permissions, ou remapper la mémoire noyau en mémoire accessible en mode utilisateur. Comprendre le parcours, c'est comprendre la surface d'attaque.
KPTI divise la table de pages en deux. Au lieu d'un seul jeu de tables de pages par processus, il y en a maintenant deux : un pour le mode utilisateur (avec presque toutes les pages noyau démontées) et un pour le mode noyau (avec tout). Le noyau échange la CR3 à chaque entrée et sortie d'appel système. Vous pouvez l'observer : arrêtez la VM alors que vous êtes en espace utilisateur et lisez CR3, puis placez un point d'arrêt sur une entrée d'appel système et lisez CR3 à nouveau. Elles différeront. La table de pages en mode utilisateur ne contient tout simplement pas d'entrées pour la mémoire noyau, donc il n'y a rien à fuiter même si le parcours se termine.
La prochaine fois qu'un article d'exploitation noyau mentionnera « remapper les tables de pages », cela ne sera plus abstrait. Vous saurez exactement de quels octets ils parlent, parce que vous les aurez lus vous-même.