
كتابة وتحليل واستغلال لثغرة CVE-2023-45777، تجاوز للتحقق من Intent داخل AccountManagerService على أندرويد 13 على الرغم من تخفيف "Lazy Bundle"
لنبدأ هذه المرة بالتصحيح الذي ظهر كإصلاح لـ CVE-2023-45777 في نشرة أمان أندرويد:```diff diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 7a19d034c2c8..5238595fe2a2 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -4923,7 +4923,7 @@ public class AccountManagerService p.setDataPosition(0); Bundle simulateBundle = p.readBundle(); p.recycle();
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
if (intent != null && intent.getClass() != Intent.class) {
return false;
}
قليل من الناس كانوا محتارين بشأنها بما يكفي لسؤالي، وقد ردّيت عليهم سابقًا ببعض التلميحات، والآن أنشر الشرح الكامل لهذه المشكلة
لكن أولًا دعونا نوفر بعض السياق حول ما يحدث في هذا التصحيح
هذا تغيير في [`checkKeyIntent()` method](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4938-4954;drc=47de64a38aa1799cb41f41b2ea0c539ee61de64d). تقوم هذه الطريقة بإجراء عدة فحوصات للتأكد من أن `Intent` المقدَّم من التطبيق آمن للنظام لإطلاقه (باستخدام صلاحيات النظام)
أولًا، تستخدم هذه الطريقة `checkKeyIntentParceledCorrectly()` التي تقوم بتسلسل وإعادة تسلسل `Bundle` الذي نفحصه وتتحقق مما إذا كان `Intent` المأخوذ من `Bundle` قبل ذلك يطابق `Intent` من `Bundle` بعد هذه الدورة. نظرًا لأن إطلاق `Intent` يحدث في عمليات تطبيقات نظام أخرى غير تلك التي تقوم بالتحقق، كان [من الممكن سابقًا إنشاء `Bundle`-s تبدو آمنة أثناء التحقق داخل `AccountManagerService`، ولكنها تحتوي على `Intent` مختلف بعد إرسالها إلى العملية التالية](https://github.com/michalbednarski/IntentsLab/issues/2#issuecomment-344365482). هذا يحاكي إرسال `Bundle` إلى العملية التالية من أجل اكتشاف مثل هذه الحالات.
بعد `checkKeyIntentParceledCorrectly()` لدينا استدعاء `bundle.getParcelable()`، وهذا التصحيح يحوّله من الإصدار القديم الذي كان يمكنه إنشاء أي كائن إلى إصدار يتحقق من أن الكائن الذي سيتم إلغاء تسلسله هو من النوع المحدد في المعامل الثاني
هذا الإصدار مع معامل النوع تم تقديمه في Android 13، كجزء من تعزيز أكبر لـ `Parcel`/`Bundle`. على وجه الخصوص، قبل Android 13 عندما كان يتم إرسال `Bundle` بين العمليات، كان يحتفظ بنسخة خام من كامل البيانات المسلسلة حتى يتم الوصول إلى أي عنصر، وعندها يتم إلغاء تسلسل كل قيمة. الآن عندما يتم الوصول إلى أي قيمة لأول مرة بعد استلام `Bundle`، يتم إلغاء تسلسل مفاتيح `String` وقيم الأنواع البدائية فقط، بينما تُترك القيم غير البدائية كـ `LazyValue`-s، التي يتم تخزين طولها كجزء من البيانات المسلسلة لضمان أنه حتى عندما يكون منطق التسلسل/إلغاء التسلسل غير متطابق، فإن هذه الاختلافات لن تؤثر على الإدخالات الأخرى
قبل أن نتعمق، دعونا نلقي نظرة على `LazyValue`: في الكود المصدري الخاص به [لدينا تعليق جميل يشرح بنية بياناته](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4392-4399;drc=03c34f57c05feecfb090de3917787f049cb5f804)```
| 4B | 4B |
mSource = Parcel{... | type | length | object | ...}
a b c d
length = d - c
mPosition = a
mLength = d - a
mPosition وmLength يصفان موقع بيانات LazyValue كاملة في Parcel الأصلية، بما في ذلك type وlength. تشير "length" (بدون "m" في البداية) إلى قيمة الطول كما كُتبت في Parcel وتستثني الترويسة (type وlength)
إذا تم إعادة توجيه Bundle الذي يحتوي على LazyValue إلى عملية أخرى، يتم نسخ LazyValue كاملة بما في ذلك حقلي type وlength حرفياً من Bundle.mParcelledData إلى Parcel الوجهة
عند الوصول إلى عنصر Bundle الممثل بواسطة LazyValue، يتم إرجاع Parcel إلى mPosition ويتم استدعاء readValue(). إذا تم تمرير وسيط نوع إلى bundle.getParcelable()، فسيتم تمريره إلى readValue() الذي سيضمن أن النوع الذي سيتم إلغاء تسلسله هو النوع المتوقع وكذلك التحقق بعد إلغاء التسلسل من أن نوع القيمة المُلغى تسلسلها هو النوع المتوقع. بعد إلغاء التسلسل، يتم استبدال LazyValue بحيث في المرة القادمة التي يُكتب فيها Bundle إلى Parcel، سيتم تسلسل القيمة عبر writeValue() مرة أخرى
استخدام وسيط النوع في Bundle.get*()/Parcel.read*() ذو صلة بشكل أساسي بطرق مثل Parcel.readParcelableList()، والتي تُرجع ArrayList وبسبب محو النوع في جافا (Java Type Erasure) حتى لو قمت بشيء مثل List<SomeParcelableType> field = parcel.readParcelableList();، فإن الجزء <SomeParcelableType> لم يكن مفروضاً في وقت التشغيل ويمكن لمثل هذه List أن تحتوي على أي فئات Parcelable متاحة في النظام وبالتالي يمكن استخدام جميع createFromParcel/writeToParcel المتاحة في النظام كجزء من التسلسل/إلغاء التسلسل للنوع الذي يحتوي على مثل هذه List
قد ترغب أيضاً في الاطلاع على عرض تقديمي من فريق أمان وخصوصية أندرويد حول تقديم هذه الآليات (الشرائح، الفيديو)
هنا، ومع ذلك، يبدو استخدام النسخة المكتوبة زائداً عن الحاجة، لأننا نتحقق أيضاً صراحةً من نوع الكائن المُعاد. فما الذي يحدث وما هي الثغرة التي يتم إصلاحها هنا؟
ألقِ نظرة على التصحيح من البداية مرة أخرى
"intent" هي Intent
Intent فسنواجه مشكلة أكبر بكثيرIntent
Intent داخل Bundle بعد إرساله إلى عملية أخرى، لكن نوع Parcelable يُحفظ في إزاحة أبكر من أي عدم تطابق محتمل ويمنعنا تحديد طول LazyValue من تعديل أزواج المفاتيح-القيم التالية في حالة عدم تطابق writeToParcel/createFromParcelإذن، ما الشيء الخطير الذي يمكن أن يفعله استدعاء bundle.getParcelable(AccountManager.KEY_INTENT) بدون وسيط نوع هنا؟
[الإجابة في الفقرة التالية، حاول التخمين قبل المتابعة. إذا كان لدي شخصية حيوانية (fursona) لكان هذا مكاناً لبعض الفنون]
الإجابة هي استدعاء createFromParcel() غير ذي صلة الذي يعدّل فعلياً البيانات الخام لـ LazyValue المخزنة تحت مفتاح مختلف والتي سيتم تمريرها حرفياً إلى العملية التالية
لدينا تنفيذ createFromParcel() يمكنه فعلياً استدعاء writeInt() على Parcel المقدمة