
كتابة وتحليل واستغلال لثغرة 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 المقدمة
ولكن ليس بسبب وضع writeInt عن طريق الخطأ، بل بسبب الانعكاس غير المقيد (unrestricted reflection). على وجه التحديد داخل PackageParser لدينا الكود التالي:```java
final Class cls = (Class) Class.forName(componentName);
final Constructor cons = cls.getConstructor(Parcel.class);
intentsList = new ArrayList<>(N); for (int i = 0; i < N; ++i) { intentsList.add(cons.newInstance(in)); }
يمكننا الحصول على كائن `Parcel` الذي تم تمريره إلى `createFromParcel` وتمريره إلى أي مُنشئ `public` متاح في النظام يقبل وسيط `Parcel` واحدًا
ثم [لدينا الكود التالي](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/PooledStringWriter.java;l=51-56;drc=782d49826862cbdc9d020fc9d85f8a6f64675dcb):```java
public PooledStringWriter(Parcel out) {
mOut = out;
mPool = new HashMap<>();
mStart = out.dataPosition();
out.writeInt(0); // reserve space for final pool size.
}
لدينا مُنشئ يستدعي writeInt(0) على Parcel المقدَّم، لكن هناك بعض الأمور التي تُعقّد الاستغلال
أولاً، على الرغم من أنه غير مرئي مباشرةً في الكود المصدري، فور استدعاء newInstance()، يتم تنفيذ تحويل نوع (cast) ويتم إلقاء استثناء ClassCastException
كنت بحاجة إلى شيء يقوم أثناء createFromParcel باستدعاء createFromParcel لفئة أخرى داخل كتلة try ثم يفشل في تمرير الاستثناء المُلتقَط
هذا هو الجزء الذي لا يعمل فيه الاستغلال فعليًا على AOSP النقي، لقد استخدمت فئة خاصة بشركة Samsung
لقد أدرجت نسخة من الأجزاء ذات الصلة من تلك الفئة في هذا المستودع
يتضمن هذا المستودع أيضًا نصًا برمجيًا يدمجها في AOSP، لذا للاختبار يمكنك تشغيله (مرّر مسار نسخة AOSP الخاصة بك كوسيط، مثل ./make-aosp-buggy.sh /path/to/aosp)، ثم تراجع عن التغيير الموصوف في بداية التقرير وتشغّل هذا الاستغلال ضد نسخة AOSP الخاصة بك
لقد استخدمت سابقًا فئة OutputConfiguration من AOSP لابتلاع الاستثناءات، قبل Android 13 كان ابتلاع استثناء في createFromParcel() مقترنًا بالسماح بإنشاء كائنات Parcelable أخرى يُعد ثغرة بحد ذاته، لكن في حالة SemImageClipData لم يكن ابتلاع الاستثناء موجودًا في إصدارات Android هذه
لكن هناك فرق مهم بين SemImageClipData وOutputConfiguration المستخدمة سابقًا: على الرغم من أن SemImageClipData يلتقط استثناءً، فإنه لا يزال يُرجع كائنًا غير فارغ، وإذا تم تحويل نوعه لاحقًا إلى نوع آخر، فسيؤدي ذلك إلى إلقاء ClassCastException وهو ما نحاول تجنّبه
محو النوع في Java يعني أن الطرق العامة (generic methods) لا تعرف فعليًا النوع العام الذي يستخدمه المُستدعي. هذا عادةً ما كان يساعد في الاستغلال```java // When we read some List, this actually didn't check if list contains only SomeParcelableType List myList = sourceParcel.readParcelableList();
// Above is why Android 13 has introduced typed methods that enforce type at runtime List myList = sourceParcel.readParcelableList(SomeParcelableType.class);
// If untyped method was used when reading, list can contain non-SomeParcelableType // items and they would be written without errors targetParcel.writeParcelableList(myList, 0);
// However if List contains non-SomeParcelableType item, this would throw during item access // (That however commonly didn't happen if we used Parcelable object only as container in gadget chain) SomeParcelableType myItem = myList.get(0);
هذه المرة لم يعمل محو النوع (type erasure) لصالحنا. أولاً كان لدينا طريقة تقوم فعلياً باستدعاء المُنشئ (constructor) عبر الانعكاس (reflection)```java
private static <T extends IntentInfo> ArrayList<T> createIntentsList(Parcel in) {
// ...
final ArrayList<T> intentsList;
// ...
intentsList.add(cons.newInstance(in));
// ...
return intentsList;
}
هذه الطريقة تحتوي على معامل عام T. لا يهم نوع المعامل الذي استخدمه المستدعي، ومع ذلك نظرًا لأن إعلان هذه الطريقة يحتوي على <T extends IntentInfo>، فإن السطر الذي يحتوي على استدعاء newInstance() يصبح intentsList.add((IntentInfo) cons.newInstance(in));، حتى وإن كان newInstance() يُرجع Object وArrayList.add() يقبل Object كوسيط. هذا أدى إلى الحاجة إلى تغليف استدعاء ذلك ببعض Parcelable الذي يبتلع Exception
ثم لدينا استدعاء `bundle.getParcelable()````java @Deprecated @Nullable public T getParcelable(@Nullable String key) { unparcel(); Object o = getValue(key); if (o == null) { return null; } try { return (T) o; } catch (ClassCastException e) { typeWarning(key, o, "Parcelable", e); return null; } }
إجراء إلغاء التسلسل يتم عبر استدعاء `getValue()`، والذي يؤدي فعليًا إلى استدعاء `createFromParcel()`. إذا حدث `ClassCastException` هناك فلن يتم التقاطه. الآن `getValue()` يُرجع أي قيمة تم إلغاء تسلسلها لهذا المفتاح عبر [`parcel.readValue()`](https://developer.android.com/reference/android/os/Parcel#readValue(java.lang.ClassLoader))
ولكن إذا وضعنا `SemImageClipData` كقيمة، ضمن `try`-`catch` سنحاول تحويلها إلى `T`، والتي في هذه الحالة هي `Parcelable` كما هو مُعلن في التصريح العام للطريقة. المتصل يستخدم هذه الطريقة كطريقة عامة مع `T` كونها `Intent`، ومع ذلك `getParcelable()` لا يعرف ذلك ويتم التحويل إلى `Intent` في المتصل، وبالتالي يتم إلقاء `ClassCastException` خارج `try`
يمكننا مع ذلك تغليف `SemImageClipData` داخل مصفوفة `Parcelable[]`، ثم التحويل إلى `T` داخل `getParcelable()` سيفشل في تحويل `Parcelable[]` إلى `Parcelable` وسيُلقي `ClassCastException` ضمن `try`، سيتم تسجيل هذا `Exception` وسيتم إرجاع `null` ثم قبوله بواسطة `checkKeyIntent()`
# التخطيط
إذن الآن نحتاج إلى محاذاة العناصر داخل `Bundle` بحيث بعد دورة `writeToParcel`/`createFromParcel` تكون محتوياته هي تلك التي أعددناها
ولكن على عكس "`Bundle` FengShui" النموذجي حيث يكون المحفز هو جعل `createFromParcel` يقرأ بيانات أكثر أو أقل مما تطابقه `writeToParcel` سابقًا، هنا لدينا `writeInt(0)` الذي يكتب فوق جزء من `LazyValue` غير المُلغى تسلسله
إذن هكذا يبدو `Bundle.mParcelledData` عندما يتم إلغاء تسلسله أول مرة بواسطة `AccountManagerService` (الإزاحات مأخوذة باستدعاء `dataPosition()` عبر مصحح أخطاء مرتبط بـ `system_server`)
<table>
<tr><th>الإزاحة</th><th>القيمة</th><th>ملاحظة</th></tr>
<tr><td>0</td><td>3</td><td>عدد أزواج المفتاح-القيمة</td></tr>
<tr><td>4</td><td>"intent"</td><td>أول مفتاح في <code>Bundle</code>، وهو الذي سيتم الوصول إليه عبر <code>getParcelable(AccountManager.KEY_INTENT)</code></td></tr>
<tr><td>24</td><td>16</td><td>أول <code>LazyValue</code> يبدأ هنا، النوع هو <code>VAL_PARCELABLEARRAY</code></td></tr>
<tr><td>28</td><td>340</td><td>الطول المُعلن لـ <code>LazyValue</code>، يُستخدم للعثور على المفتاح التالي في <code>Bundle</code>. لن يكون لـ <code>LazyValue</code> الخاص بنا هذا الحجم فعليًا بعد قراءته، لكن <code>LazyValue.apply</code> <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Parcel.java;l=4501-4505;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">يُبلغ عن ذلك عبر <code>Slog.wtfStack()</code></a> والذي <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Slog.java;l=230-235;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">لا يُلقي استثناءً</a></td></tr>
<tr><td>32</td><td>1</td><td>طول مصفوفة <code>Parcelable[]</code>، المصفوفة تحتوي على عنصر واحد فقط وهي موجودة بحيث يحدث <code>ClassCastException</code> داخل كتلة <code>try</code> الموجودة في <code>bundle.getParcelable()</code></td></tr>
<tr><td>36</td><td>"com.samsung.android.<br>content.clipboard.data.<br>SemImageClipData"</td><td>اسم فئة <code>Parcelable</code>، هذه هي الفئة المغلفة التي ستبتلع الاستثناء</td></tr>
<tr><td>160</td><td>2</td><td>وسم النوع المستخدم بواسطة <code>createClipBoardData()</code></td></tr>
<tr><td>164</td><td></td><td>العناصر التي تُقرأ بواسطة مُنشئ الفئة الفائقة <code>SemImageClipData</code> (والتي تتضمن استدعاء <code>readParcelable()</code>، ومع ذلك يحدث ذلك خارج كتلة <code>try</code>). ليست ذات صلة حقًا، لكننا بحاجة إلى المرور عبرها قبل الوصول إلى الجزء المهم من <code>createFromParcel()</code></td></tr>
<tr><td>252</td><td></td><td>البيانات المقروءة بواسطة <code>SemImageClipData.readFromSource()</code></td></tr>
<tr><td>272</td><td>"android.content.pm.<br>PackageParser$Activity"</td><td>اسم <code>Parcelable</code> المقروء بواسطة <code>mExtraParcelFd = in.readParcelable()</code>. النوع غير متطابق، ومع ذلك قبل حدوث التحويل سيتم إلقاء استثناء على أي حال</td></tr>
<tr><td>360</td><td></td><td>حقل <code>className</code> و <code>metadata</code> لـ <code>PackageParser$Component</code></td></tr>
<tr><td>368</td><td>1</td><td>عدد العناصر في <code>createIntentsList()</code></td></tr>
<tr><td>372</td><td>"android.os.<br>PooledStringWriter"</td><td>اسم الفئة التي سنقوم بإنشاء مثيل منها عبر <code>Class.forName().getConstructor(Parcel.class).newInstance()</code>. في هذا الموضع ينتهي أول <code>LazyValue</code>، ومع ذلك يستمر تحليله لأن <code>readValue()</code> لم يصل إلى النهاية. يُفسَّر أيضًا كمفتاح ثانٍ في <code>Bundle</code> أثناء <code>unparcel()</code> الأولي</td></tr>
<tr><td>436</td><td>4</td><td>ثاني <code>LazyValue</code> يبدأ هنا، هذه الـ 4 هي <code>VAL_PARCELABLE</code> التي سيعيد لها <code>Parcel.isLengthPrefixed()</code> القيمة <code>true</code>. هذه القيمة تُستبدل لاحقًا بواسطة مُنشئ <code>PooledStringWriter</code>، وبعد ذلك يتم إلقاء استثناء وينتهي <code>getParcelable(AccountManager.KEY_INTENT)</code></td></tr>
<tr><td>440</td><td>240</td><td>طول <code>LazyValue</code> الذي تم الإعلان عن نوعه كـ <code>VAL_PARCELABLE</code>، يُستخدم هذا لتحديد موضع الإدخال التالي ومقدار البيانات التي يجب نسخها إلى <code>Bundle</code> الهدف أثناء إعادة التسلسل. هذا <code>LazyValue</code> لا يتم إلغاء تسلسله فعليًا ويُستخدم كحاوية بيانات خام</td></tr>
<tr><td>684</td><td>"1&y~pw"</td><td rowspan="2">زوج المفتاح-القيمة الثالث، المفتاح مُولَّد عشوائيًا بحيث يكون <code>hashCode()</code> في Java أعلى من تلك المستخدمة سابقًا (العناصر المخزنة داخل <code>ArrayMap</code> مرتبة حسب <code>hashCode()</code> التصاعدي للمفتاح وهذا هو الترتيب الذي ستُكتب به العناصر من <code>Bundle</code> إلى <code>Parcel</code>). هذا الزوج موجود هنا فقط لزيادة العدد الإجمالي للأزواج المكتوبة، حيث أن هذا سيكون عدد الأزواج المقروءة، على الرغم من أن هذا الزوج لن يُقرأ فعليًا</td></tr>
<tr><td>704</td><td>-1 (<code>VAL_NULL</code>)</td></tr>
</table>
ثم، عندما يتم تسلسل `Bundle` مرة أخرى، يبدو هكذا:
<table>
<tr><th>الإزاحة</th><th>القيمة</th><th>ملاحظة</th></tr>
<tr><td>0</td><td>3</td><td>عدد أزواج المفتاح-القيمة</td></tr>
<tr><td>4</td><td>"intent"</td><td>أول مفتاح في <code>Bundle</code></td></tr>
<tr><td>24</td><td>16</td><td><code>VAL_PARCELABLEARRAY</code>، مصفوفة <code>Parcelable[]</code> التي تم إلغاء تسلسلها سابقًا يتم الآن تسلسلها مرة أخرى</td></tr>
<tr><td>28</td><td>196</td><td>طول <code>LazyValue</code>، وهو كائن <code>SemImageClipData</code> المغلف لدينا. هذا الطول مأخوذ من التنفيذ مع <code>SemImageClipData</code> الوهمي الخاص بي وبالتالي الإزاحات المعروضة من هذه النقطة لن تطابق تلك التي ستظهر على جهاز Samsung فعلي، ومع ذلك لن يتم إلغاء تسلسل هذا <code>LazyValue</code> مرة أخرى لذا لا يهم ذلك لتنفيذ الاستغلال</td></tr>
<tr><td>224</td><td>"android.os.<br>PooledStringWriter"</td><td>المفتاح الثاني في <code>Bundle</code></td></tr>
<tr><td>288</td><td>0</td><td>ثاني <code>LazyValue</code> يبدأ هنا، العنصر تحت مفتاح <code>"android.os.PooledStringWriter"</code> لم يتم الوصول إليه، لذا يتم نسخ هذا <code>LazyValue</code> من البيانات الأصلية، ومع ذلك تم استبدال وسم النوع بواسطة استدعاء <code>writeInt(0)</code> الذي قام به مُنشئ <code>PooledStringWriter</code> وعند الوصول إلى العملية الهدف لم يعد يُفسَّر كـ <code>LazyValue</code></td></tr>
<tr><td>292</td><td>240</td><td>كان هذا طول ثاني <code>LazyValue</code> الذي تم نسخه من <code>Bundle</code> الأصلي، ومع ذلك نظرًا لأن وسم النوع تم استبداله بـ <code>writeInt(0)</code>، وهو <code>VAL_STRING</code>، يتم الآن قراءة هذه القيمة عبر <code>readString()</code>. سابقًا، بالنسبة لـ <code>LazyValue</code>، كان الطول معبرًا عنه بالبايتات، لكن الآن، بالنسبة لـ <code>String</code>، يتم التعبير عنه بأحرف ثنائية البايت. لا توجد بيانات كافية في <code>Parcel</code> المصدر لذلك، لذا <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=2221-2226;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">يفشل <code>parcel->readString16Inplace()</code> الأصلي بعد قراءة الطول</a>، ومع ذلك لا يسبب ذلك استثناءً على جانب Java</td></tr>
<tr><td>296</td><td>"intent"</td><td>المفتاح "الثالث" في <code>Bundle</code>. في الواقع يستبدل المفتاح الأول: نظرًا لأن <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=651-659;drc=584140c83a456b5de99880b440c2d5dfc3c70506">"intent" له <code>hashCode()</code> أصغر من المفتاح الذي شوهد سابقًا، تستخدم طريقة <code>ArrayMap.append()</code> <code>put()</code> التي تسمح باستبدال القيم</a>، وإلا <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=667-675;drc=584140c83a456b5de99880b440c2d5dfc3c70506">سيكون لدينا مفتاح مكرر سيتم رفضه لاحقًا بواسطة <code>validate()</code></a></td></tr>
<tr><td>316</td><td>4</td><td><code>VAL_PARCELABLE</code>، هنا يبدأ <code>LazyValue</code> الذي يحتوي على <code>Intent</code> الفعلي الذي سيتم تشغيله</td></tr>
<tr><td>536</td><td>"1&y~pw"</td><td>عنصر الحشو الذي تم كتابته ولكن لا يُقرأ لأن جميع أزواج المفتاح-القيمة الثلاثة تمت قراءتها بالفعل. <a href="https://cs.android.com/android/_/android/platform/system/tools/aidl/+/96a02f50fdfa4d20aa46ae2dde927257eac46d4a">على عكس واجهات AIDL</a>، لا يوجد فحص <code>enforceNoDataAvail()</code> يتم على <code>Bundle</code> (ولكن حتى لو كان موجودًا، <a href="https://github.com/michalbednarski/ReparcelBug2/issues/3">يمكن تجاوزه بإدخال إدخال وهمي يحدد الطول المتوقع</a>)</td></tr>
</table>
# كيف حدث ذلك مرتين
لنناقش الآن أربعة تصحيحات، اثنان منها يصلحان الثغرة التي يتناولها هذا التقرير
* CVE-2023-20944 ([النشرة](https://source.android.com/docs/security/bulletin/2023-02-01#framework)، [التصحيح](https://android.googlesource.com/platform/frameworks/base/+/d0bc9026e2e62e09fa88c1bcbf1dc1c3fb001375%5E%21/)): هذه ثغرة أخرى اكتشفتها أنا. وبالمثل لهذه، لا يجعل التصحيح من الواضح كيف يمكن استغلالها، ولكن <a href="https://konata.github.io/posts/creator-mismatch/">يبدو أن شخصًا آخر قد اكتشفها (منشور مدونة بالصينية)</a>
* CVE-2023-21098 ([النشرة](https://source.android.com/docs/security/bulletin/2023-04-01#framework)، [التصحيح](https://android.googlesource.com/platform/frameworks/base/+/107e6377328486fca55131ea06ca9d6a3c1585e0%5E%21/)): هذه هي المرة الأولى التي أبلغ فيها عن الاستغلال المعروض هنا. يقدم هذا التصحيح أيضًا إصلاحًا لتجاوز `checkKeyIntentParceledCorrectly()` الذي ينطبق على إصدارات Android قبل 13
* CVE-2023-35669 ([النشرة](https://source.android.com/docs/security/bulletin/2023-09-01#framework)، [التصحيح](https://android.googlesource.com/platform/frameworks/base/+/f810d81839af38ee121c446105ca67cb12992fc6%5E%21/)): هذا ليس استجابة لتقريري، لكنني أعتقد أنه تم إجراؤه لإصلاح نفس المشكلة التي تناولتها CVE-2023-20944، ولكن للحالات التي يتم فيها تشغيل <code>AccountManager.KEY_INTENT</code> بواسطة Activities أخرى غير <code>ChooseTypeAndAccountActivity</code> (على سبيل المثال <a href="https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Settings/src/com/android/settings/accounts/AddAccountSettings.java;l=95-107;drc=32813a2bef49b172aed89122b4eb50bf14026ddc"><code>AddAccountSettings</code></a>، الذي فاتني عند الإبلاغ عن الثغرة في المرة الأولى). هذا التغيير استبدل استخدام <code>bundle.getParcelable()</code> المُنمَّط باستخدام غير المُنمَّط وفحص <code>getClass() != Intent.class</code> اليدوي، والذي ألغى فعليًا إصلاح CVE-2023-21098
* CVE-2023-45777 ([النشرة](https://source.android.com/docs/security/bulletin/2023-12-01#framework)، [التصحيح](https://android.googlesource.com/platform/frameworks/base/+/f4644b55d36a549710ba35b6fb797ba744807da6%5E%21/)): هذه هي المرة الثانية التي أبلغ فيها عن هذا الاستغلال. أبقى التصحيح على الفحص اليدوي <code>getClass() != Intent.class</code>، ولكن بالإضافة إلى ذلك أعاد استخدام <code>bundle.getParcelable()</code> المُنمَّط، وهي طريقة جيدة لإصلاح المشكلتين
بينما يعمل نفس الاستغلال لكل من CVE-2023-21098 و CVE-2023-45777، تختلف طريقة تجاوز `checkKeyIntentParceledCorrectly()` التي حدثت بها
في حالة CVE-2023-21098، <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=3519-3521;drc=cdd30b5c040ba7ebd0a1cc6009183ff602434fc0">لم يتم استدعاء <code>checkKeyIntent()</code> فعليًا إذا لم يكن <code>Bundle</code> المُفحص يحتوي على <code>Intent</code></a>. نظرًا لأن <code>checkKeyIntent()</code> هو ما يستدعي <code>checkKeyIntentParceledCorrectly()</code>، في الحالة التي لم يظهر فيها <code>Bundle</code> الأصلي أنه يحتوي على <code>Intent</code>، لم يتم فحص <code>Bundle</code> بعد إعادة التسلسل
في حالة CVE-2023-45777، تم استدعاء <code>checkKeyIntentParceledCorrectly()</code> بشكل صحيح، ومع ذلك <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4921-4929;drc=b0f6558fb36eb76df35c516ec5a65030a34a8734">حدث <code>writeBundle()</code> هناك قبل استدعاء <code>getParcelable()</code> بدون وسيط نوع (حتى ذلك الحين لم يغير <code>Bundle</code> محتوياته)</a>