
تجول في جداول الصفحات x86-64 يدويًا في qemu و gdb. حلل عنوانًا افتراضيًا، اتبع سجل cr3 عبر جميع مستويات الذاكرة الفعلية، واستخرج علمًا من البايتات الخام.
لقد قرأت عن التقسيم الصفحي. المخططات منطقية. أربعة مستويات، 9 بتات لكل منها، إطار الصفحة، الإزاحة. بالتأكيد. لكن بعد ذلك تواجه تحديًا يتطلب فعليًا التنقل في جداول الصفحات، وتدرك أنك لا تعرفها حقًا. أنت تعرف عنها. فرق كبير.
ما نجح معي هو الجلوس أمام QEMU و gdb والقيام بالتنقل بنفسي: حساب كل مؤشر، قراءة كل إدخال من الذاكرة الفعلية، تتبع كل مؤشر يدويًا. ظهر واحد من ذلك يمكن أن يعلم أكثر من ساعات من المحاضرات.
هذه مجموعة من ملاحظاتي من تلك العملية. إذا كنت لا تزال تفتقر إلى الجانب المفاهيمي، شاهد محاضرة Zardus حول إدارة ذاكرة النواة أولاً. تلك هي النظرية. هذا هو المختبر.
الهدف: أخذ عنوان افتراضي وتعقبه عبر الذاكرة الفعلية الخام حتى نجد البيانات. لا مساعدات النواة. لا تجريدات. مجرد آلة افتراضية QEMU، gdb، وذاكرة فعلية خام.
بحلول النهاية، لن يكون التقسيم الصفحي شيئًا تقرأ عنه، بل سيكون شيئًا تعرفه لأنك قمت به يدويًا.
تم تضمين نواة مبنية مسبقًا و initramfs، قمت بتشغيل هذا في فيدورا، لكن أي نظام تشغيل يدير QEMU و gdb يجب أن يعمل. قم بتثبيتها باستخدام مدير الحزم الخاص بك:```
sudo apt install qemu-system-x86 gdb
sudo dnf install qemu-system-x86 gdb
brew install qemu gdb
### The challenge binary
الهدف هو برنامج بسيط بلغة C يخزن علمًا (flag) في الذاكرة ويطبع عنوانه الافتراضي:```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)
{
}
}
الحلقة المزدحمة (busy loop) مقصودة. لقد استخدمت في الأصل pause()، لكن ذلك يضع العملية في حالة سكون في استدعاء نظام (syscall): عندما يوقف gdb الجهاز الافتراضي (VM)، فمن المحتمل أن وحدة المعالجة المركزية (CPU) تقوم بتشغيل مهمة الخمول (idle task) مع CR3 مختلف. الحلقة الدورانية (spinning loop) تبقي العملية على المعالج، لذا التوقف يضمن أنك في سياقها مع جداول الصفحات (page tables) الصحيحة.
تم تضمين initramfs مسبقة البناء مع هذا الثنائي بالفعل في initramfs.cpio.gz. إذا كنت بحاجة إلى إعادة بنائها (لنظام Linux فقط، يتطلب busybox و glibc-static)، قم بتشغيل make في هذا الدليل.
./start.sh
يقوم البرنامج النصي بتشغيل النواة المجمعة و initramfs تحت QEMU مع `-s`
(خادم gdb على `localhost:1234`) و `nokaslr` بحيث تظل عناوين النواة
ثابتة بين عمليات التشغيل.
يتم تشغيل الجهاز الافتراضي فورًا ويتم تشغيل binary التحدي. سترى عنوان
العلم الافتراضي مطبوعًا على وحدة التحكم.```
secret @ 0x7ffe08985c90
pid = 1
Spinning. Walk the page tables to find the flag.
اكتب ذلك العنوان الافتراضي. هذا هو هدفك.

مفتاح الهروب الافتراضي لـ QEMU هو
Ctrl-a، لكنه يتعارض مع بادئة tmux الخاصة بي، لذلك يستخدم السكريبت-echr 0x11لإعادة تعيينه إلىCtrl-q. إذا كنت تستخدمCtrl-qلشيء آخر، فغيّر القيمة السداسية العشرية فيstart.shلتناسب إعداداتك.
في طرفية ثانية:``` gdb -ex "target remote :1234"

