
CVE-2023-3269: ثغرة تصعيد صلاحيات في نواة لينكس
(استغلال مُتحقَّق منه عبر GitHub CI)
تم العثور على خلل في معالجة توسعة المكدس في نواة لينكس من الإصدار 6.1 حتى 6.4، ويُعرف باسم "Stack Rot". يمكن لشجرة مابل (maple tree)، المسؤولة عن إدارة مناطق الذاكرة الافتراضية، أن تخضع لاستبدال العقد دون الحصول بشكل صحيح على قفل الكتابة MM، مما يؤدي إلى مشاكل use-after-free. يمكن لمستخدم محلي غير مميز استغلال هذا الخلل لاختراق النواة وتصعيد امتيازاته.
بما أن StackRot ثغرة في نواة لينكس توجد في النظام الفرعي لإدارة الذاكرة، فهي تؤثر على جميع تكوينات النواة تقريبًا وتتطلب حدًا أدنى من الصلاحيات لتفعيلها. ومع ذلك، تجدر الإشارة إلى أن عقد مابل يتم تحريرها باستخدام استدعاءات RCU، مما يؤخر التحرير الفعلي للذاكرة حتى ما بعد فترة سماح RCU. وبالتالي، يُعد استغلال هذه الثغرة أمرًا صعبًا.
على حد علمي، لا توجد حاليًا أي exploits متاحة للعموم تستهدف ثغرات use-after-free-by-RCU (UAFBR). تمثل هذه المرة الأولى التي يُثبت فيها أن ثغرات UAFBR قابلة للاستغلال، حتى دون وجود إعدادات CONFIG_PREEMPT أو CONFIG_SLAB_MERGE_DEFAULT. ومن الجدير بالذكر أنه تم بنجاح توضيح هذا الاستغلال في البيئة التي يوفرها Google kCTF VRP (bzImage_upstream_6.1.25, config).
كانت ثغرة StackRot موجودة في نواة لينكس منذ الإصدار 6.1 عندما تم تغيير بنية شجرة VMA من الأشجار الحمراء-السوداء إلى أشجار مابل.
عندما يتم استخدام استدعاء النظام mmap() لإنشاء تعيين ذاكرة، تقوم النواة بإنشاء بنية تسمى vm_area_struct لتمثيل منطقة الذاكرة الافتراضية المقابلة (VMA). تخزن هذه البنية معلومات متنوعة بما في ذلك العلامات والخصائص والتفاصيل الأخرى ذات الصلة بالتعيين.```c
struct vm_area_struct {
long unsigned int vm_start; /* 0 8 /
long unsigned int vm_end; / 8 8 /
struct mm_struct * vm_mm; / 16 8 /
pgprot_t vm_page_prot; / 24 8 /
long unsigned int vm_flags; / 32 8 /
union {
struct {
struct rb_node rb attribute((aligned(8))); / 40 24 /
/ --- cacheline 1 boundary (64 bytes) --- /
long unsigned int rb_subtree_last; / 64 8 /
} attribute((aligned(8))) shared attribute((aligned(8))); / 40 32 /
struct anon_vma_name * anon_name; / 40 8 /
} attribute((aligned(8))); / 40 32 /
/ --- cacheline 1 boundary (64 bytes) was 8 bytes ago --- /
struct list_head anon_vma_chain; / 72 16 /
struct anon_vma * anon_vma; / 88 8 /
const struct vm_operations_struct * vm_ops; / 96 8 /
long unsigned int vm_pgoff; / 104 8 /
struct file * vm_file; / 112 8 /
void * vm_private_data; / 120 8 /
/ --- cacheline 2 boundary (128 bytes) --- /
atomic_long_t swap_readahead_info; / 128 8 /
struct vm_userfaultfd_ctx vm_userfaultfd_ctx; / 136 0 */
/* size: 136, cachelines: 3, members: 14 */
/* forced alignments: 1 */
/* last cacheline: 8 bytes */
} attribute((aligned(8)));
بعد ذلك، عندما يواجه النواة أخطاء الصفحات أو استدعاءات نظام أخرى
متعلقة بالذاكرة، فإنها تحتاج إلى بحث سريع عن VMA بناءً على العنوان فقط.
سابقًا، كانت تُدار مناطق VMA باستخدام الأشجار الحمراء-السوداء. ومع ذلك،
ابتداءً من إصدار نواة لينكس 6.1، تم الانتقال إلى أشجار القيقب. [أشجار
القيقب][mt] هي هياكل بيانات من نوع B-tree آمنة بالنسبة إلى RCU،
ومحسّنة لتخزين النطاقات غير المتداخلة. ومع ذلك، فإن طبيعتها المعقدة تضيف
تعقيدًا إلى قاعدة الشيفرة وتُدخل ثغرة StackRot.
[mt]: https://docs.kernel.org/6.4/core-api/maple_tree.html
في جوهرها، تتكون شجرة القيقب من عُقد قيقب. وعلى الرغم من أن بنية الشجرة
قد تكون معقدة، فمن المهم ملاحظة أن هذا التعقيد لا علاقة له
بخلل StackRot. لذلك، في جميع أنحاء هذا المقال، يُفترض أن
شجرة القيقب تتكون من عقدة واحدة فقط، أي العقدة الجذرية.
يمكن أن تحتوي هذه العقدة الجذرية على ما يصل إلى 16 فترة. وقد تمثل هذه
الفترات إما فجوة أو تشير إلى VMA. وبما أن الفجوات تُعد أيضًا فترات، فإن
جميع الفترات تكون متصلة تسلسليًا، مما يؤدي إلى الحاجة إلى 15
نقطة نهاية فقط، تُعرف أيضًا باسم المحاور، داخل بنية العقدة.
لاحظ أن نقطة النهاية في أقصى اليسار ونقطة النهاية في أقصى اليمين محذوفتان،
لأنه يمكن استرجاعهما من العقدة الأم.```c
struct maple_range_64 {
struct maple_pnode * parent; /* 0 8 */
long unsigned int pivot[15]; /* 8 120 */
/* --- cacheline 2 boundary (128 bytes) --- */
union {
void * slot[16]; /* 128 128 */
struct {
void * pad[15]; /* 128 120 */
/* --- cacheline 3 boundary (192 bytes) was 56 bytes ago --- */
struct maple_metadata meta; /* 248 2 */
}; /* 128 128 */
}; /* 128 128 */
/* size: 256, cachelines: 4, members: 3 */
};
إن بنية maple_range_64، كما هو موضح أعلاه، تمثل عقدة maple. بالإضافة إلى نقاط الارتكاز، تُستخدم الخانات للإشارة إلى بنية VMA عندما تعمل العقدة كعقدة طرفية، أو إلى عقد maple أخرى عندما تعمل العقدة كعقدة داخلية. إذا كان الفاصل الزمني يقابل فجوة، فستحتوي الخانة ببساطة على قيمة NULL. ويمكن تصور ترتيب نقاط الارتكاز والخانات كما هو موضح أدناه:```
Slots -> | 0 | 1 | 2 | ... | 12 | 13 | 14 | 15 |
┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │ │ │ └─ Implied maximum
│ │ │ │ │ │ │ └─ Pivot 14
│ │ │ │ │ │ └─ Pivot 13
│ │ │ │ │ └─ Pivot 12
│ │ │ │ └─ Pivot 11
│ │ │ └─ Pivot 2
│ │ └─ Pivot 1
│ └─ Pivot 0
└─ Implied minimum
فيما يتعلق بالتعديل المتزامن، تفرض شجرة القيقب قيدًا محددًا، وهو أنه يجب الاحتفاظ بقفل حصري من قِبل الكتّاب (*Rule W*). في حالة شجرة VMA، يتوافق القفل الحصري مع قفل الكتابة MM. أما بالنسبة للقرّاء، فهناك خياران متاحان. يتضمن الخيار الأول الاحتفاظ بقفل القراءة MM (*Rule A1*)، مما يؤدي إلى حظر الكاتب بواسطة قفل القراءة-الكتابة MM. بدلاً من ذلك، الخيار الثاني هو الدخول في القسم الحرج الخاص بـ RCU (*Rule A2*). وبذلك، لا يتم حظر الكاتب، ويمكن للقرّاء مواصلة عملياتهم نظرًا لأن شجرة القيقب آمنة مع RCU. بينما تختار معظم عمليات الوصول إلى VMA الحالية الخيار الأول (أي Rule A1)، يُستخدم Rule A2 في سيناريوهات قليلة حرجة من حيث الأداء، مثل أخطاء الصفحات بدون أقفال.
ومع ذلك، هناك جانب إضافي يتطلب اهتمامًا خاصًا، ويتعلق بتوسع المكدس. يمثل المكدس منطقة ذاكرة يتم تعيينها بعلامة MAP_GROWSDOWN، مما يشير إلى التوسع التلقائي عند الوصول إلى عنوان أسفل المنطقة. في مثل هذه الحالات، يتم تعديل عنوان البداية لـ VMA المقابل، وكذلك المدى المرتبط داخل شجرة القيقب. ومن الجدير بالذكر أن هذه التعديلات تتم دون الاحتفاظ بقفل الكتابة MM.```c
static inline
void do_user_addr_fault(struct pt_regs *regs,
unsigned long error_code,
unsigned long address)
{
// ...
if (unlikely(!mmap_read_trylock(mm))) {
// ...
}
// ...
if (unlikely(expand_stack(vma, address))) {
// ...
}
// ...
}
عادةً، توجد فجوة بين VMA المكدس وVMA المجاور له، حيث تفرض النواة حارسًا للمكدس. في هذا السيناريو، عند توسيع المكدس، فقط قيمة pivot في عقدة maple تحتاج إلى التحديث، وهي عملية يمكن تنفيذها بشكل ذري. ومع ذلك، إذا كان VMA المجاور يمتلك أيضًا علامة MAP_GROWSDOWN، فلن يتم فرض حارس المكدس.```c int expand_downwards(struct vm_area_struct *vma, unsigned long address) { // ...
if (prev) {
if (!(prev->vm_flags & VM_GROWSDOWN) &&
vma_is_accessible(prev) &&
(address - prev->vm_end < stack_guard_gap))
return -ENOMEM;
}
// ...
}
نتيجة لذلك، يمكن لتوسّع المكدس إزالة الفجوة. في مثل هذه الحالات، يجب إزالة فترة الفجوة داخل عقدة maple. نظرًا لأن شجرة maple آمنة مع RCU، فإن الكتابة فوق العقدة في مكانها غير ممكنة. بدلاً من ذلك، يتم إنشاء عقدة جديدة، مما يؤدي إلى استبدال العقدة، ثم تُدمَّر العقدة القديمة لاحقًا باستخدام استدعاء RCU callback.```c
static inline void mas_wr_modify(struct ma_wr_state *wr_mas)
{
// ...
if ((wr_mas->offset_end - mas->offset <= 1) &&
mas_wr_slot_store(wr_mas)) // <-- in-place update
return;
else if (mas_wr_node_store(wr_mas)) // <-- node replacement
return;
// ...
}
يتم استدعاء رد نداء RCU فقط بعد انتهاء جميع أقسام RCU الحرجة الموجودة مسبقًا لكن المشكلة تظهر عند الوصول إلى VMAs، حيث يُحتجز قفل القراءة الخاص بـ MM فقط، ولا يدخل القسم الحرج لـ RCU (وفقًا للقاعدة A1). وبالتالي، نظريًا، يمكن استدعاء رد النداء في أي وقت، مما يؤدي إلى تحرير عقدة القيقب القديمة. ومع ذلك، قد تكون المؤشرات إلى العقدة القديمة قد جُلبَت بالفعل، مما يؤدي إلى خطأ use-after-free عند محاولة الوصول إليها لاحقًا.
يظهر تتبع الاستدعاءات الذي يحدث فيه خطأ use-after-free (UAF) أدناه:```
mm_read_lock() mm_read_lock() expand_stack() find_vma_prev() expand_downwards() mas_walk() mas_store_prealloc() mas_state_walk() mas_wr_story_entry() mas_start() mas_wr_modify() mas_root() mas_wr_node_store() node = rcu_dereference_check() mas_replace() [ The node pointer is recorded ] mas_free() ma_free_rcu() call_rcu(&mt_free_rcu) [ The node is dead ] mm_read_unlock()
[ Wait for the next RCU grace period.. ] rcu_do_batch() mas_prev() mt_free_rcu() mas_prev_entry() kmem_cache_free() mas_prev_nentry() [ The node is freed ] mas_slot() mt_slot() rcu_dereference_check(node->..) [ UAF occurs here ] mm_read_unlock()
## الإصلاح
أبلغت فريق أمن نواة لينكس بهذه الثغرة في 15 يونيو.
بعد ذلك، قاد لينوس تورفالدس عملية معالجة هذا الخطأ.
ونظراً لتعقيده، استغرق تطوير مجموعة تصحيحات حظيت
بإجماع ما يقرب من أسبوعين.
في 28 يونيو، خلال نافذة الدمج لنواة لينكس 6.5، تم دمج الإصلاح
في شجرة لينوس. وقدّم لينوس [رسالة دمج شاملة][fix] لتوضيح
سلسلة التصحيحات من منظور تقني.
[fix]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9471f1f2f50282b9e8f59198ec6bb738b4ccc009
تم لاحقاً نقل هذه التصحيحات إلى النواة المستقرة ([6.1.37][6.1]،
[6.3.11][6.3]، و[6.4.1][6.4])، مما أدى إلى حل خطأ "Stack Rot" بشكل فعلي
في 1 يوليو.
[6.1]: https://lore.kernel.org/stable/2023070133-create-stainless-9a8c@gregkh/T/
[6.3]: https://lore.kernel.org/stable/2023070146-endearing-bounding-d21a@gregkh/T/
[6.4]: https://lore.kernel.org/stable/2023070140-eldercare-landlord-133c@gregkh/T/
## الاستغلال
يركز الاستغلال بشكل أساسي على تحدي Google kCTF، وتحديداً عندما
لا يتم تعيين CONFIG_PREEMPT ولا CONFIG_SLAB_MERGE_DEFAULT. لاستغلال
StackRot، فإن المهمة الأكثر أهمية هي تحديد موقع تكرار VMA يستوفي
المعايير التالية:
1. يمكن التحكم في توقيت التكرار. يتيح لنا هذا التحكم ضمان
انتهاء فترة سماح RCU أثناء تكرار VMA.
2. يسترجع التكرار معلومات محددة من بنية VMA، ويعيد
المعلومات إلى مساحة المستخدم. تمكننا هذه الميزة من
استغلال ثغرة UAF في عقدة maple لتسريب بعض عناوين
النواة.
3. يستدعي التكرار مؤشرات دوال معينة في بنية VMA. تسمح لنا
هذه الإمكانية تحديداً باستغلال ثغرة UAF في عقدة maple
للتحكم في عداد البرنامج (PC) في وضع النواة.
التكرار المختار لـ VMA هو التكرار المسؤول عن توليد محتويات
`/proc/[pid]/maps`. ستوضح الأقسام التالية كيف يستوفي هذا
التكرار المعايير المذكورة أعلاه.
### الخطوة 0: من UAFBR إلى UAF
أثناء أي تكرار لـ VMA، يتم الحصول على مرجع إلى العقدة الجذرية لشجرة VMA،
ويتقدم التكرار عبر فتحاتها. وبالتالي، من خلال تحفيز توسع المكدس في خيط آخر
على وحدة معالجة مركزية منفصلة أثناء تكرار VMA، يمكن بدء استبدال العقدة
بشكل متزامن. في هذه المرحلة، يُعتبر الوصول إلى العقدة القديمة حالة
استخدام بعد التحرير عبر RCU (UAFBR). ومع ذلك، تظهر المشكلات الفعلية فقط
عندما يتم تحرير العقدة القديمة فعلياً، وهو ما يحدث في استدعاء RCU.
يمثل هذا تحديين: (i) تحديد متى يتم تحرير العقدة القديمة،
و(ii) ضمان عدم اكتمال تكرار VMA قبل تحرير العقدة القديمة.
السؤال الأول بسيط نسبياً. في النواة، يمكن استخدام دالة
`synchronize_rcu()` للانتظار حتى تنتهي فترة سماح RCU، مما يضمن
استدعاء جميع استدعاءات RCU السابقة. في مساحة المستخدم، يمكن استخدام
استدعاءات النظام التي تستدعي في النهاية `synchronize_rcu()` لنفس الغرض.
وبالتالي، عندما تنتهي هذه الاستدعاءات، يُعرف أنه تم تحرير العقدة القديمة.
من الجدير بالذكر أن هناك استدعاء نظام، `membarrier(MEMBARRIER_CMD_GLOBAL, 0, -1)`،
يستدعي `synchronize_rcu()` فقط.```c
SYSCALL_DEFINE3(membarrier, int, cmd, unsigned int, flags, int, cpu_id)
{
// ...
switch (cmd) {
// ...
case MEMBARRIER_CMD_GLOBAL:
/* MEMBARRIER_CMD_GLOBAL is not compatible with nohz_full. */
if (tick_nohz_full_enabled())
return -EINVAL;
if (num_online_cpus() > 1)
synchronize_rcu();
return 0;
// ...
}
}
السؤال الثاني يتطلب مزيدًا من التأمل. هناك عدة حلول محتملة كما يلي:
jiffies_till_first_fqs (وقيمتها الافتراضية عدة jiffies)، فسيتم إرسال
مقاطعة بين المعالجات (IPI) إلى وحدة المعالجة المركزية الضحية وتشغيل
استباق طوعي. في حالة تكرار VMA، يمكن للاستباق الطوعي
أن يجعل فترة سماح RCU تنتهي ويحرر عقدة maple،
مما يحول UAFBR بشكل فعال إلى سيناريو استخدام بعد التحرير (UAF) حقيقي.إحدى الملاحظات المهمة هي أنه أثناء تكرار VMA لـ
/proc/[pid]/maps، يتم توليد مسار الملف الكامل لمناطق الذاكرة
المعيّنة من الملفات. على الرغم من أن اسم الدليل مقيد عادةً بحد أقصى
255 حرفًا، إلا أنه لا يوجد حد لعمق الدليل. وهذا يعني أنه
من خلال إنشاء ملف بعمق دليل كبير للغاية وإنشاء
تعيين ذاكرة لهذا الملف، يمكن أن يستغرق الوصول إلى /proc/[pid]/maps
وقتًا طويلاً أثناء تكرار VMA. وبالتالي، فإن هذه
المدة الممتدة تتيح إمكانية إنهاء فترة سماح RCU
والحصول على بدائية UAF.```c
static void
show_map_vma(struct seq_file *m, struct vm_area_struct *vma)
{
// ...
/*
* Print the dentry name for named mappings, and a
* special [heap] marker for the heap:
*/
if (file) {
seq_pad(m, ' ');
/*
* If user named this anon shared memory via
* prctl(PR_SET_VMA ..., use the provided name.
*/
if (anon_name)
seq_printf(m, "[anon_shmem:%s]", anon_name->name);
else
seq_file_path(m, file, "\n");
goto done;
}
// ...
}
هذه الخطوة موضّحة في الشكل التالي:

### الخطوة 1: من UAF في slab إلى UAF في page
الآن بعد أن أصبح UAF يعمل داخل slab. إذا كان `CONFIG_SLAB_MERGE_DEFAULT` مفعّلًا وتم دمج slab الخاص بعقد maple مع `kmalloc-256`، يمكن التحكم في المحتويات داخل العقدة القديمة عن طريق تخصيص بنية جديدة من `kmalloc-256` وملئها ببيانات من مساحة المستخدم. ومع ذلك، إذا لم يكن `CONFIG_SLAB_MERGE_DEFAULT` مضبوطًا، فستكون هناك حاجة إلى نهج بديل. في هذه الحالة، يجب إعادة صفحة العقدة المحرَّرة إلى مخصّص الصفحات، مما يسمح بالتحكم في العقدة القديمة عن طريق تخصيص صفحة جديدة وملئها وفقًا لذلك.
تذكّر أن شجرة VMA ستحتوي على عقدة واحدة فقط. ومن ثم، باستخدام `fork()`/`clone()`، يتم توليد أشجار VMA متعددة وعدد مساوٍ من عقد maple. بافتراض أن slab واحد يشمل M من عقد maple، ويتم الاحتفاظ بعقدة واحدة من كل M من العقد بينما يتم تحرير جميع العقد الأخرى عبر `exit()`، تصبح العقد المتبقية هي العقد الوحيدة داخل الـ slabs الخاصة بها. في البداية، تقيم هذه الـ slabs في القائمة الجزئية لوحدة المعالجة المركزية. عندما تصل القائمة الجزئية إلى سعتها القصوى، يتم دفع الـ slabs مرة أخرى إلى القائمة الجزئية لعقدة NUMA المقابلة.
إذا تم تحرير آخر عقدة maple داخل slab، يصبح الـ slab فارغًا. إذا كان هذا الـ slab موجودًا في القائمة الجزئية لعقدة NUMA، وكانت القائمة الجزئية لتلك العقدة المحددة قد بلغت بالفعل الحد الأقصى لسعتها، يتم إرجاع الصفحة فورًا إلى مخصّص الصفحات. وبالتالي، يتحول UAF على مستوى slab إلى سيناريو UAF على مستوى page. يمكن التلاعب بالمحتويات داخل الصفحة المحرَّرة عن طريق إرسال بعض البيانات عبر `msgsnd()`، الذي يخصص كائنات مرنة ويملؤها مباشرةً ببيانات المستخدم المقدَّمة.```c
static void __slab_free(struct kmem_cache *s, struct slab *slab,
void *head, void *tail, int cnt,
unsigned long addr)
{
// ...
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
goto slab_empty;
// ...
return;
slab_empty:
// ...
discard_slab(s, slab);
}
يعتمد عدد عقد maple nodes لكل slab، ويرمز له بالحرف M، على عدد وحدات المعالجة المركزية. يفترض تنفيذ الاستغلال وجود معالجين، وبالتالي يعتبر 16 قيمةً لـ M، كما هو موضح في الشكل التالي:

