
qemu और gdb में हाथ से x86-64 पेज टेबल्स को चलें। एक वर्चुअल एड्रेस को डीकंपोज़ करें, भौतिक मेमोरी के सभी स्तरों के माध्यम से cr3 का अनुसरण करें, और रॉ बाइट्स से एक फ्लैग निकालें।
आपने पेजिंग के बारे में पढ़ा है। आरेख समझ में आते हैं। चार स्तर, 9 बिट प्रत्येक, पेज फ्रेम, ऑफसेट। बिल्कुल। लेकिन फिर आप एक ऐसी चुनौती पर आते हैं जिसमें वास्तव में पेज तालिकाओं को चलने की आवश्यकता होती है, और आपको एहसास होता है कि आप इसे जानते नहीं हैं। आप इसे के बारे में जानते हैं। बड़ा अंतर।
मेरे लिए जो काम कर गया वह QEMU और gdb के सामने बैठकर स्वयं वॉक करना था: हर इंडेक्स की गणना करना, हर एंट्री को भौतिक मेमोरी से पढ़ना, हर पॉइंटर को हाथ से अनुसरण करना। उस दोपहर ने घंटों के व्याख्यानों से अधिक सिखाया होगा।
यह उस प्रक्रिया से मेरे नोट्स का एक संग्रह है। यदि आप अभी भी सैद्धांतिक पक्ष से चूक रहे हैं, तो पहले Zardus का कर्नेल मेमोरी प्रबंधन पर व्याख्यान देखें। वह सिद्धांत है। यह प्रयोगशाला है।
लक्ष्य: एक वर्चुअल पता लेना और उसे कच्ची भौतिक मेमोरी के माध्यम से तब तक ट्रैक करना जब तक हम डेटा न ढूंढ लें। कोई कर्नेल सहायक नहीं। कोई अमूर्तता नहीं। सिर्फ एक QEMU VM, gdb, और कच्ची भौतिक मेमोरी।
अंत में, पेजिंग कुछ ऐसा नहीं होगा जिसके बारे में आपने पढ़ा है, यह कुछ ऐसा होगा जिसे आप जानते हैं क्योंकि आपने इसे हाथ से किया है।
एक पूर्व-निर्मित कर्नेल और initramfs शामिल हैं, मैंने इसे Fedora में चलाया, लेकिन कोई भी OS जो QEMU और gdb चलाता है, काम करना चाहिए। इन्हें अपने पैकेज मैनेजर से स्थापित करें:```
sudo apt install qemu-system-x86 gdb
sudo dnf install qemu-system-x86 gdb
brew install qemu gdb
### चुनौती बाइनरी
लक्ष्य एक सामान्य C प्रोग्राम है जो मेमोरी में एक फ़्लैग संग्रहीत करता है और उसका वर्चुअल पता प्रिंट करता है:```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)
{
}
}
बिज़ी लूप जानबूझकर है। मैंने मूल रूप से pause() का उपयोग किया था, लेकिन वह प्रक्रिया को syscall में सोने के लिए डालता है: जब gdb VM को रोकता है, तो CPU संभवतः अलग CR3 के साथ निष्क्रिय कार्य चला रहा होता है। एक स्पिनिंग लूप प्रक्रिया को CPU पर रखता है, इसलिए रोकना यह सुनिश्चित करता है कि आप सही पेज टेबल के साथ इसके संदर्भ में हैं।
इस बाइनरी के साथ एक पूर्व-निर्मित initramfs initramfs.cpio.gz में पहले से शामिल है। यदि आपको इसे पुनर्निर्माण करने की आवश्यकता है (केवल Linux, busybox और glibc-static की आवश्यकता है), तो इस निर्देशिका में make चलाएँ।
./start.sh
स्क्रिप्ट बंडल कर्नेल और initramfs को QEMU के तहत `-s` (gdb सर्वर `localhost:1234` पर) और `nokaslr` के साथ बूट करती है ताकि कर्नेल पते रनों के बीच स्थिर रहें।
VM तुरंत बूट होता है और चुनौती बाइनरी चलती है। आप कंसोल पर फ़्लैग का वर्चुअल पता प्रिंट होता देखेंगे।```
secret @ 0x7ffe08985c90
pid = 1
Spinning. Walk the page tables to find the flag.
उस वर्चुअल पते को लिख लें। यही आपका लक्ष्य है।

डिफ़ॉल्ट QEMU एस्केप कुंजी
Ctrl-aहै, लेकिन यह मेरे tmux प्रीफिक्स से टकराता है, इसलिए स्क्रिप्ट इसेCtrl-qपर रीमैप करने के लिए-echr 0x11का उपयोग करती है। यदि आपCtrl-qका उपयोग किसी और चीज़ के लिए करते हैं, तोstart.shमें हेक्स मान को अपने सेटअप के अनुसार बदलें।
दूसरे टर्मिनल में:``` gdb -ex "target remote :1234"

