
Gehe manuell x86-64-Seitentabellen in qemu und gdb durch. Zerlege eine virtuelle Adresse, folge cr3 durch alle Ebenen des physischen Speichers und extrahiere eine Flag aus rohen Bytes.
Du hast über Paging gelesen. Die Diagramme ergeben Sinn. Vier Ebenen, je 9 Bits, Seitenrahmen, Offset. Klar. Aber dann stößt du auf eine Herausforderung, die verlangt, dass du tatsächlich Seitentabellen durchläufst, und du merkst, dass du es nicht kennst. Du kennst es über. Großer Unterschied.
Was bei mir funktioniert hat: vor QEMU und gdb zu sitzen und den Durchlauf selbst zu machen: jeden Index berechnen, jeden Eintrag aus dem physischen Speicher lesen, jeden Zeiger von Hand verfolgen. Ein Nachmittag davon konnte mehr lehren als Stunden von Vorlesungen.
Dies ist eine Sammlung meiner Notizen aus diesem Prozess. Falls dir noch die konzeptionelle Seite fehlt, schau dir zuerst Zardus' Vortrag über Kernel-Speicherverwaltung an. Das ist die Theorie. Das hier ist das Labor.
Das Ziel: eine virtuelle Adresse nehmen und sie durch den rohen physischen Speicher jagen, bis wir die Daten finden. Keine Kernel-Helfer. Keine Abstraktionen. Nur eine QEMU-VM, gdb und roher physischer Speicher.
Am Ende wird Paging nicht etwas sein, worüber du gelesen hast, sondern etwas, das du kennst, weil du es von Hand gemacht hast.
Ein vorgebauter Kernel und initramfs sind enthalten. Ich habe das unter Fedora ausgeführt, aber jedes Betriebssystem, das QEMU und gdb ausführt, sollte funktionieren. Installiere sie mit deinem Paketmanager:```
sudo apt install qemu-system-x86 gdb
sudo dnf install qemu-system-x86 gdb
brew install qemu gdb
### Die Challenge-Binärdatei
Das Ziel ist ein triviales C-Programm, das eine Flag im Speicher speichert und deren virtuelle Adresse ausgibt:```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)
{
}
}
Die aktive Schleife ist beabsichtigt. Ursprünglich habe ich pause() verwendet, aber das versetzt den Prozess in einem Syscall in den Schlafzustand: Wenn gdb die VM anhält, führt die CPU wahrscheinlich die Leerlaufaufgabe mit einer anderen CR3 aus. Eine Drehschleife hält den Prozess auf der CPU, sodass das Anhalten garantiert, dass Sie sich in seinem Kontext mit den richtigen Seitentabellen befinden.
Ein vorgebautes Initramfs mit dieser Binärdatei ist bereits in initramfs.cpio.gz enthalten. Wenn Sie es neu erstellen müssen (nur Linux, erfordert busybox und glibc-static), führen Sie make in diesem Verzeichnis aus.
./start.sh
Das Skript bootet den gebündelten Kernel und das Initramfs unter QEMU mit `-s` (GDB-Server auf `localhost:1234`) und `nokaslr`, sodass die Kernel-Adressen zwischen den Läufen fest bleiben.
Die VM bootet sofort und die Challenge-Binärdatei wird ausgeführt. Sie sehen die virtuelle Adresse der Flagge auf der Konsole ausgegeben.```
secret @ 0x7ffe08985c90
pid = 1
Spinning. Walk the page tables to find the flag.
Notieren Sie diese virtuelle Adresse. Das ist Ihr Ziel.

Die standardmäßige QEMU-Escape-Taste ist
Ctrl-a, aber das kollidiert mit meinem tmux-Präfix, daher verwendet das Skript-echr 0x11, um sie aufCtrl-qumzubelegen. Wenn SieCtrl-qfür etwas anderes verwenden, ändern Sie den Hexadezimalwert instart.shentsprechend Ihrer Einrichtung.
gdb -ex "target remote :1234"

---
## Dekomposition der virtuellen Adresse
Sie haben eine virtuelle Adresse. Aber wo sind die Daten, _wirklich_?
Virtuelle Adressen sind die höfliche Fiktion des Betriebssystems. Jeder Prozess
glaubt, er habe seinen eigenen privaten Speicher, der bei Null beginnt. In Wirklichkeit
befinden sich die Daten an einer völlig anderen, nicht zusammenhängenden Stelle im physischen RAM. Die Seitentabelle ist
die Karte zwischen beiden: eine Baumstruktur, die die CPU bei jedem
Speicherzugriff durchläuft (oder aus ihrem TLB-Cache abruft).
Machen wir also das Gleiche wie die CPU. Manuell. Um diese Adresse zu übersetzen, müssen wir
sie in die Indizes zerlegen, die die CPU auf jeder Ebene verwendet.
Eine x86-64 virtuelle Adresse ist 48 Bit breit. Diese 48 Bit sind in fünf
Felder aufgeteilt:```
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) │
└────────┴────────┴────────┴────────┴────────┴──────────┘
Jeder 9-Bit-Index wählt einen von 512 Einträgen in einer Seitentabelle auf dieser Ebene aus. Der 12-Bit-Offset wählt ein Byte innerhalb der finalen 4 KB (0x1000) Seite aus.
Um die Indizes zu extrahieren, verschieben und maskieren:``` 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 können Sie diese direkt berechnen:```
(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
Notieren Sie diese. Sie werden jede auf der entsprechenden Ebene verwenden.
Ihre Werte werden abweichen. Die Adresse
0x7ffe08985c90ist nur ein Beispiel. Verwenden Sie die Adresse, die Ihre Challenge-Binary ausgegeben hat.
Eine Anmerkung zum 5-Level-Paging. Aktuelle CPUs und Kernel unterstützen LA57, was eine fünfte Ebene (PML5) oberhalb des PGD hinzufügt und virtuelle Adressen auf 57 Bit erweitert. Der Durchlauf folgt dem gleichen Muster: ein weiterer 9-Bit-Index, eine weitere Tabellensuche. Die meisten Systeme verwenden noch 4-Level-Paging. Sie können dies überprüfen:
cat /proc/cpuinfo | grep la57. Alles in diesem Artikel geht von 4-Level-Paging aus.
Jeder Baum hat eine Wurzel. Für Seitentabellen befindet sich diese Wurzel im CR3-Register: Es enthält die physische Adresse der obersten Tabelle, des PGD. Jeder Prozess erhält seinen eigenen CR3-Wert, den der Kernel bei einem Kontextwechsel austauscht.
Dies ist unser Einstiegspunkt in den Durchlauf. Lesen Sie ihn aus gdb aus:``` (gdb) info registers cr3 cr3 0x66c7000 [ PDBR=26311 PCID=0 ]
Die Seiten-Tabellen-Basis ist `0x66c7000`. Die unteren 12 Bits sind PCID/Flags (hier Null),
die Basisadresse ist also der Wert wie er ist.
Hier beginnt der Walk.
---
## Der Walk
Hier ist der Trick: Jede Ebene
folgt demselben Muster. Die Flags variieren geringfügig zwischen den Ebenen, aber der
Prozess nicht. Das Muster:
1. **Berechne die Eintragsadresse:** `base + index * 8` (jeder Eintrag ist 8 Bytes)
2. **Lese den Eintrag aus dem physischen Speicher** mit dem QEMU-Monitor-Befehl `xp`
3. **Dekodiere die Flags** (siehe Referenz unten). Wenn Present (Bit 0) 0 ist, ist die Seite nicht gemappt und der Walk stoppt
4. **Extrahiere die Basis der nächsten Tabelle:** maskiere den Eintrag mit `& 0x000FFFFFFFFFF000`
5. **Gehe zur nächsten Ebene**
Jeder Eintrag ist 64 Bit. Die gemeinsamen Flag-Bits:```
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
Bits [51:12] enthalten die physische Adresse der nächsten Tabelle (bzw. des Seitenrahmens auf der letzten Ebene). Bits 9-11 werden von der Hardware ignoriert und stehen dem Betriebssystem zur Verfügung. Linux nutzt sie für Buchhaltungszwecke (z. B. soft-dirty tracking). Bits 52-62 sind reserviert. Du wirst beiden begegnen, wenn du in Exploit-Berichten PTEs liest.
Behalte diese Flag-Tabelle griffbereit, während du vorgehst.
Los geht's.
Wir haben die PGD-Basis aus CR3: 0x66c7000.
Unser PGD-Index ist 0xff.
Berechne die Eintragsadresse:``` entry = 0x66c7000 + 0xff * 8 = 0x66c77f8
Lies es in gdb mit QEMUs Befehl zur physischen Speicheruntersuchung:```
(gdb) monitor xp/1gx 0x66c77f8
000000066c77f8: 0x0000000006713067
Entry: 0x6713067 [Present RW User Accessed Dirty].
Next base: 0x6713067 & 0x000FFFFFFFFFF000 = 0x6713000.
Die Basis, die wir aus dem PGD-Eintrag (0x6713000) extrahiert haben, zeigt auf die PUD. Gleicher Vorgang, nächster Index: 0x1f8.```
entry = 0x6713000 + 0x1f8 * 8 = 0x6713fc0
## Installation/Bereitstellung
Die Installation und Bereitstellung sind (glücklicherweise) sehr unkompliziert:
* Erstellen Sie ein AWS-Konto, einen [IAM-Benutzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html) und [Zugriffsschlüssel](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html).
* Installieren Sie die [Terraform CLI](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli).
* Installieren Sie die [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) und [konfigurieren Sie die Anmeldeinformationen](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) für den soeben erstellten Benutzer.
* Klonen Sie das Repository: `git clone [email protected]:hashishrajan/DVFaaS.git`
* Führen Sie `terraform init` aus
* Führen Sie `terraform apply` aus – dies wird einige Minuten dauern. Terraform gibt die öffentlich zugängliche API Gateway-URL aus, über die die Challenge-App bereitgestellt wird. Notieren Sie sich diese.
Terraform stellt die folgenden Ressourcen in Ihrem AWS-Konto bereit:
* **AWS Systems Manager Parameter Store-Parameter**, die als Konfigurationseingaben in den Lambda-Funktionscode verwendet werden.
* _Hinweis: Wie könnte ein Angreifer diese Konfigurationszeichenfolgen herunterladen? Wofür könnten sie verwendet werden?_
* **Lambda-Funktionen** mit der Challenge-Logik. Die Funktionstrigger werden über einen API Gateway-Endpunkt bereitgestellt.
* **API Gateway REST-API** mit Endpunkten für jedes Challenge-Szenario.
## Bereinigung
Wenn Sie fertig sind, stellen Sie sicher, dass Sie die Infrastruktur abbauen, damit Ihnen keine Kosten mehr entstehen. Gehen Sie dazu wie folgt vor:
1. Fügen Sie `force_destroy = true` zu den Ressourcen `aws_s3_bucket.lambda_logs` und `aws_s3_bucket.serverless_access_logs` in `infra/main.tf` hinzu. Andernfalls wird Terraform diese Buckets nicht zerstören, da sie Zugriffsprotokolle enthalten. Alternativ können Sie diese Buckets selbst löschen.
2. Führen Sie `terraform destroy` aus
## Bekannte Probleme
* Für einen erstmaligen Benutzer eines brandneuen AWS-Kontos schlägt Terraform gelegentlich fehl, die Challenge-Ressourcen (einschließlich der REST-API) beim ersten Durchlauf zu erstellen. Wenn Sie die Fehlermeldung `Error: creating API Gateway v2 Stage: NotFoundException: Invalid Route key, or Stage not found` erhalten, führen Sie einfach `terraform apply` erneut aus. Es sollte beim nächsten Mal funktionieren.
* Wenn Sie beim Testen eines der API-Endpunkte die Meldung `Invalid permissions on Lambda function` erhalten, warten Sie einige Minuten und versuchen Sie es erneut. Möglicherweise gibt es eine Verzögerung, bis die API Gateway-Konfiguration vollständig bereit ist.```
(gdb) monitor xp/1gx 0x6713fc0
00000006713fc0: 0x00000000066ac067
Entry: 0x66ac067 [Vorhanden, RW, Benutzer, Zugegriffen, Verändert]. Seitengröße (Bit 7) = 0, keine 1-GB-Riesenseite.
Nächste Basis: 0x66ac067 & 0x000FFFFFFFFFF000 = 0x66ac000.
Basis: 0x66ac000. PMD-Index: 0x44.```
entry = 0x66ac000 + 0x44 * 8 = 0x66ac220
(gdb) monitor xp/1gx 0x66ac220 000000066ac220: 0x00000000066c4067
Entry: `0x66c4067` [Present RW User Accessed Dirty]. Seitengröße (Bit 7) = 0, keine 2 MB huge
page.
Nächste Basis: `0x66c4067 & 0x000FFFFFFFFFF000` = `0x66c4000`.
### Ebene 1: PT (Seitentabelle)
Basis: `0x66c4000`. PT-Index: `0x185`.```
entry = 0x66c4000 + 0x185 * 8 = 0x66c4c28
Eintrag: `0x80000000037fd867` [Present RW User Accessed Dirty NX]. Dies ist der
endgültige PTE.
Physischer Seitenrahmen: `0x80000000037fd867 & 0x000FFFFFFFFFF000` =
`0x37fd000`.
### Was ist, wenn Present = 0?
Schauen wir, was passiert, wenn der Walk auf eine nicht gemappte Seite trifft. Wählen Sie eine Adresse,
die mit ziemlicher Sicherheit nicht gemappt ist, etwas in der Mitte des Adressraums:```
(gdb) p/x (0x0000414141414000 >> 39) & 0x1ff
$1 = 0x82
Was ist es?``` (gdb) monitor xp/1gx 0x66c7000 + 0x82 * 8 00000000066c7410: 0x0000000000000000
Alle Nullen. Bit 0 (Present) ist gelöscht. Der Walk stoppt hier. Es gibt keine PUD, keine
PMD, keine PT, keinen Page Frame. Diese Adresse bildet keinen physikalischen Speicher ab.
Wenn die CPU während der normalen Ausführung darauf gestoßen wäre, hätte sie einen **Page Fault**
(Interrupt 14) ausgelöst. Der Fault-Handler des Kernels würde dann entscheiden, was zu tun ist: die
Seite von der Festplatte laden (Swap), eine neue Seite zuweisen (Demand Paging) oder den Prozess mit
einem Segfault beenden.
Der Punkt: Die Seitentabelle ist nicht nur eine Übersetzungsstruktur. Sie ist auch der
Mechanismus, der virtuellen Speicher _virtuell_ macht. Nicht jede Adresse muss physischen Speicher
dahinter haben. Die CPU entdeckt dies während des Walks, eine Ebene nach der anderen.
---
## Die Enthüllung
Kombiniere den physikalischen Page Frame mit dem Offset aus der ursprünglichen virtuellen
Adresse:```
Physical address = 0x37fd000 | 0xc90 = 0x37fdc90
Lesen Sie es jetzt:``` (gdb) monitor xp/6bx 0x37fdc90 00000000037fdc90: 0x46 0x4c 0x41 0x47 0x7b 0x70
Das sind `F`, `L`, `A`, `G`, `{`, `p`: der Anfang unserer Flagge. Weiterlesen:```
(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
| Titel | Beschreibung |
|---|---|
| PwnAdventure3 - Web | Web-Hacking-Lösung für PwnAdventure3 |
| PwnAdventure3 | Einen Multiplayer-Spiel hacken. |
| FLAG{p4g3_t4bl3_w4lk3r} |

Da ist es. Du hast gerade das getan, was die CPU milliarden Mal pro Sekunde tut, aber du hast es von Hand gemacht, indem du rohe Bytes aus dem physischen Speicher gelesen hast. Vier Tabellen tief, nichts hinter einer Abstraktion versteckt.
Zuvor war Paging ein Diagramm in einer Folienpräsentation. Jetzt ist es eine Abfolge von Lesevorgängen, die du in deinem Kopf wiederholen kannst: Basis, Index, Verschiebung, Maske, folgen. Dieser Unterschied ist wichtig, wenn du vor einem Kernel-Exploit stehst und darüber nachdenken musst, was ein Schreiben auf eine PTE tatsächlich bewirkt.
Du kannst dein Ergebnis mit dem QEMU-Monitorbefehl `gva2gpa` (virtuelle Gastadresse in physische Gastadresse) überprüfen, der den Walk intern durchführt:```
(qemu) gva2gpa 0x7ffe08985c90
gpa: 0x37fdc90
Wir haben während des Walkthroughs auf jeder Ebene Flags dekodiert, aber übersprungen, was sie für die Sicherheit bedeuten. Schauen Sie sich den endgültigen PTE:``` 0x80000000037fd867
Das Flag --connection-data nimmt einen URL-kodierten Wert entgegen, zum Beispiel: <user>:<pass>@tcp(<hostname>:<port>)/?readTimeout=5s&writeTimeout=30s".```
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
Das macht Sinn: Das Geheimnis ist eine Stack-Variable. Der Stack ist lesbar, beschreibbar und 'dirty' (es wurde darauf geschrieben). Er ist als nicht ausführbar markiert, da moderne Systeme W^X erzwingen: Eine Seite, die beschreibbar ist, sollte nicht ausführbar sein.
Die Flags auf jeder Ebene werden von der Hardware UND-verknüpft. Wenn der PUD-Eintrag User=0 hat, ist nichts darunter für den Benutzer zugänglich, unabhängig davon, was der PTE sagt. Die restriktivste Berechtigung gewinnt.
Vier Speicherzugriffe, um nur ein Byte zu lesen. Das ist teuer. Die CPU führt den Walk durch die Seitentabelle nicht bei jedem Speicherzugriff tatsächlich durch. Sie speichert das Ergebnis in einem Translation Lookaside Buffer (TLB) zwischen.
Nach dem ersten Zugriff auf die virtuelle Adresse unseres Flags speichert die CPU die Zuordnung 0x7ffe08985c90 -> 0x37fdc90 (ungefähr) im TLB. Nachfolgende Zugriffe treffen den Cache und überspringen den Walk vollständig. Die Seitentabelle bleibt unberührt im RAM.
Dies ist für normalen Code transparent. Aber es wird relevant, sobald Sie einen Seitentabelleneintrag modifizieren. Wenn Sie eine neue physikalische Adresse in eine PTE schreiben, bemerkt die CPU das nicht: Der TLB enthält immer noch die alte Zuordnung. Sie müssen ihn explizit leeren.
Der Kernel tut dies mit der invlpg-Instruktion, die den TLB-Eintrag für eine einzelne virtuelle Adresse ungültig macht. Rufen Sie mprotect aus dem Userspace auf, und das passiert im Hintergrund: Der Kernel aktualisiert die PTE-Flags und leert dann den TLB, damit die CPU die neuen Berechtigungen übernimmt.
Dies hat direkte Sicherheitsauswirkungen. In einem Kernel-Exploit müssen Sie, wenn Sie es schaffen, in eine PTE zu schreiben (z.B. das NX-Bit löschen, um den Stack ausführbar zu machen), auch den TLB leeren, bevor die CPU die Änderung übernimmt. Manchmal erledigt der Kernel dies als Nebeneffekt des von Ihnen ausgelösten Codepfads. Manchmal müssen Sie es selbst arrangieren. In jedem Fall müssen Sie wissen, dass der TLB existiert, sonst funktioniert Ihr Exploit in der Theorie, aber nicht in der Praxis.
Im obigen Walkthrough sind wir durch alle vier Ebenen gegangen. Aber der Walk kann vorzeitig enden, wenn ein Page Size Bit (Bit 7) gesetzt ist.
Auf Ebene 3 (PUD): Wenn Bit 7 gesetzt ist, bildet der Eintrag direkt eine 1-GB-Seite ab. Die physikalische Adresse wird aus dem Eintrag übernommen, und die Bits [29:0] der virtuellen Adresse werden zum Offset (30 Bits = 1 GB).
Auf Ebene 2 (PMD): Wenn Bit 7 gesetzt ist, bildet der Eintrag eine 2-MB-Seite ab. Die Bits [20:0] der virtuellen Adresse werden zum Offset (21 Bits = 2 MB).
Sie werden oft Huge Pages in Kernel-Mappings sehen. Die Direct-Map-Region des Kernels (0xffff888000000000 auf den meisten 64-Bit-Kerneln) verwendet häufig 2-MB- oder 1-GB-Seiten, um den TLB-Druck zu verringern.
Wenn Sie während Ihres Walks auf eine Huge Page stoßen, ändert sich die Formel:``` 2 MB page: phys = (PMD_entry & 0x000FFFFFFFE00000) | (VA & 0x1FFFFF) 1 GB page: phys = (PUD_entry & 0x000FFFFFC0000000) | (VA & 0x3FFFFFFF)
---
## Automatisieren des Durchlaufs
Jetzt, da wir den Prozess kennen, kodieren wir ihn. `pagewalk.py` ist ein
gdb Python-Skript, das den gleichen Durchlauf durchführt, den wir gerade gemacht haben. Die Kernlogik passt in
eine Funktion:```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
Das vollständige Skript (mit Flag-Dekodierung und hübscher Ausgabe) befindet sich in pagewalk.py. Führen Sie es aus und verwenden Sie es, um Ihre manuelle Arbeit zu überprüfen oder andere Adressen zu erkunden:```
(gdb) source ./pagewalk.py
Page walk command loaded. Usage: pagewalk
(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

Beachten Sie, wie das Skript auf PUD- und PMD-Ebene nach Riesen-Seiten (huge pages) sucht, bevor es den Durchlauf fortsetzt. Das ist dieselbe Logik, die wir im Abschnitt über Riesen-Seiten besprochen haben: Wenn PageSize (Bit 7) gesetzt ist, endet der Durchlauf vorzeitig und der Offset ist breiter.
Versuchen Sie, die Adresse einer Funktion zu durchlaufen: Sie werden sehen, dass das NX-Bit gelöscht ist (Code muss ausführbar sein). Versuchen Sie einen schreibgeschützten Datenbereich: Sie werden sehen, dass R/W gelöscht ist.
---
## Lesen des physischen Speichers ohne QEMU
Während dieser Übung haben wir `monitor xp` verwendet, um den physischen Speicher direkt zu lesen. Das funktioniert, weil der QEMU-Monitor außerhalb der VM sitzt und auf den physischen Adressraum des Gasts zugreifen kann. In einem echten Exploit haben Sie diesen Luxus nicht.
Der Kernel löst dies für sich selbst mit der **Direct-Map-Region**: einer zusammenhängenden virtuellen Abbildung des _gesamten_ physischen RAMs. Auf x86-64 beginnt diese Region konventionell bei `0xffff888000000000`, aber mit aktiviertem KASLR ist die Basis randomisiert. Der Kernel speichert die tatsächliche Basis in einem Symbol namens `page_offset_base`.
Wir haben mit `nokaslr` gebootet, daher ist die Basis auf ihrem Standardwert. Bestätigen wir das:```
(gdb) x/s 0xffff888000000000 + 0x37fdc90
0xffff888037fdc90: "FLAG{p4g3_t4bl3_w4lk3r}"
Derselbe physische Speicher, der über eine Kernel-Virtualadresse zugegriffen wird. So liest der Kernel selbst beliebigen physischen Speicher: phys_to_virt() ist einfach page_offset_base + phys_addr. Aus diesem Grund ist es für Kernel-Angriffe wichtig, die page_offset_base zu leaken. Wenn KASLR aktiviert ist, weiß man nicht, wo das direkte Mapping beginnt, und kann keine physischen Adressen in Kernel-Virtualadressen umwandeln. Leake die Basis, und man kann jede physische Adresse über das direkte Mapping lesen oder schreiben, einschließlich der Seitentabelleneinträge selbst.
Unser Setup garantierte, dass CR3 auf die Seitentabellen der Challenge-Binärdatei zeigte, als gdb die VM anhielt. Aber was, wenn man die Seitentabellen eines anderen Prozesses durchlaufen muss? Der CR3-Wert jedes Prozesses wird in seiner task_struct gespeichert. Der Pfad ist:```
task_struct -> mm_struct -> pgd -> physical page
In gdb mit Kernel-Symbolen kann man die task_struct von init (PID 1) finden und ihre Seitentabellenwurzel extrahieren:```
(gdb) p/x init_task.mm->pgd
$1 = 0xffff8880066c7000
Das ist eine Kernel-Virtualadresse in der direkten Abbildung. Entferne die Basis, um die physische Adresse zu erhalten:``` 0xffff8880066c7000 - 0xffff888000000000 = 0x66c7000
Das ist derselbe CR3, mit dem wir begonnen haben, was Sinn ergibt: Unser Challenge-Binary
_ist_ PID 1 in diesem minimalen Initramfs.
Bei anderen Prozessen würdest du die Aufgabenliste durchgehen (`init_task.tasks`-verkettete Liste),
das Ziel finden und auf dieselbe Weise dessen `mm->pgd` extrahieren. Jeder Prozess hat
seinen eigenen Seitentabellenbaum, der in seinem eigenen CR3 verwurzelt ist. Der Kernel tauscht CR3 bei jedem
Kontextwechsel aus und gibt jedem Prozess die Illusion eines privaten Speichers.
---
## Was das bedeutet
Wenn du bis hierher gekommen bist, indem du tatsächlich den Walk durchgeführt hast (nicht nur gelesen),
hast du jetzt etwas, das dir keine noch so große Anzahl an Diagrammen geben kann: Intuition dafür, wie
Speicher auf Hardware-Ebene tatsächlich funktioniert. Hier zahlt sich diese Intuition aus:
**ASLR randomisiert die virtuelle Adresse, nicht den Walk.** Die Struktur der
Seitentabelle ist immer gleich: vier Ebenen, 512 Einträge pro Ebene, gleiches Bit-Layout.
ASLR ändert, welche Indizes du berechnen wirst, aber der Vorgang ist identisch.
**W^X wird in der Seitentabelle erzwungen.** Das R/W-Bit und das NX-Bit im PTE sind das,
was `mprotect` zum Funktionieren bringt. Wenn ein Exploit versucht, Shellcode auf dem
Stack auszuführen, überprüft die CPU während der Übersetzung das NX-Bit und löst einen Fehler aus.
**SMEP und SMAP prüfen das User-Bit.** Supervisor Mode Execution/Access
Prevention prüft das User/Supervisor-Bit auf allen Ebenen der Seitentabelle.
Wenn irgendein Eintrag die Adresse als Benutzermodus markiert und Kernel-Code versucht,
sie auszuführen oder darauf zuzugreifen, löst die CPU einen Fehler aus. Deshalb können moderne Kernel-
Exploits nicht einfach zu Userspace-Shellcode springen.
**Kernel-Exploits zielen oft direkt auf Seitentabellen ab.** Wenn du in einen
PTE schreiben kannst, kannst du ändern, auf welchen physischen Speicher eine virtuelle Adresse abgebildet wird,
Berechtigungen ändern oder Kernel-Speicher als benutzerzugänglich ummappen. Den Walk zu verstehen bedeutet,
die Angriffsfläche zu verstehen.
**KPTI spaltet die Seitentabelle in zwei.** Statt eines Satzes von Seitentabellen pro
Prozess gibt es jetzt zwei: einen für den Benutzermodus (mit fast allen Kernel-Seiten
unabgebildet) und einen für den Kernel-Modus (mit allem). Der Kernel tauscht CR3 bei jedem
Syscall-Eintritt und -Austritt aus. Du kannst dies beobachten: Halte die VM an, während du dich im Userspace
befindest, und lies CR3, setze dann einen Breakpoint auf einen Syscall-Eintritt und lies CR3
erneut. Sie werden sich unterscheiden. Die Benutzermodus-Seitentabelle enthält einfach keine Einträge
für Kernel-Speicher, sodass es nichts zu leaken gibt, selbst wenn der Walk abgeschlossen wird.
Wenn das nächste Mal ein Kernel-Exploit-Write-up „Seitentabellen ummappen“ erwähnt, wird es nicht abstrakt sein.
Du wirst genau wissen, von welchen Bytes sie sprechen, weil du sie selbst gelesen hast.
---
## Weiterführende Literatur
- [Understanding Paging](https://blog.zolutal.io/understanding-paging/):
das Tutorial, das dieses hier inspiriert hat. Dieser Artikel versucht, die
Erkundung etwas weiter zu treiben, aber hier hat es für mich angefangen.
- [Intel SDM, Volume 3A, Chapter 4: "Paging"](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html):
die maßgebliche Referenz (überraschend lesbar, wenn du einmal einen Walk
von Hand durchgeführt hast).
- [pwn.college: Kernel Security](https://pwn.college/system-security/kernel-security/):
die Herausforderungen, die mich dazu gebracht haben, diese Dinge tatsächlich zu erkunden, sehr empfehlenswert.