عند اكتساب السيطرة على عقدة maple node، يصبح من الممكن التلاعب بعناوين VMAs اللاحقة التي سيتم تكرارها لاحقًا. وبما أن التكرار المستهدف يهدف إلى توليد /proc/self/maps، فإن بعض معلومات VMA، مثل عنواني البداية والنهاية الموجودين داخل بنية VMA، تُرجَع إلى مساحة المستخدم.
غير أن هناك تحديًا: لا يمكن تعيين عنوان بنية VMA في عقدة maple node بشكل مناسب إلا إذا كانت بعض العناوين معروفة بالفعل. لحسن الحظ، يخدم CVE-2023-0597 هذا الغرض مباشرةً. وفقًا لـ CVE-2023-0597، فإن عنوان cpu_entry_area غير عشوائي. وعلى الرغم من تصحيح هذه الثغرة في Linux 6.2، إلا أنها لم تُنقل إلى النوى المستقرة الأقدم حتى وقت كتابة هذا الشرح. وبالتالي، من خلال الكتابة فوق عنوان بنية VMA بعنوان آخر إدخال في IDT، يتم تسريب الإدخال الذي يحتوي على عنوان asm_sysvec_spurious_apic_interrupt مباشرةً، مما يكشف عن العناوين الأساسية لكود النواة وبيانات النواة.

يمكن استخدام الطريقة المذكورة سابقًا بشكل متكرر لكشف المزيد من العناوين تدريجيًا من قسم بيانات النواة. على سبيل المثال، يشير مؤشر init_task.tasks.prev داخل قسم البيانات إلى بنية task_struct الخاصة بأحدث مهمة تم إنشاؤها، والتي لا شك أنها مُخصَّصة على الكومة (heap).

عند إنهاء جميع المهام المنشأة حديثًا، سيتم تحرير بنى task_struct الخاصة بها لاحقًا. إذا كان عدد هذه المهام كبيرًا بما يكفي، يمكن إعادة الصفحات المقابلة إلى مُخصِّص الصفحات (page allocator). وهذا يتيح إمكانية إعادة تخصيص هذه الصفحات وملؤها ببيانات المستخدم. ومع ذلك، يجب مراعاة أن الصفحات المحررة تنتمي عمومًا إلى قائمة الصفحات لكل معالج (PCP). أما بالنسبة للصفحات الموجودة في قائمة PCP، فيمكن إعادة تخصيصها حصريًا بنفس رتبة الصفحة. وبالتالي، فإن مجرد تعيين صفحات جديدة في مساحة المستخدم، الأمر الذي يتطلب صفحات من الرتبة 0 فقط من مُخصِّص الصفحات، لن يحقق الأهداف.
ومع ذلك، فإن استدعاء النظام msgsnd سيطلب كتلًا من الذاكرة عبر kmalloc ويملأ هذه الكتل ببيانات يحددها المستخدم. وعندما يُستنفد مخبأ kmalloc، سيطلب صفحات من مُخصِّص الصفحات برتبة محددة. إذا تم ضبط حجم الرسالة بدقة، فسيتم الحصول على الرتبة المطلوبة بالضبط. وبالتالي، ستُعاد تخصيص الصفحة التي تم تسريب عنوانها مسبقًا. ونتيجةً لذلك، يصبح من الممكن الحصول على صفحة بعنوان معروف وبيانات يتحكم فيها المستخدم.
أصبح من الممكن الآن تزوير بنية VMA في الصفحة المعروفة عنوانها والتحكم في مؤشر الدالة vma->vm_ops->name. وتشمل الخطوة التالية إيجاد أدوات (gadgets) مناسبة للهروب من الحاويات واكتساب امتيازات الجذر.```c
static void
show_map_vma(struct seq_file *m, struct vm_area_struct *vma)
{
// ...
if (vma->vm_ops && vma->vm_ops->name) {
name = vma->vm_ops->name(vma);
if (name)
goto done;
}
// ...
}

تركيبات الأدوات (gadgets) هي كما يلي:
1. تدوير المكدس: `movq %rbx, %rsi; movq %rbp, %rdi; call
__x86_indirect_thunk_r13` -> `pushq %rsi; jmp 46(%rsi)` -> `popq %rsp; ret`
-> `popq %rsp; ret`, حيث تشير %rdi و %rbx و %r13 _مبدئيًا_ إلى
بيانات يتحكم فيها المستخدم.
2. الحصول على صلاحيات الجذر: `popq %rdi; ret` -> `prepare_kernel_cred` -> `popq
%rdi; ret` -> `movq %rax, (%rdi); ret`, حيث تشير %rdi _الآن_ إلى
قمة المكدس; `popq %rdi; ret` -> `commit_creds`, وهو ما ينفذ فعليًا
`commit_creds(prepare_kernel_cred(&init_task))`.
3. الهروب من الحاويات: `popq %rdi; ret` -> `find_task_by_vpid` -> `popq %rdi;
ret` -> `movq %rax, (%rdi); ret`, حيث تشير %rdi _الآن_ إلى قمة المكدس;
`popq %rdi; ret` -> `popq %rsi; ret` -> `switch_task_namespaces`,
وهو ما ينفذ فعليًا `switch_task_namespaces(find_task_by_vpid(1),
&init_nsproxy)`.
4. فتح قفل mm: `popq %rax; ret` -> `movq %rbp, %rdi; call
__x86_indirect_thunk_rax`, حيث يشير %rbp إلى ملف seq_file الأصلي;
`popq %rax; ret` -> `m_stop`, وهو ما ينفذ فعليًا `m_stop(seq_file, ..)`.
5. العودة إلى مساحة المستخدم: استخدم `swapgs_restore_regs_and_return_to_usermode`، و
استدعِ `execve()` للحصول على الصدفة.
أخيرًا، باستخدام `nsenter --mount=/proc/1/ns/mnt` لاستعادة مساحة أسماء التحميل
والحصول على العلم عبر `cat /flag/flag`.
### كود المصدر
الكود الكامل للاستغلال متاح [هنا](https://github.com/lrh2000/stackrot/blob/HEAD/exp). لمزيد من التفاصيل، راجع
ملف README الخاص به.