---
## تفكيك العنوان الافتراضي
لديك عنوان افتراضي. لكن أين البيانات، _حقًا_؟
العناوين الافتراضية هي الخيال المهذب لنظام التشغيل. كل عملية تعتقد أن لديها ذاكرة خاصة بها تبدأ من الصفر. في الواقع، تعيش البيانات في موقع غير مرتبط تمامًا في ذاكرة الوصول العشوائي الفعلية. جدول الصفحات هو الخريطة بينهما: بنية شجرية يقوم المعالج المركزي باجتيازها في كل وصول للذاكرة (أو يبحث عنها من ذاكرة التخزين المؤقت TLB الخاصة به).
لذا دعنا نفعل ما يفعله المعالج المركزي. يدويًا. لترجمة ذلك العنوان، نحتاج إلى تفكيكه إلى المؤشرات التي يستخدمها المعالج المركزي في كل مستوى.
العنوان الافتراضي في 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 كيلوبايت (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هو مجرد مثال. استخدم أي عنوان يطبعه برنامج التحدي الخاص بك.
ملاحظة حول التقسيم بخمس مستويات. تدعم المعالجات والنواة الحديثة LA57، الذي يضيف مستوى خامسًا (PML5) فوق PGD ويمدد العناوين الافتراضية إلى 57 بتًا. المسار هو نفس النمط: مؤشر 9 بتات إضافي، بحث إضافي في جدول واحد. معظم الأنظمة لا تزال تستخدم التقسيم بأربعة مستويات. يمكنك التحقق من نظامك:
cat /proc/cpuinfo | grep la57. كل شيء في هذه المقالة يفترض التقسيم بأربعة مستويات.
كل شجرة لها جذر. بالنسبة لجداول الصفحات، هذا الجذر موجود في المسجل CR3: فهو يحمل العنوان المادي للجدول الأعلى مستوى، وهو PGD. كل عملية تحصل على قيمة CR3 خاصة بها، وتقوم النواة بتبديلها عند تبديل السياق.
هذه هي نقطة دخولنا إلى المسار. اقرأها من gdb:``` (gdb) info registers cr3 cr3 0x66c7000 [ PDBR=26311 PCID=0 ]
قاعدة جدول الصفحات هي `0x66c7000`. البتات الـ 12 السفلية هي PCID/أعلام (صفر هنا)، لذا فإن العنوان الأساسي هو القيمة كما هي.
هنا يبدأ التجوال.
---
## التجوال
إليك الحيلة: كل مستوى يتبع نفس النمط. تختلف الأعلام قليلاً بين المستويات، لكن العملية لا تتغير. النمط:
1. **حساب عنوان الإدخال:** `base + index * 8` (كل إدخال هو 8 بايت)
2. **قراءة الإدخال من الذاكرة الفعلية** باستخدام أمر `xp` في مراقب QEMU
3. **فك تشفير الأعلام** (انظر المرجع أدناه). إذا كانت بتة الوجود (bit 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 يتم تجاهلها بواسطة العتاد وهي متاحة لنظام التشغيل. يستخدمها لينكس لأغراض حفظ الدفاتر (تتبع الأوساخ الناعمة، على سبيل المثال). البتات 52-62 محجوزة. ستواجه كليهما عند قراءة PTEs في كتابات الاستغلال.
احتفظ بجدول الأعلام هذا في متناول يدك أثناء السير.
لنبدأ.
لدينا قاعدة PGD من CR3: 0x66c7000.
مؤشر PGD الخاص بنا هو 0xff.
احسب عنوان الإدخال:``` entry = 0x66c7000 + 0xff * 8 = 0x66c77f8
اقرأها من gdb باستخدام أمر فحص الذاكرة الفعلية لـ QEMU:```
(gdb) monitor xp/1gx 0x66c77f8
000000066c77f8: 0x0000000006713067
المدخل: 0x6713067 [Present RW User Accessed Dirty].
القاعدة التالية: 0x6713067 & 0x000FFFFFFFFFF000 = 0x6713000.