
Percorri le tabelle delle pagine x86-64 a mano in qemu e gdb. Scomponi un indirizzo virtuale, segui cr3 attraverso tutti i livelli della memoria fisica ed estrai una flag da byte grezzi.
Hai letto del paging. I diagrammi hanno senso. Quattro livelli, 9 bit ciascuno, page frame, offset. Certo. Ma poi ti imbatti in una sfida che richiede di camminare effettivamente nelle tabelle delle pagine, e ti rendi conto che non lo sai. Sai a grandi linee. Grande differenza.
Ciò che ha funzionato per me è stato sedermi davanti a QEMU e gdb e fare la camminata da solo: calcolare ogni indice, leggere ogni entry dalla memoria fisica, seguire ogni puntatore a mano. Un pomeriggio di questo può insegnare più di ore di lezioni.
Questa è una raccolta dei miei appunti da quel processo. Se ti manca ancora il lato concettuale, guarda per prima la lezione di Zardus sulla gestione della memoria del kernel. Quella è la teoria. Questo è il laboratorio.
L'obiettivo: prendere un indirizzo virtuale e inseguirlo attraverso la memoria fisica grezza fino a trovare i dati. Nessun helper del kernel. Nessuna astrazione. Solo una VM QEMU, gdb e memoria fisica grezza.
Alla fine, il paging non sarà qualcosa che hai letto, sarà qualcosa che conosci perché l'hai fatto a mano.
Un kernel precompilato e initramfs sono inclusi. L'ho eseguito in Fedora, ma qualsiasi sistema operativo che esegue QEMU e gdb dovrebbe funzionare. Installali con il tuo gestore di pacchetti:```
sudo apt install qemu-system-x86 gdb
sudo dnf install qemu-system-x86 gdb
brew install qemu gdb
### Il binario della sfida
Il target è un semplice programma C che memorizza una flag in memoria e stampa il suo indirizzo virtuale:```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)
{
}
}
The busy loop is intentional. I originally used pause(), but that puts the
process to sleep in a syscall: when gdb halts the VM, the CPU is likely
running the idle task with a different CR3. A spinning loop keeps the process
on-CPU, so halting guarantees you're in its context with the right page tables.
Il ciclo occupato è intenzionale. Inizialmente ho usato pause(), ma questo mette il processo in pausa in una syscall: quando gdb ferma la VM, la CPU sta probabilmente eseguendo il task idle con un CR3 diverso. Un ciclo di spinning mantiene il processo sulla CPU, quindi l'arresto garantisce di trovarsi nel suo contesto con le tabelle delle pagine corrette.
A pre-built initramfs with this binary is already included in
initramfs.cpio.gz. If you need to rebuild it (Linux only, requires
busybox and glibc-static), run make in this directory.
Un initramfs pre-costruito con questo binario è già incluso in
initramfs.cpio.gz. Se è necessario ricostruirlo (solo Linux, richiede
busybox e glibc-static), esegui make in questa directory.
./start.sh
Lo script avvia il kernel e l'initramfs inclusi sotto QEMU con `-s` (server gdb su `localhost:1234`) e `nokaslr` in modo che gli indirizzi del kernel rimangano fissi tra le esecuzioni.
La VM si avvia immediatamente e il binario della challenge viene eseguito. Vedrai l'indirizzo virtuale della flag stampato sulla console.```
secret @ 0x7ffe08985c90
pid = 1
Spinning. Walk the page tables to find the flag.
Annota quell'indirizzo virtuale. Questo è il tuo obiettivo.

Il tasto di escape predefinito di QEMU è
Ctrl-a, ma questo collide con il mio prefisso tmux quindi lo script usa-echr 0x11per rimapparlo aCtrl-q. Se usiCtrl-qper qualcos'altro, cambia il valore esadecimale instart.shper adattarlo alla tua configurazione.
In un secondo terminale:``` gdb -ex "target remote :1234"

---
## Decomposizione dell'indirizzo virtuale
Hai un indirizzo virtuale. Ma dove sono i dati, _davvero_?
Gli indirizzi virtuali sono la finzione educata del sistema operativo. Ogni processo pensa di avere la propria memoria privata che inizia da zero. In realtà, i dati si trovano in una posizione completamente diversa nella RAM fisica. La tabella delle pagine è la mappa tra i due: una struttura ad albero che la CPU percorre ad ogni accesso alla memoria (o cerca nella sua cache TLB).
Quindi facciamo ciò che fa la CPU. Manualmente. Per tradurre quell'indirizzo, dobbiamo scomporlo negli indici che la CPU usa ad ogni livello.
Un indirizzo virtuale x86-64 è largo 48 bit. Quei 48 bit sono suddivisi in cinque campi:```
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) │
└────────┴────────┴────────┴────────┴────────┴──────────┘
Ciascun indice a 9 bit seleziona una delle 512 voci in una tabella delle pagine a quel livello. L'offset a 12 bit seleziona un byte all'interno della pagina finale di 4 KB (0x1000). Per estrarre gli indici, esegui shift e maschera:``` PGD index = (VA >> 39) & 0x1FF PUD index = (VA >> 30) & 0x1FF PMD index = (VA >> 21) & 0x1FF PT index = (VA >> 12) & 0x1FF Offset = VA & 0xFFF
In gdb, puoi calcolare questi direttamente:```
(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
Prendi nota di questi. Li userai a ciascun livello corrispondente.
I tuoi valori saranno diversi. L'indirizzo
0x7ffe08985c90è solo un esempio. Usa l'indirizzo stampato dal tuo binario della challenge.
Una nota sul paging a 5 livelli. CPU e kernel recenti supportano LA57, che aggiunge un quinto livello (PML5) sopra il PGD ed estende gli indirizzi virtuali a 57 bit. Il walk segue lo stesso schema: un indice di 9 bit in più, una ricerca in più nella tabella. La maggior parte dei sistemi utilizza ancora il paging a 4 livelli. Puoi verificare il tuo:
cat /proc/cpuinfo | grep la57. Tutto in questo articolo assume il paging a 4 livelli.
Ogni albero ha una radice. Per le tabelle delle pagine, quella radice è nel registro CR3: contiene l'indirizzo fisico della tabella di livello superiore, il PGD. Ogni processo ha il proprio valore CR3; il kernel lo scambia durante il context switch.
Questo è il nostro punto di ingresso nel walk. Leggilo da gdb:``` (gdb) info registers cr3 cr3 0x66c7000 [ PDBR=26311 PCID=0 ]
La base della tabella delle pagine è `0x66c7000`. I 12 bit bassi sono PCID/flag (zero qui), quindi l'indirizzo base è il valore così com'è.
Qui inizia la camminata.
---
## La camminata
Ecco il trucco: ogni livello segue lo stesso schema. I flag variano leggermente tra i livelli, ma il processo no. Lo schema: