
مقاطعة طويلة جدًا جدًا جدًا جدًا جدًا جدًا جدًا
استغلال وضع إدارة النظام (SMM) باستخدام مقاطعة طويلة جداً جداً جداً جداً جداً جداً جداً.
اتضح أنه يمكنك كسر SMM — بيئة التنفيذ الآمنة فائقة الامتياز التي تعمل بشكل غير مرئي في الخلفية على كل معالج x86 — باستخدام تعليمة آلة طويلة بشكل فاحش فقط.
يتطلب SMM أن تكون جميع النوى إما داخل SMM أو خارجه في نفس الوقت. نموذجه الأمني لا يعمل بدون هذا — عندما يدخل خيط واحد إلى SMM، فإنه يجبر جميع الخيوط الأخرى على الدخول أيضاً.
لكسر هذا، كل ما نحتاجه هو شخص مشغول جداً بحيث لا يلاحظ أنه من المفترض أن ينضم إلى SMM.
يعمل الأمر تقريباً بهذا الشكل:
core 0 - ابدأ تعليمة طويلة
|
|
|
core 1 - ادعُ core 0 إلى smm
|
|
|
core 1 - ادخل إلى smm
|
|
|
core 1 - انتظر core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - انتظر core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - انتظر core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - انتظر core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - انتظر core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - انتظر core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - انتظر core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - انتظر core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - استسلم
core 1 - قم بأشياء smm السرية
core 1 - أنهِ smm
|
|
|
core 0 - انضم إلى smm
في هذه المرحلة، يكون core 1 خارج SMM بينما core 0 داخله، مما يسمح لـ core 1 بمهاجمة core 0. إليك النقطة الأساسية: لكي يعمل هذا، نحتاج إلى تعليمة طويلة جداً جداً جداً — أطول مما كان من المفترض أن تستغرقه أي تعليمة على الإطلاق. معظم تعليمات الآلة على معالج حديث سريعة: add تستغرق دورة واحدة. لجعل core 1 يستسلم في انتظار core 0، نحتاج إلى تعليمة على core 0 تستغرق حوالي 4,000,000,000 دورة — أكثر من ثانية واحدة من زمن الساعة الحقيقي.
يقوم برنامج x86 الثابت (firmware) بتشغيل الكود التالي عندما يدخل نواة معالج إلى SMM:
for (Timer = StartSyncTimer ();
!IsSyncTimerTimeout (Timer, mTimeoutTicker) && SyncNeeded;
)
{
mSmmMpSyncData->AllApArrivedWithException = AllCpusInSmmExceptBlockedDisabled ();
if (mSmmMpSyncData->AllApArrivedWithException) {
break;
}
CpuPause ();
}
ينتظر الكود دخول جميع النوى إلى SMM، أو حتى ثانية واحدة، أيهما يحدث أولاً. لجعل نواة تنفذ كود SMM بينما تبقى نواة أخرى تنفذ خارج SMM، نحتاج إلى أن تبقى تلك النواة الخارجية غير قابلة للمقاطعة طوال الثانية كاملة — يتم التقاط SMI عند حدود تعليمة، لذا أي فجوة بين تعليمتين تسمح لـ SMI المعلق بسحب النواة إلى SMM. لذلك يجب أن يكون التأخير تعليمة واحدة: عملية واحدة غير قابلة للمقاطعة تدوم أطول من موعد الالتقاء (rendezvous) البالغ ثانية واحدة.
هناك طرق عديدة للوصول إلى تعليمة الثانية الواحدة المحظورة، والنهج الدقيق سيختلف من منصة إلى أخرى. لكن، بشكل تقريبي: ابحث عن عنوان MMIO عالي الكمون، ثم أقنع المعالج بقراءته بأبطأ ما يمكن — استغل منطقة غير موثقة تستجيب للقراءات ببطء شديد، واستخدم أوسع تحميل توفره ISA لنقل أكبر عدد ممكن من البايتات عبره في تعليمة واحدة، ودع النوى الأخرى تتنافس على نفس الناقل لإبطائه أكثر. قراءة واحدة، تعليمة واحدة، ويعلق المعالج بها لجزء كبير من الثانية.
إن إثبات المفهوم المقدم مضبوط لمعالج Zen 3 Ryzen 7 5800H، حيث يتوقف تحميل xmm عريض من MMIO بطيء عند 0xfcc68860 لفترة كافية لكسر موعد الالتقاء لجميع النوى:
mov $0xfcc68860, %rsi ; عنوان MMIO المستهدف
vmovdqu (%rsi), %xmm0 ; التحميل الطويل جداً جداً
يستغل إثبات المفهوم هذا بوضع نواتين ضد بعضهما البعض. نواة واحدة تُبقى خارج SMM بواسطة التعليمة الطويلة — حلقة ضيقة على التحميل البطيء جداً:
/* النواة الضحية: تدور على التحميل الذي يستغرق ~ثانية واحدة، مشغولة جداً عن الاستجابة لـ SMI */
for (;;)
asm volatile ("vmovdqu (%0), %%xmm0" :: "r"(mmio) : "xmm0");
في هذه الأثناء، تقوم نواة أخرى بتجهيز عدادات SMI لكل نواة:
#define MSR_PERF_CTL0 0xc0010200 /* MSR اختيار حدث الأداء لنواة AMD */
#define MSR_PERF_CTR0 0xc0010201 /* العداد المقترن بـ 48 بت */
for (int cpu = 0; cpu < ACTIVE_CPUS; cpu++) {
msr_write(cpu, MSR_PERF_CTL0, 0x43002b); /* EN | OS | USR | حدث 0x2b */
msr_write(cpu, MSR_PERF_CTR0, 0); /* صفر العدد */
}
ثم يطلق عاصفة من SMIs:
asm volatile ("outb %%al, $0xb2" :: "a"(0)); /* ارفس المنفذ 0xb2 -> #SMI */
ويقرأ إجمالي كل نواة:
/* ...أطلق العاصفة، ثم اقرأ إجمالي كل نواة... */
uint64_t delta = smi_max - smi_min;
if (delta)
puts("!!! نواة عملت خارج SMM");
إذا تباعدت الأعداد، فهذا يعني أن نواة استمرت في العمل خارج SMM بينما تم سحب البقية إليها — لقد فاتتها SMIs التي خدمتها بقية النوى.
لتوضيح ذلك بشكل أفضل، يمكننا تشغيل إثبات المفهوم خلف واجهة رسومية مبهرجة بلا داعٍ وعديمة الفائدة تماماً، تتتبع عدادات SMI على كل نواة، لمشاهدتها وهي تنفذ بتناغم تام حتى تكسر نواة متأخرة بشكل كبير التزامن المطلوب:

ضمانة SMM الوحيدة — أن لا شيء آخر يعمل أثناء عمله — تنهار أمام تعليمة طويلة بشكل سخيف واحدة.
يعتمد أمان SMM على افتراض بسيط: بينما يعمل، لا يعمل أي شيء آخر.
هناك أكثر من 100 ثغرة TOCTOU في SMM خارج هناك: معالج SMM يتحقق من قيمة في ذاكرة مشتركة، ثم يستخدمها. كل ما تحتاجه للاستغلال هو إعادة كتابة تلك القيمة بين الفحص والاستخدام، وستكون داخل SMM. لكن هذه المشكلات تبقى خاملة وغير مصححة إلى حد كبير في البرية، بسبب افتراض واحد: الاستغلال يتطلب شيئاً يعدّل الذاكرة المشتركة بينما ينفذ SMM، وبسبب موعد التقاء SMM لا توجد أي نوى معالجة خارج SMM لشن هجوم. الطريقة الوحيدة للدخول كانت طرفاً طرفياً قادراً على DMA يكتب خلف ظهر المعالج — وصول مادي، جهاز خبيث — لذلك يُكتب الفصل بأكمله كمشكلة عتاد.
إزالة تزامن SMI تزيل الشرط المسبق الذي أبقى المنصة آمنة: نواة خارجية، بدون وصول مادي أو عتاد مطلوب، يمكنها الآن العمل بينما ينفذ SMM — وتصبح ثغرات CVE الخاملة قابلة للاستغلال من البرمجيات.
من المحتمل ألا توجد أي منها، وهذا ما يجعل هذه المشكلة أكثر إثارة للاهتمام إلى حد ما من مشكلات SMM التقليدية. أبقِ المهلة، وسيتم كسر موعد الالتقاء بسهولة. أزل المهلة، وستعلق نواة عالقة بشكل شرعي المنصة عند أول SMI. زد المهلة، وستقتل الأداء على المنصات متعددة النوى التي تُجبر على إسكات جميع النوى عند كل دخول إلى SMM. ليس من الواضح ما هو أفضل مسار للمضي قدماً، أو إذا كان هناك حتى مسار للمضي قدماً على الإطلاق.
حتى ذلك الحين، الحل البديل الموصى به هو عدم تنفيذ أي تعليمات طويلة.
إن vmovdqu الافتراضي عند 0xfcc68860 في إثبات المفهوم هو نقطة بطيئة على هذا الجهاز — Zen 3 Ryzen 7 5800H — ومن المحتمل ألا يكون كذلك في أي مكان آخر. لكسر موعد الالتقاء على جهازك، ستحتاج إلى إعادة ضبط التعليمة الطويلة بحيث يتجاوز التوقف مهلة SMM الخاصة بك. بعض النصائح حول كيفية القيام بذلك:
-r xmm → ymm → zmm حتى يتجاوز التوقف مهلة موعد الالتقاء.make # يبني smiiiiiiiiiiiiiiii
الإعدادات الافتراضية مضبوطة لجهاز واحد. على أي شيء غير Zen 3 Ryzen 7 5800H، لا تتوقع أي تباعد حتى تعيد ضبط التعليمة الطويلة — انظر النقل إلى منصتك.
شغّل الأداة لإطلاق التعليمة الطويلة جداً جداً بشكل متكرر أثناء مراقبة عداد SMI لكل نواة بحثاً عن تباعد:
sudo ./smiiiiiiiiiiiiiiii # الافتراضي: -r xmm عند 0xfcc68860
الخيارات:
| الخيار | الافتراضي | الوصف |
|---|---|---|
-r xmm|ymm|zmm | xmm | عرض سجل المتجهات لقراءة MMIO الموقوتة (16/32/64 بايت). إذا لم يُلاحظ أي فرق في عدد SMI، تنصح الأداة بالترقية إلى الحجم التالي. |
-a <phys-addr> | 0xfcc68860 | العنوان الفعلي المستهدف لحلقة توقيت MMIO (سداسي عشري 0x... أو عشري). |
-h, --help | — | طباعة الاستخدام والخروج. |
smiiiiiiiiiiiiiiii هو جهد بحثي من كريستوفر دوماس (@xoreaxeaxeax).