
شرح واستغلال ثغرة CVE-2024-49746: إغلاق Parcel::continueWrite في أندرويد لـ File Descriptors التي تُستخدم لاحقًا
ظهر إصلاح لهذه المشكلة تحت الرقم CVE-2024-49746: bulletin, patch
العنوان أعلاه هو التعليق من طريقة Parcel::continueWrite، وهي المسؤولة فعليًا عن تغيير حجم كائنات Parcel، سواء عندما يطلب المستخدم ذلك صراحةً (على سبيل المثال من خلال setDataSize()) أو عند استدعاء إحدى طرق write عندما تكون سعة البيانات الحالية صغيرة جدًا.```cpp
status_t Parcel::continueWrite(size_t desired)
{
// SNIP: Validate desired size
// SNIP: Assign kernelFields & rpcFields from variant member of this class
// SNIP: Count number of objects (Binder handles and File Descriptors)
// that will be present after resize and assign to objectsSize
if (mOwner) {
// If the size is going to zero, just release the owner's data.
if (desired == 0) {
freeData();
return NO_ERROR;
}
// If there is a different owner, we need to take
// posession.
uint8_t* data = (uint8_t*)malloc(desired);
// SNIP: Check if malloc succeeded
binder_size_t* objects = nullptr;
if (kernelFields && objectsSize) {
objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
// SNIP: Check if calloc succeeded
// Little hack to only acquire references on objects
// we will be keeping.
size_t oldObjectsSize = kernelFields->mObjectsSize;
kernelFields->mObjectsSize = objectsSize;
acquireObjects();
kernelFields->mObjectsSize = oldObjectsSize;
}
// SNIP: rpcFields handling for non-/dev/binder Parcels
if (mData) {
memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
}
if (objects && kernelFields && kernelFields->mObjects) {
memcpy(objects, kernelFields->mObjects, objectsSize * sizeof(binder_size_t));
}
// ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
if (kernelFields) {
// TODO(b/239222407): This seems wrong. We should only free FDs when
// they are in a truncated section of the parcel.
closeFileDescriptors();
}
mOwner(mData, mDataSize, kernelFields ? kernelFields->mObjects : nullptr,
kernelFields ? kernelFields->mObjectsSize : 0);
mOwner = nullptr;
// SNIP: Allocation count tracking
// SNIP: Assign data and objects to this object
} else if (mData) {
// SNIP: Resize data owned by this instance of Parcel
} else {
// SNIP: Allocate initial data for currently empty Parcel
}
return NO_ERROR;
}
[عندما تم تقديم هذا التعليق](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/)، تم نقل استدعاء `closeFileDescriptors()` من `IPCThreadState::freeBuffer()` (الذي يتم استدعاؤه في الكود أعلاه عبر مؤشر الدالة `mOwner()`) إلى طريقة `continueWrite()`، لكن المنطق بقي كما كان من قبل. بعد كل شيء، `Parcel` هو جزء أساسي من IPC في أندرويد، وإذا كان IPC الأساسي يغلق واصفات الملفات التي لا ينبغي له إغلاقها، فستكون مشكلة واضحة.
مما يقودنا إلى الجزء المهم: متى يتم استخدام الكود أعلاه؟ يتم استخدامه عندما تقوم فئة `Parcel` بنقل ملكية البيانات المستلمة من برنامج تشغيل Binder (والتي توجد في ذلك الوقت في [`/dev/binder` `mmap`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567) ولا يمكن الكتابة عليها (أي محاولة لكتابة تلك الذاكرة ستؤدي إلى `SIGSEGV`))، أي أن `Parcel` إما أن تكون بيانات معاملة واردة (وسيطة `data` التي تم تمريرها إلى [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))) أو رد وارد (أي كائن `Parcel` الذي تم تمريره إلى استدعاء [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) كوسيطة `reply`، حيث يقوم `transact()` بتعيين مرجع داخل كائن `Parcel` ذلك)
من الناحية العملية، الحالة الوحيدة التي قد ندخل فيها كتلة `if (mOwner)` هي عندما [يستدعي النظام `setDataSize(0)` لتحرير بيانات المعاملة](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567)، ولكن في هذه الحالة سندخل أيضًا `if (desired == 0)` الذي يقوم بالإرجاع المبكر. أثناء الاستخدام الشرعي للنظام، لا توجد حالة ندخل فيها مسار "إذا كان هناك مالك مختلف، نحتاج إلى الاستيلاء على الملكية"
# تفعيل مسار "الاستيلاء على الملكية"
في أحد استغلالاتي السابقة، أوضحت [حالة حيث يمكن لـ `createFromParcel()` في الواقع استدعاء `writeInt(0)` على `Parcel` الذي يفترض أن تقرأ منه](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects). بينما منع الإصلاح هناك تنفيذ أي طرق `createFromParcel()` غير `Intent` داخل `AccountManagerService`، إلا أن مسار `createFromParcel()` إلى `writeInt(0)` بقي محفوظًا.
للتلخيص، [داخل `PackageParser` لدينا الكود التالي](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759):```java
final Class<T> cls = (Class<T>) Class.forName(componentName);
final Constructor<T> 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 واحدة
وفي مكان آخر لدينا الكود التالي:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }
لذا، لتفعيل مسار "الاستحواذ"، نحتاج إلى استدعاء `readParcelable` بشكل اعتباطي على `Parcel` الذي تم تمريره إلى `onTransact()` كـ `data`. في هذا الاستغلال، أستخدم لذلك [نفس المسار الذي استخدمته سابقًا في مسار آخر](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them). لدي استدعاء `readParcelable` لـ `PackageParser$Activity.CREATOR.createFromParcel()`، والذي بدوره يقرأ اسم `PooledStringWriter` ويستدعي الباني الخاص به، وبعد ذلك تنتهي بيانات `Parcel`، لذا فإن `writeInt()` تحتاج إلى إعادة تخصيص `Parcel`، مما يدخل في مسار "الاستحواذ" الخاص بنا.
جدير بالذكر هنا، إذا لم تكن هناك نهاية لبيانات `Parcel` في تلك النقطة، فإن `writeInt()` ستحاول الكتابة فوق البيانات في مكانها، مما يؤدي في حالة البيانات المدعومة بـ `/dev/binder` `mmap` إلى حدوث `SIGSEGV`.
# مُعقّم واصفات الملفات
كانت فكرتي الأولية هي أن يقوم مسار "الاستحواذ" بإغلاق واصفات الملفات، وبعد ذلك في نهاية المعاملة سيتم إغلاق نفس الواصفات مرة أخرى، ولكن بين تلك الأحداث، سأضع واصف ملف آخر داخل `system_server` في معاملة أخرى، وفي وقت لاحق سأستعيد واصف الملف الخاص بي، حيث أن واصف الملف (FD) هذا يشير في تلك النقطة إلى ملف مختلف.
نجح هذا الأمر على جهاز المحاكاة الخاص بي باستخدام إصدار قديم من AOSP، ولكن بمجرد أن جربت مع إصدار أحدث، تم إيقاف هذه الخطة بواسطة [مُعقّم واصفات الملفات (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md)
على وجه الخصوص، [في `android-14.0.0_r29` تم توسيع تغطية FDSan لتشمل واصفات الملفات داخل `Parcel`](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/)
في الواقع، بعد أن غطى FDSan `Parcel`، لم أتمكن حتى من الوصول إلى استدعاء `closeFileDescriptors()` عندما كان `Parcel` يحتوي على واصفات ملفات. قبل ذلك الاستدعاء، هناك استدعاء لـ `acquireObjects();`، والذي يحصل على مراجع لمقابض `Binder` (والتي يتم تحريرها بواسطة استدعاء `mOwner()` لاحقًا داخل تلك الدالة)، ومع ذلك فإن `acquireObjects()` سيقوم أيضًا [بتعيين علامات FDSan لواصفات الملفات](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=175-178;drc=f4c9b48c19f1b040efb35932b322f47e7779cafe).```cpp
case BINDER_TYPE_FD:
if (obj.cookie != 0) { // owned
FdTag(obj.handle, nullptr, who);
}
الشيء هو أننا لقد قمنا بالفعل بوضع علامات على واصفات الملفات (FDs) المستلمة من النواة```cpp // In Parcel::ipcSetDataReference, which assigns this Parcel object to data from kernel (mOwner != null) if (type == BINDER_TYPE_FD) { // FDs from the kernel are always owned FdTag(flat->handle, nullptr, this); }
لذا، يؤدي استعمال `FdTag` المزدوج (دون إغلاق أو تغيير الوسم مع تحديد الوسم القديم المتوقع) إلى خطأ FDSan، مما يؤدي إلى إنهاء العملية، ولم نصل حتى إلى استدعاء `closeFileDescriptors()` بعد. وبما أن مسار "الاستيلاء" هو كود ميت أثناء الاستخدام العادي، فقد تمر هذه المشكلات دون ملاحظة
ومع ذلك، يمكننا رؤية الشرط `if (obj.cookie != 0)`. إذا كان خطأ، فهذا يعني أن واصف الملف الموجود داخل `Parcel` ليس مملوكًا فعليًا لذلك `Parcel`، وأنه من مسؤولية مستخدم `Parcel` إبقائهم مفتوحين طالما أن `Parcel` موجود. ولكن بما أن هذا `Parcel` جاء للتو من النواة، فإن قيم `cookie` تأتي في الواقع من العملية الأصلية وتعتبر غير ذات صلة عندما يكون لـ `Parcel` فعليًا `mOwner`. لكن مسار "الاستيلاء" لا يأخذ ذلك في الاعتبار وسيقوم فقط بنسخ قيم `cookie`
بجمع كل ذلك معًا، من خلال تعيين قيم `cookie` إلى صفر على جانب المرسل، يمكننا الحصول على `Parcel` يشير إلى واصفات ملفات تم إغلاقها، ولكنه لا يعتبر أيضًا أنه يملكها، مما يعني أنه لن يقوم بإغلاقها مرة أخرى. يتيح لنا ذلك تجنب التعثر مع FDSan، ولكنه يلغي أيضًا أي مسارات استغلال للإغلاق المزدوج
# حيل على جانب Java من Parcel
لا يزال من الممكن تمرير واصفات الملفات هذه إلى `Parcel` آخر (ثم إلى عملية أخرى)، ومع ذلك فإن طريقتنا في تحفيز إنشاء واصفات الملفات المعلقة هذه تتضمن بناء `PooledStringWriter` عبر الانعكاس، وبعد ذلك يتم طرح `ClassCastException` عندما نحاول `add()` إلى `ArrayList<IntentInfo>`
سنحتاج إلى:
* كن في نهاية `Parcel` الذي تم تمريره كوسيطة `data` إلى `onTransact()`
* قم ببناء `PooledStringWriter`، وبعد ذلك سيكون لدى Parcel واصفات ملفات معلقة، ولكن سيتم أيضًا طرح `ClassCastException`
* انتظر حتى يتم تخصيص واصفات الملفات التي نريد تسريبها داخل `system_server`
* احصل على واصفات الملفات من ذلك `Parcel` المنسوخة إلى `Parcel` آخر سيتم إرسالها إلى عمليتنا
من أجل تلبية جميع هذه المتطلبات، سأحتاج إلى استخدام بعض من الحيل القديمة والجديدة
## الحيل القديمة
دعنا نبدأ بمراجعة الحيل القديمة، معظمها تم وصفها بالفعل في استغلال [`LazyValue`-using-`Parcel`-after-`recycle()` الخاص بي](https://github.com/michalbednarski/LeakValue)
1. تقوم فئة [`RemoteViews` بإلغاء تسلسل `Bundle` المضمن مع تعيين `Parcel.ReadWriteHelper`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4). [عندما يتم تعيين `ReadWriteHelper`، لا يتم إلغاء تسلسل `Bundle`-s بشكل جشع بدلاً من كسول](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545). ومن الجدير بالذكر أيضًا أنه سيتم إلغاء تسلسل جميع `Bundle`-s الموجودة داخل ذلك `Bundle` ضمن `RemoteViews` بشكل جشع، وليس فقط تلك الموجودة مباشرة داخل `RemoteViews`
2. [إذا تم طرح `BadParcelableException` أثناء إلغاء تسلسل `Bundle` داخل `system_server`، فسيتم التقاط هذا `BadParcelableException` بصمت وسيتم مسح محتويات `Bundle`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e). لاحظ مع ذلك أن مشغل الكتابة في `createFromParcel` الخاص بنا يطرح `ClassCastException` والذي لن يتم التقاطه هنا
3. تقوم فئة [`ParceledListSlice` أثناء إلغاء التسلسل باستدعاء Binder خارجي محظور للكائن المحدد في البيانات المسلسلة](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364)، والذي يمكننا استخدامه لتعطيل تنفيذ إلغاء التسلسل
كل هذه ستكون مطلوبة الآن، ولكن هذا ليس كل شيء
## الحيل الجديدة
[AIDL هي أداة لتوليد تنفيذ واجهة RPC](https://developer.android.com/guide/components/aidl)، ومع ذلك بالإضافة إلى ذلك فهي قادرة أيضًا على توليد تطبيقات هيكل `Parcelable`
هذه الهياكل مسبوقة بالطول، لذا فإن الإصدارات المختلفة من نفس الهيكل متوافقة داخل النظام طالما لم تتم إضافة أي حقول في المنتصف (أي أن الإصدارات متوافقة إذا كان أحد الإصدارات هو بادئة للآخر)
دعنا نلقي نظرة على الكود الذي أنشأه AIDL لهيكل [`ReceiverInfo`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ReceiverInfo.aidl):```java
public final void readFromParcel(android.os.Parcel _aidl_parcel)
{
int _aidl_start_pos = _aidl_parcel.dataPosition();
int _aidl_parcelable_size = _aidl_parcel.readInt();
try {
if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
intent = _aidl_parcel.readTypedObject(android.content.Intent.CREATOR);
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
data = _aidl_parcel.readString();
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
extras = _aidl_parcel.readTypedObject(android.os.Bundle.CREATOR);
// SNIP: Other fields
} finally {
if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
throw new android.os.BadParcelableException("Overflow in the size of parcelable");
}
_aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
}
}
هذا الكود سيسمح لنا بتنفيذ الحيلتين المتبقيتين، وكلتاهما سنحتاجهما لتنفيذ إجراءات أخرى بعد تفعيل مسار "الاستيلاء" (take possession)، والذي يتطلب منا أن نكون في نهاية Parcel ويُحدث ClassCastException
أولاً، نقرأ الطول من Parcel، ونستخدمه لتخطي الحقول غير الموجودة في الإصدار الذي تم كتابته، وفي النهاية سنقوم بتعديل الموضع داخل Parcel بناءً على ذلك الطول. لا يُسمح لنا بالتحرك قبل موضع ذلك Parcelable الخاص بـ AIDL (التحرك بعد نهاية Parcel ممكن، لكنه لا يفعل أي شيء خطير في الواقع). ومع ذلك، يمكننا التحرك إلى منتصف كائن تمت قراءته بالفعل، لذلك يمكننا أثناء القراءة الأولى الوصول إلى نهاية Parcel، وإنشاء PooledStringWriter، ثم الرجوع إلى منتصف شيء داخل كائن ReceiverInfo
هذا يحل مشكلة "الوجود في نهاية Parcel"، ولا تزال هناك مشكلة ClassCastException. لكن هنا لدينا كتلة finally التي تتحقق قبل استدعاء setDataPosition() مما إذا كان هناك تجاوز. إذا كان هناك تجاوز، فسيتم طرح BadParcelableException. الآن، ماذا يحدث إذا قامت كتلة finally بطرح Exception بينما هناك استثناء آخر معلق؟ الاستثناء الذي يُطرح داخل finally له الأولوية، ويختفي الاستثناء السابق بصمت. الآن بدلاً من ClassCastException لدينا BadParcelableException، والتي تتجاهلها Bundle بشكل مفيد لتجنب استثناءات داخل system_server
ParcelableListBinderنحن نستخدم ParcelableListBinder الخاص بـ MediaSession لوضع Parcelable عشوائي داخل system_server ثم استرجاع ذلك الكائن لاحقًا
مؤخرًا كان هناك تصحيح لـ ParcelableListBinder يمنع ذلك تحديدًا
من وجهة نظر هذا الاستغلال، هذا التصحيح لا يمنعني في الواقع، لكني بحاجة إلى التعامل مع وجوده أو غيابه بشكل مختلف
إذا كان هذا التصحيح موجودًا، فسيتم إزالة العناصر غير QueueItem بصمت من القائمة التي يستقبلها system_server (لا تسبب استثناءات). نظرًا لأننا نحتاج إلى عنصر غير QueueItem للآثار الجانبية، يمكننا وضع عنصر غير QueueItem كعنصر أول ثم متابعته بـ QueueItem فعلي، وهو لا يسمح لنا بتنفيذ إلغاء تسلسل Parcelable عشوائي، ولكنه يحتوي على Bundle يمكن أن يحتوي على واصفات ملفات (سنريد تمريرها إلى عمليتنا)
إذا كان التصحيح غائبًا، فليس لدينا هذه العقبة، لكن لا يمكننا أيضًا استخدام نفس التدفق. لأنه في الحالة أعلاه سيكون لدينا قائمة تحتوي على كل من RemoteViews و QueueItem، لذلك سيرفض ParceledListSlice نقل القائمة ذات الأنواع المختلطة. في هذه الحالة، أحتاج إلى وضع واصفات الملفات المسربة داخل مثيل RemoteViews
الأكثر إثارة للاهتمام حول هذا التصحيح هو سبب وضعه، يذكر نص الالتزام "السماح للتطبيقات بالبدء من الخلفية"، وبينما لا يذكر Android Security Bulletin الكثير، يمكننا العثور على معلومات مفيدة في إدخال CVE، والذي يشير إلى حقل Notification.mAllowlistToken، والذي أثناء القراءة يمكن أخذه من حقل ثابت، والذي داخل system_server هو رمز يسمح ببدء الأنشطة في الخلفية ولاحقًا سيتم كتابة ذلك الرمز في Notification.writeToParcel(). هل يعني هذا أن جميع الحالات التي يقوم فيها system_server بإلغاء تسلسل Parcelable عشوائي وإرساله مرة أخرى إلى التطبيق أصبحت الآن ثغرات؟ على أي حال، هذا مجرد تفكير الآن، في هذا الاستغلال أريد أن أفعل المزيد على أي حال
أعتقد أن هذا الاستغلال يحتوي على أكثر سلسلة أدوات Parcelable تعقيدًا قمت بها على الإطلاق:
RemoteViews (1)
ReflectionAction (2)
Bundle
Parcelable[] (3)
ReceiverInfo (للبحث، 4 و 10)
Intent
ComponentName (الجزء أ، 5)
ParceledListSlice (11)ParcelableParcel أو QueueItem (12)Bundle (للالتقاط، الجزء ب، 6)
ReceiverInfo (لإعادة الطرح، 7)
Bundle (8)
تشير التعليقات التوضيحية "الجزء أ" و "ب" في القائمة أعلاه إلى الكتل بين تعليقات "START A"/"END A"/"START B"/"END B" في صف FdLeaker.java الخاص بي، وتشير الأرقام إلى النقاط في القائمة أدناه
تصف الشجرة أعلاه التسلسل الهرمي من منظور المرسل، ومن منظور المستقبل يبدو الأمر مختلفًا بعض الشيء:
ParcelableListBinder، الكائن الخارجي هو RemoteViewsRemoteViews يوجد Bundle متداخل. سيقوم RemoteViews بتعيين Parcel.ReadWriteHelper لذلك ستتم قراءة هذا Bundle وجميع Bundle-s بداخله بفارغ الصبر. هذا ضروري لأننا بخلاف ذلك لن نتمكن من تنفيذ readParcelable عشوائي من ReceiverInfo.readFromParcel()Parcelable[] هو مجرد غلاف مناسب هنا لتجميع جميع Parcelable-s التي أضعها داخل RemoteViewsدعنا نلقي نظرة على الهجوم أعلاه من منظور عالي المستوى:
system_server، يتم تذكر مرجع إليه ويتم إغلاق واصف الملفsystem_server لفتح واصف ملف مختلفهناك قيود كبيرة على هذا الهجوم: لا يمكننا الحصول على واصفات ملفات تم فتحها قبل بدء هجومنا
ومع ذلك، لا يزال هناك بعض الأشياء المفيدة التي يمكننا القيام بها
InputChannelلكي نكون صادقين، هذا هو النوع الوحيد من الاستغلال الذي تمكنت من جعله يعمل دون افتراضات إضافية
أحداث الإدخال، أي الأحداث من شاشة اللمس ولوحة المفاتيح، يتم استقبالها بواسطة التطبيق من خلال مقبس UNIX من system_server. عند بدء Activity أو إضافة نافذة جديدة إلى النظام، يتم إنشاء InputChannel جديد، مما يعني بدوره أنه يتم إنشاء زوج مقابس UNIX و يتم إرسال أحد الطرفين إلى التطبيق ويستخدم الطرف الآخر بواسطة system_server لإرسال الأحداث
تخطيط الهياكل المرسلة عبر هذه المقابس محدد جيدًا (يجب أن تكون متوافقة بين العمليات 32 بت و 64 بت) ويبدو أن لا شيء ينزعج من أرقام التسلسل غير المتوقعة. أيضًا يبدو أن InputChannel هو المقبس الوحيد المخصص داخل system_server بعد استدعاء startActivity()، لذلك يمكن تحديد أي واصف ملف هو مقبس جانب الخادم لـ InputChannel بسهولة

بينما هذا مثال بسيط، يمكننا أيضًا الموافقة على مطالبات الأذونات أو تثبيت التطبيقات، أو تمكين Media Projection أو Accessibility Service
zygote أثناء الإقلاعهذا نظري إلى حد كبير، تمكنت من تنفيذ هذا الهجوم على محاكي بطيء، لكن على جهاز حقيقي كانت نافذة السباق صغيرة جدًا
يفتح system_server اتصالاً بـ /dev/socket/zygote مرة واحدة فقط عند بدء التشغيل، وبعد ذلك يتم إرسال جميع الطلبات باستخدام هذا الاتصال
أثناء إقلاع system_server، يتم نشر MediaSessionService (الذي يُستخدم لإرسال واستقبال Parcelable-s من/إلى system_server) في servicemanager قبل إنشاء الاتصال بـ zygote
لذلك، من الناحية النظرية، من الممكن أن يقوم التطبيق بإطلاق عملية ثانوية، وجعل system_server يتعطل، ثم من تلك العملية الخلفية تنفيذ الهجوم أثناء بدء تشغيل system_server
zygote من خلال SensorServiceهناك خطأ آخر وجدته، إليك طريقة SensorService::createSensorDirectConnection()```cpp
sp SensorService::createSensorDirectConnection(
const String16& opPackageName, int deviceId, uint32_t size, int32_t type, int32_t format,
const native_handle *resource) {
// SNIP: Reject direct connections when sensor privacy is enabled
// SNIP: Irrelevant parameter checks
// check specific to memory type
switch(type) {
case SENSOR_DIRECT_MEM_TYPE_ASHMEM: { // channel backed by ashmem
if (resource->numFds < 1) {
ALOGE("Ashmem direct channel requires a memory region to be supplied");
android_errorWriteLog(0x534e4554, "70986337"); // SafetyNet
return nullptr;
}
// SNIP: Further validation for SENSOR_DIRECT_MEM_TYPE_ASHMEM
}
case SENSOR_DIRECT_MEM_TYPE_GRALLOC:
// no specific checks for gralloc
break;
default:
ALOGE("Unknown direct connection memory type %d", type);
return nullptr;
}
native_handle_t *clone = native_handle_clone(resource);
if (!clone) {
return nullptr;
}
native_handle_set_fdsan_tag(clone);
sp<SensorDirectConnection> conn;
int channelHandle = 0;
if (deviceId == RuntimeSensor::DEFAULT_DEVICE_ID) {
// SNIP: usual case where sensor belong to this device (not app streaming)
} else {
auto runtimeSensorCallback = mRuntimeSensorCallbacks.find(deviceId);
if (runtimeSensorCallback == mRuntimeSensorCallbacks.end()) {
ALOGE("Runtime sensor callback for deviceId %d not found", deviceId);
} else {
int fd = dup(clone->data[0]);
channelHandle = runtimeSensorCallback->second->onDirectChannelCreated(fd);
}
}
// SNIP: Return connection
}
لدينا استدعاء `dup(clone->data[0])`. المتغير `clone` هو `native_handle_t` تم استلامه من عملية بعيدة. يحتوي المقبض الأصلي على عدد معين من واصفات الملفات (FDs) وعدد معين من الأعداد الصحيحة العادية داخل البيانات، ويجب على مستخدم المقبض الأصلي التحقق من هذه الأعداد من خلال [النظر إلى `numFds` و `numInts`](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e) قبل الوصول إلى `data`.
لدينا هنا أيضًا قسم "التحقق الخاص بنوع الذاكرة" الذي يتحقق من ذلك، بالنسبة لـ `SENSOR_DIRECT_MEM_TYPE_ASHMEM` يتم التحقق هنا، بينما بالنسبة لـ `SENSOR_DIRECT_MEM_TYPE_GRALLOC` فإن تنسيق المقبض الأصلي خاص بالجهاز ولا يمكن التحقق منه هنا. الشيء هو أنه بالنسبة لـ "مستشعرات وقت التشغيل" يتم دائمًا معاملة النوع على أنه `SENSOR_DIRECT_MEM_TYPE_ASHMEM`، ولكن يمكننا تحديد `SENSOR_DIRECT_MEM_TYPE_GRALLOC` لتجاوز التحقق في هذه الحالة.
ومع ذلك، لا يمكن الوصول إلى هذا الكود إلا إذا كانت "مستشعرات وقت التشغيل" موجودة. لست متأكدًا من الحالة التي يحدث فيها ذلك بالفعل، أعتقد أنها عندما يستخدم المستخدم "بث التطبيقات القريبة" (؟)
ولكن للاختبار، أقوم بإضافة فئة صغيرة تسمح بتسجيل [`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary)، يمكنك استخدامها من خلال```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'
بعد ذلك، يصبح من الممكن تنفيذ الكود بمعرف المستخدم (uid) الخاص بالنظام (system) على الجهاز الإنتاجي

PackageParser$Activity (9)
PooledStringWriterReceiverInfo الخارجي له طول محدد يقع في منتصف بياناته، لكن أثناء القراءة هذا غير معروف بعد ويتم قراءة Intent منه بشكل طبيعيreadString الذي يقوم به static ComponentName.readFromParcel() (باستخدام ComponentName لأنه يستخدم readString UTF-16 بغض النظر عن إصدار Android، سيظل هذا readString يعيد null لأن تلك السلسلة تتداخل مع كائنات Binder، لذلك بينما تخطى بناء ComponentName البيانات المخفية عن هذا التمرير، لم يتم إنشاء كائن ComponentNameBundle المتداخل الثاني، في نهاية إلغاء تسلسل هذا Bundle سيتم التقاط BadParcelableExceptionReceiverInfo الثاني. هذا ReceiverInfo له طول محدد كـ Integer.MAX_VALUE وبالتالي سيقوم بطرح BadParcelableException في كتلة finally، متجاهلاً ClassCastException بصمتBundle الثالث. هذا فقط للتمكن من الوصول إلى readParcelable عشوائي من ReceiverInfo، والذي يمكننا فعله الآن لأننا داخل RemoteViews لذلك تتم قراءة Bundle بفارغ الصبرPackageParser$Activity + PooledStringWriter تؤدي إلى استدعاء writeInt(0) على Parcel الذي يتم قراءته منه. نظرًا لأننا كنا في نهاية Parcel، يجب على writeInt() توسيع سعة Parcel، مما يؤدي إلى تفعيل مسار "الاستيلاء". تؤدي هذه المجموعة أيضًا إلى ClassCastException، والتي يتم ابتلاعها كما هو موضح في الخطوتين 7 و 6 أعلاهReceiverInfo الخارجي، يسعى ReceiverInfo إلى الموضع وفقًا للطول في رأسه ويسقط داخل البيانات التي كانت سابقًا داخل ComponentName، والتي تتم قراءتها كعناصر تالية في Parcelable[]ParceledListSlice الذي يقوم بإجراء معاملة Binder محظورة إلى عمليتي. في هذه المرحلة، تم إغلاق واصفات الملفات المحددة في هذا Parcel، لكن لا شيء مثير للاهتمام قد حل محلها بعد. بينما ينتظر إلغاء التسلسل هذا العودة من هذا الاستدعاء، يمكنني جعل النظام يفتح بعض واصفات الملفات المثيرة للاهتمام والتي سيتم إرسالها إلي بعد ذلكParcelableListBinder يقوم بتصفية العناصر أم لا
أ. إذا كان ParcelableListBinder لا يقوم بتصفية العناصر، يتم حفظ واصفات الملفات داخل ParcelableParcel، والذي يشبه Bundle في نسخ بيانات Parcel حرفيًا باستخدام Parcel.appendFrom()، لكن ليس لديه منطق hasReadWriteHelper() الخاص لذلك يفعل ذلك على الرغم من وجوده تحت Bundle الخاص بـ RemoteViews
ب. إذا كان ParcelableListBinder يقوم بتصفية العناصر، فهذه هي نهاية RemoteViews، يتم تجاهل كائن RemoteViews بواسطة ParcelableListBinder، لكن هذا مقبول بالنسبة لي حيث حدثت الآثار الجانبية بالفعل. العنصر التالي الذي يستقبله ParcelableListBinder هو QueueItem، الذي يحتوي على MediaDescription، والذي بدوره يحتوي على Bundle، وهو المكان الذي تُحفظ فيه واصفات الملفات المسربة الخاصة بي