
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:
1. **Calcola l'indirizzo dell'entry:** `base + index * 8` (ogni entry è di 8 byte)
2. **Leggi l'entry dalla memoria fisica** usando il comando `xp` del monitor QEMU
3. **Decodifica i flag** (vedi il riferimento sotto). Se Present (bit 0) è 0, la pagina non è mappata e la camminata si ferma
4. **Estrai la base della prossima tabella:** maschera l'entry con `& 0x000FFFFFFFFFF000`
5. **Passa al livello successivo**
Ogni entry è di 64 bit. I bit di flag comuni:```
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
I bit [51:12] contengono l'indirizzo fisico della tabella successiva (o del frame di pagina al livello finale). I bit 9-11 sono ignorati dall'hardware e disponibili per l'uso da parte del sistema operativo. Linux li utilizza per la contabilità (tracciamento soft-dirty, per esempio). I bit 52-62 sono riservati. Li incontrerai entrambi leggendo le PTE nei writeup di exploit.
Tieni a portata di mano questa tabella dei flag mentre procedi.
Andiamo.
Abbiamo la base PGD da CR3: 0x66c7000.
Il nostro indice PGD è 0xff.
Calcola l'indirizzo della voce:``` entry = 0x66c7000 + 0xff * 8 = 0x66c77f8
Leggilo da gdb usando il comando di esame della memoria fisica di QEMU:```
(gdb) monitor xp/1gx 0x66c77f8
000000066c77f8: 0x0000000006713067
Entry: 0x6713067 [Present RW User Accessed Dirty].
Next base: 0x6713067 & 0x000FFFFFFFFFF000 = 0x6713000.
The base we extracted from the PGD entry (0x6713000) points to the PUD. Same
process, next index: 0x1f8.```
entry = 0x6713000 + 0x1f8 * 8 = 0x6713fc0
Please provide the Markdown content to translate.```
(gdb) monitor xp/1gx 0x6713fc0
00000006713fc0: 0x00000000066ac067
Entry: 0x66ac067 [Presente RW Utente Acceduto Sporco]. Dimensione pagina (bit 7) = 0, non una pagina enorme da 1 GB.
Base successiva: 0x66ac067 & 0x000FFFFFFFFFF000 = 0x66ac000.
Base: 0x66ac000. Indice PMD: 0x44.```
entry = 0x66ac000 + 0x44 * 8 = 0x66ac220
## Flag comuni
* `-h`, `--help` - Mostra questo aiuto
* `-q`, `--quiet` - Disabilita le intestazioni e altri output non argparse
* `-v`, `--verbose` - Essere prolisso (usare più volte per maggiore effetto)
* `--encoding <encoding>` - Codifica del testo di input (predefinita: locale di sistema)
* `--fallback-encoding <encoding>` - Codifica di fallback dell'input
* `--no-stdin` - Non leggere stdin; usare solo argomenti CLI (ignorato se stdin non è un tty)
* `--threads <threads>` - Numero di thread da utilizzare (predefinito: numero di CPU disponibili)```
(gdb) monitor xp/1gx 0x66ac220
000000066ac220: 0x00000000066c4067
Entry: 0x66c4067 [Presente RW Utente Accessato Sporco]. Dimensione Pagina (bit 7) = 0, non è una pagina enorme da 2 MB.
Prossimo base: 0x66c4067 & 0x000FFFFFFFFFF000 = 0x66c4000.
Base: 0x66c4000. Indice PT: 0x185.```
entry = 0x66c4000 + 0x185 * 8 = 0x66c4c28
|`Network ID`|Esegue la scansione di una rete tramite diversi protocolli (SMTP, HTTP, ecc.) per trovare gli ID dei sistemi|
|`Chat GPT`|Esplora ChatGPT direttamente o tramite API per ottenere approfondimenti di intelligenza artificiale|
|`Security Headers`|Verifica la presenza di header di sicurezza HTTP|
|`Send Mail`|Invia un'email con un modello predefinito|
|`CVE`|Cerca dettagli CVE|
|`Part Of`|Individua l'organizzazione che contiene un dato IP|
|`Domain Reputation`|Controlla la reputazione del dominio di qualsiasi organizzazione pubblica|
|`Subdomains`|Esegue la scansione dei dati DNS di un dato dominio|
|`MSI Gryphon`|Esegue la scansione delle vulnerabilità utilizzando il database Gryphon|
|`Abuse IP DB`|Trova segnalazioni di attività dannose per IP|
|`URLhaus`|Controlla gli host che diffondono malware|
|`URLScan`|Controlla gli URL per contenuti dannosi o tentativi di phishing|
|`Shodan Search`|Cerca informazioni sui dispositivi e porte aperte|
|`Wayback Machine`|Visualizzazione storica di una pagina web|
|`Google Dorking`|Genera in blocco query Google Dorking preimpostate|
|`HTTP Status`|Controlla lo stato di un endpoint HTTP|
|`Reqres`|Recupera dati di test reqres e simula richieste HTTP|
|`JSON Placeholder`|Recupera dati JSON di esempio|
|`Update Time`|Ora e data correnti in vari fusi orari per IP|
|`GitHub OSINT`|Ottiene informazioni GitHub su un utente o un'email|```
(gdb) monitor xp/1gx 0x66c4c28
000000066c4c28: 0x80000000037fd867
Voce: 0x80000000037fd867 [Present RW User Accessed Dirty NX]. Questo è il
PTE finale.
Frame di pagina fisica: 0x80000000037fd867 & 0x000FFFFFFFFFF000 =
0x37fd000.
Vediamo cosa accade quando la camminata incontra una pagina non mappata. Scegli un indirizzo che quasi certamente non è mappato, qualcosa nel mezzo dello spazio degli indirizzi:``` (gdb) p/x (0x0000414141414000 >> 39) & 0x1ff $1 = 0x82
### Esempi
* [App Android di esempio (beta)](https://androidfilehost.com/?fid=1395089523397927603)
* [App Android di esempio (2.x)](https://androidfilehost.com/?fid=673368273298982507)
* [App Android di esempio (3.x)](https://androidfilehost.com/?fid=17248734296332209533)
* [Scarica script Nmap](https://github.com/jpylypiw/pivotsuite/tree/master/nse)
* [Dashboard di esempio](https://github.com/jpylypiw/pivotsuite/sample-dashboard)```
(gdb) monitor xp/1gx 0x66c7000 + 0x82 * 8
00000000066c7410: 0x0000000000000000
Tutti zeri. Il bit 0 (Presente) è a 0. La walk si ferma qui. Non c'è PUD, non c'è PMD, non c'è PT, non c'è page frame. Questo indirizzo non mappa a memoria fisica.
Se la CPU avesse incontrato questo durante l'esecuzione normale, avrebbe sollevato un page fault (interrupt 14). L'handler del kernel per il fault deciderebbe quindi cosa fare: caricare la pagina dal disco (swap), allocare una nuova pagina (paginazione su richiesta), o terminare il processo con un segfault.
Il punto: la tabella delle pagine non è solo una struttura di traduzione. È anche il meccanismo che rende la memoria virtuale virtuale. Non tutti gli indirizzi devono avere memoria fisica dietro di sé. La CPU lo scopre durante la walk, un livello alla volta.
Combina il page frame fisico con l'offset dell'indirizzo virtuale originale:``` Physical address = 0x37fd000 | 0xc90 = 0x37fdc90
Ora leggilo:```
(gdb) monitor xp/6bx 0x37fdc90
00000000037fdc90: 0x46 0x4c 0x41 0x47 0x7b 0x70
Quello è F, L, A, G, {, p: l'inizio della nostra flag. Leggi di più:```
(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 input content provided.```
FLAG{p4g3_t4bl3_w4lk3r}

Ecco fatto. Hai appena fatto quello che la CPU fa miliardi di volte al secondo, ma lo hai fatto a mano, leggendo byte grezzi dalla memoria fisica. Quattro livelli di tabelle, nulla nascosto dietro un'astrazione.
Prima, il paging era un diagramma in una slide. Ora è una sequenza di letture che puoi ripercorrere mentalmente: base, index, shift, mask, follow. Questa differenza conta quando stai esaminando un exploit del kernel e devi ragionare su cosa faccia effettivamente una scrittura in un PTE.
Puoi verificare il tuo risultato con il comando monitor gva2gpa (indirizzo virtuale guest a indirizzo fisico guest) di QEMU, che esegue la walk internamente:```
(qemu) gva2gpa 0x7ffe08985c90
gpa: 0x37fdc90
---
## Flags and permissions
Abbiamo decodificato i flag a ogni livello durante la procedura, ma abbiamo saltato cosa significano per la sicurezza. Guarda il PTE finale:```
0x80000000037fd867
Crea il percorso dello store risultante. Opzioni supportate: --file, --impure, --out-link, --build-host.
Note:
Puoi anche utilizzare la compilazione remota per aggirare i build che falliscono.
Se vuoi saperne di più su Nix, dai un'occhiata al libro Nix Pills.
Il comando nix build:
$ nix build
Questo costruisce la derivazione nella directory corrente.
Testa il percorso dello store risultante. Puoi anche testare una derivazione senza costruirla. Per farlo, usa il comando nix derivation:
$ nix derivation show
Opzioni supportate per nix build:
--file / -f percorso: Imposta il percorso del file dell'espressione Nix da costruire.--impure: Abilita la valutazione impura.--out-link / -o percorso: Imposta il percorso del symbolic link al risultato del build.--build-host host: L'host su cui costruire.```
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 pageHa senso: il segreto è una variabile di stack. Lo stack è leggibile, scrivibile e sporco (è stato scritto). È contrassegnato come no-execute perché i sistemi moderni impongono W^X: una pagina scrivibile non dovrebbe essere eseguibile.
I flag a ogni livello vengono combinati da un AND dall'hardware. Se l'entry PUD ha User=0, nulla al di sotto è accessibile dall'utente, indipendentemente da ciò che dice il PTE. Il permesso più restrittivo vince.
---
## Il TLB: quando la CPU salta la camminata
Quattro letture di memoria solo per accedere a un byte. È costoso. La CPU non esegue effettivamente la camminata della tabella delle pagine a ogni accesso in memoria. Memorizza nella cache il risultato in un **Translation Lookaside Buffer (TLB)**.
Dopo il primo accesso all'indirizzo virtuale del nostro flag, la CPU memorizza il mapping `0x7ffe08985c90 -> 0x37fdc90` (approssimativamente) nel TLB. Gli accessi successivi colpiscono la cache e saltano completamente la camminata. La tabella delle pagine rimane intatta in RAM.
Questo è trasparente per il codice normale. Ma diventa importante nel momento in cui _modifichi_ un'entry della tabella delle pagine. Se scrivi un nuovo indirizzo fisico in un PTE, la CPU non se ne accorge: il TLB mantiene ancora il vecchio mapping. Devi esplicitamente invalidarlo.
Il kernel fa questo con l'istruzione `invlpg`, che invalida l'entry TLB per un singolo indirizzo virtuale. Chiama `mprotect` dallo spazio utente e questo è ciò che accade sotto il cofano: il kernel aggiorna i flag del PTE, poi invalida il TLB in modo che la CPU acquisisca le nuove autorizzazioni.
Questo ha implicazioni di sicurezza dirette. In un exploit del kernel, se riesci a scrivere su un PTE (ad esempio, cancellando il bit NX per rendere lo stack eseguibile), hai anche bisogno che il TLB venga invalidato prima che la CPU onori la modifica. A volte il kernel lo fa per te come effetto collaterale del percorso di codice che hai attivato. A volte devi organizzarlo tu stesso. In ogni caso, devi sapere che il TLB esiste, altrimenti il tuo exploit funziona in teoria ma non in pratica.
---
## Pagine grandi: quando la camminata termina presto
Nel walkthrough precedente, abbiamo attraversato tutti e quattro i livelli. Ma la camminata può terminare presto se un bit Page Size (bit 7) è impostato.
**Al Livello 3 (PUD):** Se il bit 7 è impostato, l'entry mappa direttamente una pagina da 1 GB. L'indirizzo fisico è preso dall'entry, e i bit [29:0] dell'indirizzo virtuale diventano l'offset (30 bit = 1 GB).
**Al Livello 2 (PMD):** Se il bit 7 è impostato, l'entry mappa una pagina da 2 MB. I bit [20:0] dell'indirizzo virtuale diventano l'offset (21 bit = 2 MB).
Vedrai spesso pagine grandi nei mapping del kernel. La regione di mappatura diretta del kernel (`0xffff888000000000` sulla maggior parte dei kernel a 64 bit) utilizza frequentemente pagine da 2 MB o 1 GB per ridurre la pressione sul TLB.
Se incontri una pagina grande durante la tua camminata, la formula cambia:```
2 MB page: phys = (PMD_entry & 0x000FFFFFFFE00000) | (VA & 0x1FFFFF)
1 GB page: phys = (PUD_entry & 0x000FFFFFC0000000) | (VA & 0x3FFFFFFF)
Ora che conosciamo il processo, codifichiamolo. pagewalk.py è uno script Python per gdb che esegue la stessa camminata che abbiamo appena fatto. La logica principale è contenuta in una funzione:```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
Lo script completo (con decodifica delle flag e output formattato) si trova in `pagewalk.py`.
Caricalo e usalo per verificare il tuo lavoro manuale, o per esplorare altri indirizzi:```
(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

Nota come lo script controlla le pagine enormi ai livelli PUD e PMD prima di continuare la camminata. È la stessa logica discussa nella sezione sulle pagine enormi: se PageSize (bit 7) è impostato, la camminata termina presto e l'offset è più ampio.
Prova a camminare l'indirizzo di una funzione: vedrai che il bit NX è azzerato (il codice deve essere eseguibile). Prova una sezione dati di sola lettura: vedrai che R/W è azzerato.
Durante questo esercizio abbiamo usato monitor xp per leggere direttamente la memoria fisica. Questo funziona perché il monitor di QEMU si trova al di fuori della VM e può accedere allo spazio degli indirizzi fisici dell'ospite. In un exploit reale, non hai quel lusso.
Il kernel risolve questo problema per sé con la regione di mappatura diretta: una mappatura virtuale contigua di tutta la RAM fisica. Su x86-64, questa regione convenzionalmente inizia a 0xffff888000000000, ma con KASLR abilitato la base è randomizzata. Il kernel memorizza la base effettiva in un simbolo chiamato page_offset_base.
Abbiamo avviato con nokaslr, quindi la base è al suo valore predefinito. Confermiamo:```
(gdb) x/s 0xffff888000000000 + 0x37fdc90
0xffff888037fdc90: "FLAG{p4g3_t4bl3_w4lk3r}"
Stessa memoria fisica, accessibile tramite un indirizzo virtuale del kernel. È così che il kernel stesso legge memoria fisica arbitraria: `phys_to_virt()` è semplicemente `page_offset_base + phys_addr`.
Questo è anche il motivo per cui gli exploit del kernel si preoccupano di divulgare `page_offset_base`. Se KASLR è abilitato, non sai dove inizia la mappa diretta, quindi non puoi convertire indirizzi fisici in indirizzi virtuali del kernel. Divulga la base e potrai leggere o scrivere qualsiasi indirizzo fisico attraverso la mappa diretta, incluse le stesse voci della tabella delle pagine.
---
## Trovare le tabelle delle pagine di un altro processo
La nostra configurazione garantiva che CR3 puntasse alle tabelle delle pagine del binario della challenge quando gdb fermava la VM. Ma cosa succede se devi percorrere le tabelle delle pagine di un processo _diverso_?
Il valore CR3 di ogni processo è memorizzato nella sua `task_struct`. Il percorso è:
task_struct -> mm_struct -> pgd -> physical page
In gdb con i simboli del kernel, puoi trovare task_struct di init (PID 1) ed estrarre la sua radice della tabella pagine:```
(gdb) p/x init_task.mm->pgd
$1 = 0xffff8880066c7000
Questa è un indirizzo virtuale del kernel nella mappa diretta. Sottrai la base per ottenere l'indirizzo fisico:``` 0xffff8880066c7000 - 0xffff888000000000 = 0x66c7000
È lo stesso CR3 con cui abbiamo iniziato, il che ha senso: il nostro binario challenge _è_ PID 1 in questo initramfs minimale.
Per altri processi, percorreresti la lista dei task (la lista concatenata `init_task.tasks`), troveresti il target ed estrarresti il suo `mm->pgd` allo stesso modo. Ogni processo ha il proprio albero di tabelle delle pagine radicato nel proprio CR3. Il kernel scambia il CR3 a ogni cambio di contesto, dando a ogni processo l'illusione di una memoria privata.
---
## Cosa significa questo
Se sei arrivato fin qui facendo effettivamente la camminata (non solo leggendo), ora hai qualcosa che nessun diagramma può darti: l'intuizione di come la memoria funzioni davvero a livello hardware.
Ecco dove quell'intuizione viene ripagata:
**ASLR randomizza l'indirizzo virtuale, non la camminata.** La struttura della tabella delle pagine è sempre la stessa: quattro livelli, 512 voci ciascuno, stessa disposizione dei bit. ASLR cambia quali indici calcolerai, ma il processo è identico.
**W^X è applicato nella tabella delle pagine.** Il bit R/W e il bit NX nel PTE sono ciò che fa funzionare `mprotect`. Quando un exploit tenta di eseguire uno shellcode nello stack, la CPU controlla il bit NX durante la traduzione e genera un fault.
**SMEP e SMAP controllano il bit User.** Supervisor Mode Execution/Access Prevention controlla il bit User/Supervisor in tutti i livelli della tabella delle pagine. Se una qualsiasi voce segna l'indirizzo come modalità utente e il codice del kernel tenta di eseguirlo o accedervi, la CPU genera un fault. Questo è il motivo per cui gli exploit moderni del kernel non possono semplicemente saltare a uno shellcode nello spazio utente.
**Gli exploit del kernel spesso mirano direttamente alle tabelle delle pagine.** Se puoi scrivere in un PTE, puoi cambiare a quale memoria fisica un indirizzo virtuale è mappato, cambiare i permessi o rimappare memoria del kernel come accessibile all'utente. Capire la camminata significa capire la superficie di attacco.
**KPTI divide la tabella delle pagine in due.** Invece di un unico set di tabelle delle pagine per processo, ora ce ne sono due: una per la modalità utente (con quasi tutte le pagine del kernel smappate) e una per la modalità kernel (con tutto). Il kernel scambia CR3 a ogni ingresso e uscita da una syscall. Puoi osservarlo: ferma la VM mentre sei nello spazio utente e leggi CR3, poi imposta un breakpoint su un ingresso di syscall e leggi CR3 di nuovo. Saranno diversi. La tabella delle pagine della modalità utente semplicemente non contiene voci per la memoria del kernel, quindi non c'è nulla da far trapelare anche se la camminata è completata.
La prossima volta che un write-up di un exploit del kernel menziona "remapping delle tabelle delle pagine", non sarà astratto. Saprai esattamente di quali byte stanno parlando, perché li hai letti tu stesso.
---
## Ulteriori letture
- [Comprendere il Paging](https://blog.zolutal.io/understanding-paging/): il tutorial che ha ispirato questo. Questo articolo cerca di spingere l'esplorazione un po' più avanti, ma è da lì che ho iniziato.
- [Intel SDM, Volume 3A, Chapter 4: "Paging"](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html): il riferimento autorevole (sorprendentemente leggibile una volta che hai fatto una camminata a mano)
- [pwn.college: Kernel Security](https://pwn.college/system-security/kernel-security/): le sfide che mi hanno spinto a esplorare realmente queste cose, altamente raccomandate