
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 पढ़ते समय दोनों का सामना करेंगे।
चलते समय इस फ्लैग तालिका को पास रखें।
चलिए शुरू करते हैं।
हमारे पास CR3 से PGD बेस है: 0x66c7000।
हमारा PGD इंडेक्स 0xff है।
एंट्री पता गणना करें:``` entry = 0x66c7000 + 0xff * 8 = 0x66c77f8
इसे gdb से QEMU के भौतिक मेमोरी जाँच कमांड का उपयोग करके पढ़ें:```
(gdb) monitor xp/1gx 0x66c77f8
000000066c77f8: 0x0000000006713067
Entry: 0x6713067 [Present RW User Accessed Dirty].
Next base: 0x6713067 & 0x000FFFFFFFFFF000 = 0x6713000.
PGD एंट्री से निकाला गया आधार (0x6713000) PUD की ओर इंगित करता है। वही प्रक्रिया, अगला इंडेक्स: 0x1f8।```
entry = 0x6713000 + 0x1f8 * 8 = 0x6713fc0
.(No input provided)```
(gdb) monitor xp/1gx 0x6713fc0
00000006713fc0: 0x00000000066ac067
Entry: 0x66ac067 [Present RW User Accessed Dirty]. पृष्ठ आकार (बिट 7) = 0, 1 GB ह्यूज पेज नहीं है।
अगला आधार: 0x66ac067 & 0x000FFFFFFFFFF000 = 0x66ac000।
आधार: 0x66ac000। PMD सूचकांक: 0x44।```
entry = 0x66ac000 + 0x44 * 8 = 0x66ac220
| [ZMap](https://zmap.io/) | इंटरनेट-व्यापी नेटवर्क स्कैनर। |
|--------|--------|
| [Zgrab2](https://github.com/zmap/zgrab2) | ZMap के साथ संचालित होने वाला Go-आधारित एप्लिकेशन लेयर स्कैनर। |
## 📊 नेटवर्क विश्लेषण
नेटवर्क ट्रैफ़िक विश्लेषण, ट्रैफ़िक निरीक्षण, नेटवर्क फोरेंसिक्स, नेटवर्क निगरानी और पैकेट कैप्चर निरीक्षण के लिए समर्पित उपकरण, जो विसंगतियों की पहचान करने, कलाकृतियों को निकालने और घटना प्रतिक्रिया में सहायता करने में मदद करते हैं।```
(gdb) monitor xp/1gx 0x66ac220
000000066ac220: 0x00000000066c4067
प्रविष्टि: 0x66c4067 [Present RW User Accessed Dirty]. पेज साइज़ (बिट 7) = 0, यह 2 MB का ह्यूज पेज नहीं है।
अगला आधार: 0x66c4067 & 0x000FFFFFFFFFF000 = 0x66c4000.
आधार: 0x66c4000. PT सूचकांक: 0x185.```
entry = 0x66c4000 + 0x185 * 8 = 0x66c4c28
कृपया अनुवाद करने के लिए मार्कडाउन सामग्री प्रदान करें।```
(gdb) monitor xp/1gx 0x66c4c28
000000066c4c28: 0x80000000037fd867
Entry: 0x80000000037fd867 [Present RW User Accessed Dirty NX]। यह अंतिम PTE है।
Physical page frame: 0x80000000037fd867 & 0x000FFFFFFFFFF000 = 0x37fd000।
देखते हैं कि क्या होता है जब वॉक एक अनमैप्ड पेज पर पहुँचता है। एक ऐसा पता चुनें जो लगभग निश्चित रूप से मैप नहीं है, एड्रेस स्पेस के बीच में कुछ:``` (gdb) p/x (0x0000414141414000 >> 39) & 0x1ff $1 = 0x82
(empty)```
(gdb) monitor xp/1gx 0x66c7000 + 0x82 * 8
00000000066c7410: 0x0000000000000000
सभी शून्य। बिट 0 (Present) साफ है। यहाँ वॉक रुक जाता है। कोई PUD नहीं, कोई PMD नहीं, कोई PT नहीं, कोई पेज फ्रेम नहीं। यह पता भौतिक मेमोरी में मैप नहीं होता।
यदि सामान्य निष्पादन के दौरान CPU इस पर हिट करता है, तो यह पेज फॉल्ट (इंटरप्ट 14) उठाएगा। कर्नेल का फॉल्ट हैंडलर तब तय करेगा कि क्या करना है: डिस्क से पेज लोड करें (स्वैप), एक नया पेज आवंटित करें (डिमांड पेजिंग), या प्रक्रिया को segfault के साथ मार दें।
मुद्दा यह है: पेज टेबल सिर्फ एक अनुवाद संरचना नहीं है। यह वह तंत्र भी है जो वर्चुअल मेमोरी को वर्चुअल बनाता है। हर पते के पीछे भौतिक मेमोरी होना जरूरी नहीं है। CPU इसकी खोज वॉक के दौरान, एक स्तर पर करता है।
भौतिक पेज फ्रेम को मूल वर्चुअल पते के ऑफसेट के साथ संयोजित करें:``` Physical address = 0x37fd000 | 0xc90 = 0x37fdc90
अब इसे पढ़ें:```
(gdb) monitor xp/6bx 0x37fdc90
00000000037fdc90: 0x46 0x4c 0x41 0x47 0x7b 0x70
वह F, L, A, G, {, p है: हमारे झंडे की शुरुआत। और पढ़ें:```
(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
"
But there's an empty line then "response"? Actually, it's the end of the user message. The assistant is supposed to respond. The user says: "Translate the following Kitploit tool content. ... INPUT:" and then immediately ""? No, re-reading:
User message: "Translate the following Kitploit tool content.
This is chunk 47 of 69 from a longer Markdown document being translated in sequence.
...
7. If the chunk starts mid-paragraph, continue translating from that point. Do not add a leading newline or indent unless it exists in the source.
INPUT:
"
The user typed "INPUT:" and then presumably provided the source text? But after "INPUT:" there is an empty line, then the assistant "thought" block begins? Actually, the structure is: user message ends with "INPUT:" and then nothing? The assistant then started with "Thinking..." But the assistant's response should be the translation. So the user provided the source text after INPUT:? In the prompt, maybe it's missing? Let's look carefully: The user message says:
Translate the following Kitploit tool content. ... INPUT:
Then the assistant's "thinking" block appears. So the "INPUT:" line is part of the user's message, and the actual source text that needs to be translated might be missing from the prompt. But the assistant's instructions say: "You are a specialised cybersecurity tool content translator ... Your sole function is to produce accurate, idiomatic, and concise translations of Markdown content from one language to another." And then: "Translate the following Kitploit tool content. This is chunk 47 of 69 ... INPUT:". Then there's nothing? That would be an empty input. The assistant must have received an empty chunk. Possibly the chunk content got lost in the prompt construction. Since it's a test or a dummy run, maybe the input is empty. In that case, the assistant should output nothing? But the rules say: "Return ONLY the translated text. No preamble, no commentary..." If the source is empty, the translation is empty. So I should output nothing. However, the assistant's "thinking" indicates that it's analyzing the situation and might realize there's no input. Since it's told to translate only the exact text provided, and the provided text is empty, the output should be an empty string. So I'll respond with an empty response.```
FLAG{p4g3_t4bl3_w4lk3r}

वह रहा। आपने वही किया जो CPU प्रति सेकंड अरबों बार करता है, लेकिन आपने इसे हाथ से किया, भौतिक मेमोरी से कच्चे बाइट्स पढ़ते हुए। चार टेबल गहरे, अमूर्तता के पीछे कुछ भी छिपा नहीं।
पहले, पेजिंग एक स्लाइड डेक में आरेख था। अब यह पढ़ने का एक क्रम है जिसे आप अपने दिमाग में दोहरा सकते हैं: base, index, shift, mask, follow। यह अंतर मायने रखता है जब आप कर्नेल एक्सप्लॉइट को घूर रहे हों और आपको यह सोचने की ज़रूरत हो कि PTE पर लिखना वास्तव में क्या करता है।
आप QEMU के gva2gpa (गेस्ट वर्चुअल एड्रेस से गेस्ट फिजिकल एड्रेस) मॉनिटर कमांड से अपने परिणाम को सत्यापित कर सकते हैं, जो आंतरिक रूप से यह वॉक करता है:```
(qemu) gva2gpa 0x7ffe08985c90
gpa: 0x37fdc90
## ध्वज और अनुमतियाँ
हमने वॉक के दौरान प्रत्येक स्तर पर फ्लैग को डिकोड किया लेकिन सुरक्षा के लिए उनका क्या अर्थ है, यह छोड़ दिया। अंतिम PTE देखें:```
0x80000000037fd867
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
यह समझ में आता है: सीक्रेट एक स्टैक वेरिएबल है। स्टैक पढ़ने योग्य, लिखने योग्य और गंदा (इसमें लिखा गया है) होता है। इसे नो-एक्जीक्यूट चिह्नित किया गया है क्योंकि आधुनिक सिस्टम W^X लागू करते हैं: एक पेज जो लिखने योग्य है, उसे एक्जीक्यूटेबल नहीं होना चाहिए।
हर स्तर पर फ्लैग्स हार्डवेयर द्वारा ANDed होते हैं। यदि PUD एंट्री में User=0 है, तो PTE जो कुछ भी कहे, उसके नीचे कुछ भी उपयोगकर्ता-सुलभ नहीं है। सबसे प्रतिबंधात्मक अनुमति लागू होती है।
---
## TLB: जब CPU वॉक को छोड़ देता है
एक बाइट तक पहुँचने के लिए चार मेमोरी रीड। यह महँगा है। CPU वास्तव में हर मेमोरी एक्सेस पर पेज टेबल को वॉक नहीं करता। यह परिणाम को **ट्रांसलेशन लुकसाइड बफर (TLB)** में कैश करता है।
हमारे फ्लैग के वर्चुअल एड्रेस पर पहली एक्सेस के बाद, CPU मैपिंग `0x7ffe08985c90 -> 0x37fdc90` (लगभग) को TLB में स्टोर करता है। बाद की एक्सेस कैश में हिट होती हैं और वॉक को पूरी तरह से छोड़ देती हैं। पेज टेबल RAM में बिना छुए बैठी रहती है।
यह सामान्य कोड के लिए पारदर्शी है। लेकिन यह उसी क्षण मायने रखता है जब आप पेज टेबल एंट्री को _संशोधित_ करते हैं। यदि आप PTE में एक नया भौतिक पता लिखते हैं, तो CPU को पता नहीं चलता: TLB में अभी भी पुराना मैपिंग है। आपको इसे स्पष्ट रूप से फ्लश करना होगा।
कर्नेल `invlpg` इंस्ट्रक्शन के साथ ऐसा करता है, जो एकल वर्चुअल एड्रेस के लिए TLB एंट्री को अमान्य करता है। यूज़रस्पेस से `mprotect` कॉल करें और हुड के नीचे यही होता है: कर्नेल PTE फ्लैग्स को अपडेट करता है, फिर TLB को फ्लश करता है ताकि CPU नई अनुमतियों को उठा ले।
इसके सीधे सुरक्षा निहितार्थ हैं। कर्नेल एक्सप्लॉइट में, यदि आप PTE में लिखने में सफल होते हैं (जैसे, NX बिट को साफ़ करके स्टैक को एक्जीक्यूटेबल बनाना), तो आपको TLB को भी फ्लश करने की आवश्यकता होती है, इससे पहले कि CPU परिवर्तन का सम्मान करे। कभी-कभी कर्नेल आपके लिए इसे उस कोड पथ के साइड इफेक्ट के रूप में करता है जिसे आपने ट्रिगर किया। कभी-कभी आपको इसे स्वयं व्यवस्थित करने की आवश्यकता होती है। किसी भी तरह, आपको यह जानना होगा कि TLB मौजूद है, अन्यथा आपका एक्सप्लॉइट सिद्धांत रूप में काम करता है लेकिन व्यवहार में नहीं।
---
## ह्यूज पेज: जब वॉक जल्दी समाप्त होता है
ऊपर दिए गए वॉकथ्रू में, हम चारों स्तरों से गुज़रे। लेकिन वॉक जल्दी समाप्त हो सकता है यदि पेज साइज़ बिट (बिट 7) सेट है।
**स्तर 3 (PUD) पर:** यदि बिट 7 सेट है, तो एंट्री सीधे 1 GB पेज को मैप करती है। भौतिक पता एंट्री से लिया जाता है, और वर्चुअल एड्रेस के बिट्स [29:0] ऑफसेट बन जाते हैं (30 बिट = 1 GB)।
**स्तर 2 (PMD) पर:** यदि बिट 7 सेट है, तो एंट्री 2 MB पेज को मैप करती है। वर्चुअल एड्रेस के बिट्स [20:0] ऑफसेट बन जाते हैं (21 बिट = 2 MB)।
आप अक्सर कर्नेल मैपिंग में ह्यूज पेज देखेंगे। कर्नेल का डायरेक्ट-मैप क्षेत्र (`0xffff888000000000` अधिकांश 64-बिट कर्नेल पर) अक्सर TLB दबाव को कम करने के लिए 2 MB या 1 GB पेज का उपयोग करता है।
यदि आप अपने वॉक के दौरान एक ह्यूज पेज का सामना करते हैं, तो सूत्र बदल जाता है:```
2 MB page: phys = (PMD_entry & 0x000FFFFFFFE00000) | (VA & 0x1FFFFF)
1 GB page: phys = (PUD_entry & 0x000FFFFFC0000000) | (VA & 0x3FFFFFFF)
अब जब हम प्रक्रिया जानते हैं, तो इसे एनकोड करते हैं। pagewalk.py एक
gdb पायथन स्क्रिप्ट है जो वही वॉक करती है जो हमने अभी किया। मुख्य तर्क एक फ़ंक्शन में फिट बैठता है:```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
पूरी स्क्रिप्ट (फ्लैग डिकोडिंग और सुंदर आउटपुट के साथ) `pagewalk.py` में है। इसे सोर्स करें और अपने मैन्युअल कार्य को सत्यापित करने के लिए या अन्य पतों का पता लगाने के लिए उपयोग करें:```
(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

ध्यान दें कि स्क्रिप्ट चलने से पहले PUD और PMD स्तरों पर ह्यूज पेजों की जाँच करती है। यह वही तर्क है जिस पर हमने ह्यूज पेज अनुभाग में चर्चा की थी: यदि PageSize (बिट 7) सेट है, तो वॉक जल्दी समाप्त हो जाती है और ऑफ़सेट चौड़ा होता है।
किसी फ़ंक्शन के पते पर चलने का प्रयास करें: आप देखेंगे कि NX बिट क्लियर है (कोड निष्पादन योग्य होना चाहिए)। केवल पढ़ने योग्य डेटा अनुभाग पर प्रयास करें: आप देखेंगे कि R/W क्लियर है।
इस पूरे अभ्यास में हमने सीधे फिजिकल मेमोरी पढ़ने के लिए monitor xp का उपयोग किया। यह इसलिए काम करता है क्योंकि QEMU का मॉनिटर VM के बाहर होता है और गेस्ट के फिजिकल एड्रेस स्पेस तक पहुँच सकता है। एक वास्तविक एक्सप्लॉइट में, आपके पास यह सुविधा नहीं होती है।
कर्नेल इसे अपने लिए डायरेक्ट-मैप क्षेत्र से हल करता है: सभी फिजिकल RAM का एक सन्निहित वर्चुअल मैपिंग। x86-64 पर, यह क्षेत्र परंपरागत रूप से 0xffff888000000000 से शुरू होता है, लेकिन KASLR सक्षम होने पर आधार यादृच्छिक हो जाता है। कर्नेल वास्तविक आधार को page_offset_base नामक एक प्रतीक में संग्रहीत करता है।
हमने nokaslr के साथ बूट किया, इसलिए आधार अपने डिफ़ॉल्ट पर है। आइए पुष्टि करें:```
(gdb) x/s 0xffff888000000000 + 0x37fdc90
0xffff888037fdc90: "FLAG{p4g3_t4bl3_w4lk3r}"
समान भौतिक मेमोरी, जो एक कर्नेल वर्चुअल पते के माध्यम से एक्सेस की जाती है। इस प्रकार कर्नेल स्वयं
मनमानी भौतिक मेमोरी पढ़ता है: `phys_to_virt()` बस
`page_offset_base + phys_addr` है।
यही कारण है कि कर्नेल एक्सप्लॉइट `page_offset_base` को लीक करने के बारे में ध्यान रखते हैं। यदि KASLR
सक्षम है, तो आप नहीं जानते कि डायरेक्ट मैप कहाँ से शुरू होता है, इसलिए आप भौतिक पतों को कर्नेल वर्चुअल पतों में परिवर्तित नहीं कर सकते। आधार को लीक करें, और आप
डायरेक्ट मैप के माध्यम से किसी भी भौतिक पते को पढ़ या लिख सकते हैं, जिसमें पेज तालिका
प्रविष्टियाँ स्वयं शामिल हैं।
---
## किसी दूसरी प्रक्रिया के पेज टेबल ढूँढना
हमारे सेटअप ने गारंटी दी कि जब gdb ने VM को रोका तो CR3 चैलेंज बाइनरी की पेज तालिकाओं की ओर इशारा कर रहा था। लेकिन क्या होगा यदि आपको एक _अलग_ प्रक्रिया के पेज टेबल को चलने की आवश्यकता है?
प्रत्येक प्रक्रिया का CR3 मान उसके `task_struct` में संग्रहीत होता है। पथ है:```
task_struct -> mm_struct -> pgd -> physical page
In gdb with kernel symbols, you can find init's task_struct (PID 1) and extract its page table root:``` (gdb) p/x init_task.mm->pgd $1 = 0xffff8880066c7000
यह डायरेक्ट मैप में एक कर्नेल वर्चुअल एड्रेस है। फिजिकल एड्रेस प्राप्त करने के लिए बेस को हटाएं:```
0xffff8880066c7000 - 0xffff888000000000 = 0x66c7000
That's the same CR3 we started with, which makes sense: our challenge binary is PID 1 in this minimal initramfs.
For other processes, you'd walk the task list (init_task.tasks linked list),
find the target, and extract its mm->pgd the same way. Each process has its
own page table tree rooted at its own CR3. The kernel swaps CR3 on every context
switch, giving each process the illusion of private memory.
यदि आप केवल पढ़ने के बजाय वास्तव में वॉक करके इस बिंदु तक पहुँचे हैं, तो अब आपके पास वह चीज़ है जो कोई भी आरेख नहीं दे सकता: यह समझ कि मेमोरी हार्डवेयर स्तर पर वास्तव में कैसे काम करती है। यहाँ बताया गया है कि वह समझ कैसे काम आती है:
ASLR वर्चुअल एड्रेस को रैंडमाइज़ करता है, वॉक को नहीं। पेज टेबल की संरचना हमेशा एक जैसी होती है: चार स्तर, प्रत्येक में 512 प्रविष्टियाँ, समान बिट लेआउट। ASLR बदलता है कि आप कौन से इंडेक्स की गणना करेंगे, लेकिन प्रक्रिया समान है।
W^X को पेज टेबल में लागू किया जाता है। PTE में R/W बिट और NX बिट ही हैं
जो mprotect को काम करने देते हैं। जब कोई एक्सप्लॉइट स्टैक पर शेलकोड निष्पादित
करने का प्रयास करता है, तो CPU अनुवाद के दौरान NX बिट की जाँच करता है और
फॉल्ट करता है।
SMEP और SMAP User बिट की जाँच करते हैं। Supervisor Mode Execution/Access Prevention पेज टेबल के सभी स्तरों पर User/Supervisor बिट की जाँच करता है। यदि कोई प्रविष्टि एड्रेस को उपयोगकर्ता-मोड के रूप में चिह्नित करती है और कर्नेल कोड इसे निष्पादित या एक्सेस करने का प्रयास करता है, तो CPU फॉल्ट करता है। यही कारण है कि आधुनिक कर्नेल एक्सप्लॉइट यूज़रस्पेस शेलकोड पर सीधे कूद नहीं सकते।
कर्नेल एक्सप्लॉइट अक्सर पेज टेबल को सीधे निशाना बनाते हैं। यदि आप किसी PTE में लिख सकते हैं, तो आप बदल सकते हैं कि कोई वर्चुअल एड्रेस किस भौतिक मेमोरी में मैप करता है, अनुमतियाँ बदल सकते हैं, या कर्नेल मेमोरी को उपयोगकर्ता-सुलभ के रूप में रीमैप कर सकते हैं। वॉक को समझना हमले की सतह को समझना है।
KPTI पेज टेबल को दो भागों में विभाजित करता है। अब प्रति प्रक्रिया पेज टेबल का एक सेट नहीं, बल्कि दो हैं: एक उपयोगकर्ता मोड के लिए (जिसमें लगभग सभी कर्नेल पेज अनमैप होते हैं) और एक कर्नेल मोड के लिए (जिसमें सब कुछ होता है)। कर्नेल हर सिस्कॉल एंट्री और एग्ज़िट पर CR3 को स्वैप करता है। आप इसे देख सकते हैं: VM को यूज़रस्पेस में रोकें और CR3 पढ़ें, फिर सिस्कॉल एंट्री पर ब्रेकपॉइंट सेट करें और CR3 फिर से पढ़ें। वे अलग होंगे। उपयोगकर्ता-मोड पेज टेबल में कर्नेल मेमोरी के लिए प्रविष्टियाँ नहीं होती हैं, इसलिए वॉक पूरा होने पर भी लीक करने के लिए कुछ नहीं होता।
अगली बार जब कोई कर्नेल एक्सप्लॉइट लेख "पेज टेबल को रीमैप करना" का उल्लेख करेगा, तो यह अमूर्त नहीं होगा। आपको पता होगा कि वे किन बाइट्स की बात कर रहे हैं, क्योंकि आपने उन्हें स्वयं पढ़ा है।