
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**