---
## वर्चुअल एड्रेस को विघटित करना
आपके पास एक वर्चुअल एड्रेस है। लेकिन डेटा, _वास्तव में_ कहाँ है?
वर्चुअल एड्रेस ऑपरेटिंग सिस्टम का एक शिष्ट कल्पना (polite fiction) हैं। हर प्रक्रिया सोचती है कि उसकी अपनी निजी मेमोरी शून्य से शुरू होती है। वास्तव में, डेटा फिजिकल RAM में किसी पूरी तरह से असंबंधित स्थान पर रहता है। पेज टेबल दोनों के बीच का नक्शा है: एक पेड़ संरचना जिसे CPU हर एक मेमोरी एक्सेस पर चलता है (या अपने TLB कैश से देखता है)।
तो चलिए वही करते हैं जो CPU करता है। मैन्युअली। उस एड्रेस का अनुवाद करने के लिए, हमें इसे उन सूचकांकों में विघटित करना होगा जिनका उपयोग CPU प्रत्येक स्तर पर करता है।
एक x86-64 वर्चुअल एड्रेस 48 बिट चौड़ा होता है। उन 48 बिटों को पाँच क्षेत्रों में विभाजित किया गया है:```
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) │
└────────┴────────┴────────┴────────┴────────┴──────────┘
प्रत्येक 9-बिट इंडेक्स उस स्तर पर पेज टेबल में 512 प्रविष्टियों में से एक का चयन करता है। 12-बिट ऑफ़सेट अंतिम 4 KB (0x1000) पेज के भीतर एक बाइट का चयन करता है।
इंडेक्स निकालने के लिए, शिफ्ट और मास्क करें:``` PGD index = (VA >> 39) & 0x1FF PUD index = (VA >> 30) & 0x1FF PMD index = (VA >> 21) & 0x1FF PT index = (VA >> 12) & 0x1FF Offset = VA & 0xFFF
gdb में, आप इन्हें सीधे गणना कर सकते हैं:```
(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
इन्हें लिख लें। आप प्रत्येक का उपयोग उसके संबंधित स्तर पर करेंगे।
आपके मान भिन्न होंगे। पता
0x7ffe08985c90केवल एक उदाहरण है। अपने चैलेंज बाइनरी द्वारा मुद्रित किसी भी पते का उपयोग करें।
5-स्तरीय पेजिंग पर एक नोट। हाल के CPUs और कर्नेल LA57 का समर्थन करते हैं, जो PGD के ऊपर एक पाँचवाँ स्तर (PML5) जोड़ता है और वर्चुअल पतों को 57 बिट तक विस्तारित करता है। वॉक वही पैटर्न है: एक और 9-बिट इंडेक्स, एक और टेबल लुकअप। अधिकांश सिस्टम अभी भी 4-स्तरीय पेजिंग चलाते हैं। आप अपना जाँच सकते हैं:
cat /proc/cpuinfo | grep la57। इस लेख में सब कुछ 4-स्तरीय मानता है।
हर पेड़ की एक जड़ होती है। पेज टेबल के लिए, वह जड़ CR3 रजिस्टर में है: इसमें शीर्ष-स्तरीय तालिका, PGD का भौतिक पता होता है। प्रत्येक प्रक्रिया को अपना स्वयं का CR3 मान मिलता है, कर्नेल इसे कॉन्टेक्स्ट स्विच पर बदलता है।
यह वॉक में हमारा प्रवेश बिंदु है। इसे gdb से पढ़ें:``` (gdb) info registers cr3 cr3 0x66c7000 [ PDBR=26311 PCID=0 ]
पेज टेबल बेस `0x66c7000` है। निचले 12 बिट PCID/फ़्लैग्स (यहाँ शून्य) हैं, इसलिए बेस पता ज्यों का त्यों मान है।
यहीं से वॉक शुरू होता है।
---
## वॉक
यहाँ ट्रिक है: हर स्तर एक ही पैटर्न का पालन करता है। फ़्लैग्स स्तरों के बीच थोड़े भिन्न होते हैं, लेकिन प्रक्रिया नहीं बदलती। पैटर्न:
1. **प्रविष्टि पता निकालें:** `base + index * 8` (प्रत्येक प्रविष्टि 8 बाइट्स है)
2. **भौतिक मेमोरी से प्रविष्टि पढ़ें** QEMU मॉनिटर के `xp` कमांड का उपयोग करके
3. **फ़्लैग्स को डिकोड करें** (नीचे दिए गए संदर्भ देखें)। यदि Present (बिट 0) 0 है, तो पेज मैप नहीं हुआ है और वॉक रुक जाता है
4. **अगले टेबल का बेस निकालें:** प्रविष्टि को `& 0x000FFFFFFFFFF000` से मास्क करें
5. **अगले स्तर पर जाएँ**
प्रत्येक प्रविष्टि 64 बिट्स की होती है। सामान्य फ़्लैग बिट्स:```
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
बिट्स [51:12] अगली तालिका (या अंतिम स्तर पर पेज फ्रेम) का भौतिक पता रखते हैं। बिट्स 9-11 हार्डवेयर द्वारा अनदेखा किए जाते हैं और OS उपयोग के लिए उपलब्ध हैं। लिनक्स उन्हें बुककीपिंग (उदाहरण के लिए, सॉफ्ट-डर्टी ट्रैकिंग) के लिए उपयोग करता है। बिट्स 52-62 आरक्षित हैं। आप एक्सप्लॉइट राइटअप में PTEs पढ़ते समय दोनों का सामना करेंगे।