Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
LeakValue — استغلال الثغرة CVE-2022-20452، تصعيد الامتيازات على Android من تطبيق مثبت إلى تطبيق النظام (أو تطبيق آخر) عبر LazyValue باستخدام Parcel بعد recycle() | Kitploit
أدوات/GitHubGitHub/michalbednarski/leakvalue
أمان أندرويدتصعيد الامتيازاتتحليل الثغرات الأمنيةالاستغلالأمن الجوالاستغلال الملفات الثنائية
GitHubmichalbednarski/leakvalue

LeakValue

استغلال الثغرة CVE-2022-20452، تصعيد الامتيازات على Android من تطبيق مثبت إلى تطبيق النظام (أو تطبيق آخر) عبر LazyValue باستخدام Parcel بعد recycle()

عرض المستودع
34765منذ 3 سنواتتمت المراجعة من قبل Kitploit

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

يُقدّم Android 13 العديد من التحسينات بهدف تعزيز آلية تسلسل Parcel.

إليك عرض تقديمي من فريق أمان وخصوصية Android حول التحسينات التي تم إجراؤها

هذا رائع، فهو بالتأكيد يزيل العديد من الثغرات أو يجعلها غير قابلة للاستغلال.

كما يصفون كسر استغلالي السابق، الذي يسمح للتطبيقات بتحميل شفرتها إلى تطبيقات أخرى (بما في ذلك تطبيقات النظام)

لكنني عدت الآن باستغلال جديد يحقق نفس الشيء، وإن كان بطريقة مختلفة.

يعتمد على الثغرات التالية التي تم تقديمها أثناء تقوية Parcel المذكورة أعلاه:

  • CVE-2022-20452 (نشرة, تصحيح)
  • CVE-2022-20474 (نشرة, تصحيح)

لقطة شاشة لتطبيق يعرض نصًا. Title: LeakValue. Main text: Created 6 ValueLeaker-s. Locking ActivityTaskManagerService. Locked ActivityTaskManagerService. Unlocking ActivityTaskManagerService. Unlocked ActivityTaskManagerService. leakedBinders=[android.os.BinderProxy@f06702e]. Leaked interface: android.app.IApplicationThread. Requesting code execution. Shellcode has been executed in uid=1000 pid=6904 packageName=com.android.settings uid=1000(system) gid=1000(system) groups=1000(system),1007(log),1065(reserved_disk),1077(external_storage),3001(net_bt_admin),3002(net_bt),3003(inet),3007(net_bw_acct),9997(everybody) context=u:r:system_app:s0. في أسفل الشاشة زران: START و MANUAL TESTING

(أيضًا logcat من تنفيذ التطبيق، الاستغلال صاخب في السجلات)

مقدمة إلى أخطاء عدم التوافق بين Parcel و Parcelable

فئة Parcel في Android هي أساس التواصل بين العمليات

يمكن للكائنات تنفيذ واجهة Parcelable للسماح بكتابتها إلى Parcel، على سبيل المثال (منسوخ من AOSP):```java public class UsbAccessory implements Parcelable { public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public UsbAccessory createFromParcel(Parcel in) { String manufacturer = in.readString(); String model = in.readString(); String description = in.readString(); String version = in.readString(); String uri = in.readString(); IUsbSerialReader serialNumberReader = IUsbSerialReader.Stub.asInterface( in.readStrongBinder());

root@kitploit:~
        return new UsbAccessory(manufacturer, model, description, version, uri,
                serialNumberReader);
    }
};

public void writeToParcel(Parcel parcel, int flags) {
    parcel.writeString(mManufacturer);
    parcel.writeString(mModel);
    parcel.writeString(mDescription);
    parcel.writeString(mVersion);
    parcel.writeString(mUri);
    parcel.writeStrongBinder(mSerialNumberReader.asBinder());

} }

root@kitploit:~
لاحظ أن `Parcel` يخزن داخليًا الموضع الذي يتم فيه الكتابة أو القراءة، وأن `readString()` يحلل البيانات إلى `String` بالإضافة إلى تقدم الموضع. يمكن الحصول على هذا الموضع أو تعيينه يدويًا من خلال [`dataPosition()`](https://developer.android.com/reference/android/os/Parcel#dataPosition())/[`setDataPosition()`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)). يجب على تطبيقات واجهة `Parcelable` التأكد من أن `writeToParcel` و `createFromParcel` يكتبان/يقرآن نفس مقدار البيانات، وإلا ستحصل جميع القراءات اللاحقة على بيانات من إزاحات خاطئة.

يمكن أن يحتوي [`Bundle`](https://developer.android.com/reference/android/os/Bundle) (خريطة مفتاح-قيمة يمكن إرسالها عبر العمليات) على [مجموعة متنوعة من الكائنات التي يمكن كتابتها إلى `Parcel` عبر `writeValue()`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=1792-1937). عند قراءة محتويات `Bundle` من `Parcel`، يمكن قراءة أي فئة `Parcelable` متاحة في النظام هناك.

يؤجل `Bundle` التحليل الفعلي للمحتويات عن طريق كتابة طول البيانات المجمعة بالكامل في `Parcel` ثم [نسخ الجزء ذي الصلة من `Parcel` الأصلي إلى `Parcel` ثانوي مخزّن في `mParcelledData`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=1675-1683) (وهذا يسمح على سبيل المثال لـ [`Activity.onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle)) بتوفير كائنات `Parcelable` غير متوفرة في `system_server`، ويتم تمرير `Bundle` بالكامل إلى `system_server` وإعادته كما هو دون تحليل المحتويات).

ولكن بمجرد الوصول إلى أي قيمة في `Bundle`، [تم إلغاء تجميع](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=227-313) جميع القيم داخل `Bundle` و [تم تحليل كل زوج مفتاح-قيمة موجود](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=3613-3632). إذا احتوت هذه الخريطة على `Parcelable` لديه طرق `writeToParcel` و `createFromParcel` غير متوازنة، ثم تم إعادة توجيه هذا `Bundle` لاحقًا إلى عملية أخرى، فقد ترى تلك العملية الأخرى محتويات مختلفة من `Bundle`. وهذا جعل جميع [حالات عدم التطابق هذه في الفئات المتاحة في النظام ثغرات أمنية](https://github.com/michalbednarski/ReparcelBug) حيث توجد [أماكن في النظام يتم فيها فحص `Bundle` ليكون آمنًا](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=5037-5046) ثم يتم إعادة توجيهه إلى عملية أخرى.

في هذه المذكرة، أسمي مثل هذا `Bundle` الذي يظهر محتويات واحدة ثم أخرى بعد إعادة التوجيه بـ `Bundle` ذاتي التغيير.

شيء مهم آخر هنا هو أنه بالإضافة إلى البايتات فقط (سلاسل، أرقام، كائنات مكونة مما سبق)، يمكن أن يحتوي `Parcel` أيضًا على واصفات ملفات و `Binder`s. `Binder`s هي كائنات يمكن من خلالها إجراء استدعاء RPC، أي أن عملية تنشئ كائن `Binder` وتتجاوز [`onTransact()` method](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). ثم يتم تمرير `Binder` إلى عملية أخرى، في الكود المثال أعلاه يمكنك رؤية استدعاءات `read`/`writeStrongBinder()` المستخدمة لقراءته وكتابته إلى `Parcel`. في العملية الأخرى، عند استخدام `readStrongBinder()` يتم إنشاء كائن `BinderProxy` (مخفي خلف [واجهة `IBinder`](https://developer.android.com/reference/android/os/IBinder)). ثم يمكن لتلك العملية الأخرى استدعاء [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) على ذلك الكائن وفي الكائن الأصلي سيتم تنفيذ `onTransact()`. عادةً، لا يقوم الشخص بكتابة `transact()`/`onTransact()` يدويًا بل [يستخدم AIDL بدلاً من ذلك](https://developer.android.com/guide/components/aidl).

# دخول `LazyValue`، نهاية `Bundle` ذاتي التغيير

نظرًا لوجود العديد من حالات الفئات ذات عدم التطابق في `writeToParcel`/`createFromParcel` في الماضي، يحل Android 13 مشكلة وجود أي فئة من هذا القبيل في أي مكان في النظام مما يسمح ببناء `Bundle` ذاتي التغيير من خلال [تقديم `LazyValue`](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/)

الآن، عند استخدام `writeValue`، إذا كانت القيمة المكتوبة ليست بدائية، [يتم كتابة طول القيمة في `Parcel` أيضًا](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2331-2344;drc=03c34f57c05feecfb090de3917787f049cb5f804)

عندما يستخدم التطبيق العادي مباشرة `Parcel.readValue()`، [يحدث كل شيء كما كان من قبل باستثناء طباعة تحذير إذا كان `length` المقروء من `Parcel` لا يتطابق مع حجم البيانات المقروءة فعليًا](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4330-4348;drc=03c34f57c05feecfb090de3917787f049cb5f804) (لاحظ مع ذلك أن [`Slog.wtfStack` لا يرمي استثناءً أبدًا](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/Slog.java;l=108-116;drc=23c7543b8e608ebcbb38b952761b54bb56065577))

أما `Bundle`، فيستخدم الآن [`Parcel.readLazyValue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4350-4420;drc=03c34f57c05feecfb090de3917787f049cb5f804) بدلاً من ذلك.

لنلقي نظرة أقرب على كيفية عمله: في فئة `LazyValue` لدينا [تعليق جيد يشرح هيكل بيانات `LazyValue` داخل `Parcel`](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

mSource هو مرجع إلى Parcel الأصلي الذي تم استدعاء readLazyValue() عليه

mPosition و mLength يصفان موقع بيانات LazyValue كاملة في Parcel الأصلي، بما في ذلك type و length

"length" (بدون "m" في البداية) يشير إلى قيمة الطول كما تم كتابتها إلى Parcel ويستثني الرأس (type و length)

إذن هذا ما يحدث عندما يأخذ شخص (سواء النظام أو التطبيق) قيمة من Bundle تمت قراءتها من Parcel:

  1. يستخدم المتصل إحدى طرق get*() المتعددة لفئة Bundle، على سبيل المثال طريقة getParcelable() الجديدة مع وسيط type (سيكون التدفق هو نفسه لكل من الطرق الجديدة والقديمة، فقط الطرق الجديدة تضمن أن وسيط clazz ليس null بينما الطرق القديمة تضبطه على null)
  2. يتم استدعاء unparcel()، والتي ستتحقق مما إذا كان هذا Bundle لديه mParcelledData (بمعنى أنه تمت قراءته من Parcel ولكن لم يتم الوصول إلى أي قيمة بعد ولم يتم فك أسماء المفاتيح بعد، إذا لم يكن الأمر كذلك، انتقل إلى الخطوة 5.)
  3. تقوم unparcel() بتفويض المهمة إلى unparcel(boolean itemwise)، والتي ، حيث يتم تعيين إلى ، وهي نسخة من التي أنشأها ، ويتم تعيين معامل على للإشارة إلى أن الممررة مملوكة لـ وأنه من المسموح استدعاء عليها

إذا كان Bundle يتم تمريره للأمام بينما لا يزال يحتوي على LazyValue (بمعنى أن هذه القيمة المعينة لم يتم الوصول إليها، ولكن تم الوصول إلى قيمة أخرى من ذلك Bundle (أي تم استدعاء unparcel()، ولكن لم يتم استدعاء LazyValue.apply() لهذا العنصر)):

  1. يتم اكتشاف LazyValue بواسطة Parcel.writeValue() ويتم تفويض الكتابة إلى LazyValue.writeToParcel()
  2. LazyValue.writeToParcel() تستخدم out.appendFrom(source, mPosition, mLength) لنسخ بيانات LazyValue كاملة من Parcel الأصلي (مرة أخرى، mPosition و mLength يشملان رأس LazyValue، لذلك هذا ينسخ أيضًا type و length من الأصلي)

Parcel.ReadWriteHelper و Parcel.readSquashed

(تفاصيل هذه الأمور ليست مهمة لهذا الاستغلال، الشيء الوحيد ذو الصلة هنا هو أن هذه الآليات موجودة)

ميزة أخرى مثيرة للاهتمام في Parcel هي القدرة الاختيارية على إزالة ازدواجية السلاسل النصية (Strings) والكائنات المكتوبة

يتم إزالة ازدواجية السلاسل النصية عن طريق تجاوز فئة Parcel.ReadWriteHelper: في الواقع Parcel.readString() تفوض إلى ReadWriteHelper والمساعد الافتراضي يقرأ String من Parcel مباشرة

يمكن للتطبيق البديل لـ Parcel.ReadWriteHelper استبدال استدعاءات readString بـ قراءة مجموعة من السلاسل النصية مسبقًا واستخدام readInt للحصول على فهارس السلاسل في المجموعة؛ ومع ذلك، لا يتم ذلك أبدًا مع Parcel التي يتحكم فيها التطبيق

Parcel تقدم طريقة hasReadWriteHelper()، والتي تسمح للمتصلين باكتشاف وجود آلية إزالة الازدواجية هذه وتعطيل الميزات غير المتوافقة معها

آلية إزالة الازدواجية الأخرى المتاحة في Parcel هي squashing:

  1. أولاً، يجب تمكين squashing باستخدام Parcel.allowSquashing()
  2. ثم، عند كتابة فئة تدعم squashing، فإنها أولاً تستدعي Parcel.maybeWriteSquashed(this). إذا أعادت هذه الطريقة true فهذا يعني أن الكائن قد تم كتابته بالفعل إلى هذا Parcel والآن تم فقط كتابة الإزاحة إلى بيانات الكائن السابقة في Parcel. وإلا (إما أن squashing غير ممكن أو أن هذه هي المرة الأولى التي يُكتب فيها هذا الكائن) فإن maybeWriteSquashed يكتب صفرًا كإزاحة للإشارة إلى أن الكائن ليس مضغوطًا (squashed) ويعيد false للإشارة إلى المتصل أنه يجب عليهم كتابة بيانات الكائن الفعلية
  3. عند القراءة، يتم استدعاء Parcel.readSquashed ويتم تمرير دالة القراءة الفعلية كـ lambda. يتحقق readSquashed مما إذا كانت الإزاحة التي كتبها تشير إلى أن مثيلًا آخر من الكائن قد تمت قراءته سابقًا: إذا كان الأمر كذلك، يتم إرجاع الكائن الذي تمت قراءته سابقًا، وإلا يتم استدعاء lambda المقدمة لقراءته الآن

استخدام بعد إعادة تدوير Parcel (Use-after-Parcel.recycle())

في جانب Java، يمكن إعادة تدوير كائنات Parcel في مجموعة (pool)، أي بمجرد الانتهاء من Parcel يمكنك استدعاء recycle() عليها وفي المرة التالية التي يستدعي فيها شخص ما Parcel.obtain() سيحصلون على Parcel المعاد تدويرها سابقًا. هذا يسمح بتقليل عدد تخصيصات الكائنات وجمع القمامة اللاحق (Garbage Collection)

من ناحية أخرى، تجلب إدارة الذاكرة اليدوية هذه إمكانية حدوث أخطاء شبيهة بـ Use-After-Free في Java (وإن كان مع أمان النوع، على عكس Use-After-Free المعتاد في لغة C)

كما لوحظ أعلاه، ينشئ Bundle نسخة من Parcel ولن يستدعي Parcel.recycle() إذا كان LazyValue موجودًا، ومع ذلك هذا ليس هو الحال إذا كان Parcel.hasReadWriteHelper() هو true، في هذه الحالة:

  1. يتم استدعاء initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle);، وهذا يعني أن Bundle لن يعيد تدوير Parcel لأنها لا تزال مملوكة للمتصل، ومع ذلك هذا ينشئ LazyValues التي تشير إلى Parcel الأصلي ويمكن أن تعمر أكثر من العمر الافتراضي لـ Parcel الأصلي
  2. لذلك، الشيء التالي بعد ذلك هو استدعاء unparcel(/* itemwise */ true)، والذي سوف يستخدم getValueAt() على جميع العناصر لاستبدال جميع LazyValues الموجودة في Bundle بالقيم الفعلية

الآن، هل يمكننا جعل هذه LazyValues تبقى على قيد الحياة بعد الخطوة 2. وتحويل هذا السلوك إلى Use-After-Recycle؟

إذا فشل إلغاء التسلسل (على سبيل المثال، لم يتم العثور على الفئة التي تحمل الاسم المحدد داخل Parcel)، يتم طرح BadParcelableException ثم يتم التقاطها بواسطة getValueAt(). إذا كان الحقل الثابت BaseBundle.sShouldDefuse هو true، فلن يتم رفع استثناء ويستمر التنفيذ تاركًا Bundle يحتوي على LazyValue يشير إلى Parcel الأصلي. يشير sShouldDefuse إلى أن القيم غير المتوفرة من Bundle لا ينبغي أن تسبب استثناءات في عملية معينة ويتم تعيينه على true في

إذا تم إعادة تدوير Parcel الأصلي وبعد ذلك سيتم كتابة Bundle المقروء منه إلى Parcel آخر، سيتم نسخ محتويات Parcel الأصلي إلى Parcel الوجهة، ولكن في تلك النقطة يمكن إعادة استخدام Parcel الأصلي لشيء آخر ويمكن نسخ بيانات من عملية IPC غير مرتبطة

حسنًا، ولكن كيف نجعل Parcel.hasReadWriteHelper() يكون true بينما يتم إلغاء تسلسل Bundle الذي نقدمه؟

اتضح أن فئة RemoteViews (التي تُستخدم عادةً على سبيل المثال لتمرير الأدوات (widgets) إلى الشاشة الرئيسية) تقوم بتعيين ReadWriteHelper صراحة عند قراءة Bundles المتداخلة فيها. هذا ReadWriteHelper لا يقوم بإزالة ازدواجية String وهو موجود فقط ليتسبب في أن يتخطى Bundle نسخ البيانات إلى Parcel ثانوية. السبب وراء ذلك هو أن RemoteViews تتيح squashing من أجل إزالة ازدواجية كائنات ApplicationInfo المتداخلة فيها، ولكن هذا قد يتسبب أيضًا في ضغط كائنات ApplicationInfo الموجودة داخل Bundle، لذلك لا يمكن تأجيل قراءة هذا لأن تلك الكائنات المضغوطة ستفشل في فك الضغط

وضع Parcelables في system_server واسترجاعها

لذا نريد الآن أن يقوم system_server بقراءة RemoteViews الخاص بنا الذي يحتوي على Bundle يحتوي على LazyValue يفشل في إلغاء التسلسل، ثم لاحقًا (في معاملة IPC أخرى لـ Binder) إعادة ذلك الكائن إلينا

ربما يمكن القيام بذلك من خلال وسائل مشروعة، مثل تسجيل أنفسنا كمضيف لعناصر واجهة التطبيق (app widget host) (ولكن ذلك سيتطلب تفاعل المستخدم لمنحنا الإذن) أو نشر Notification مع تعيين contentView (ولكن ذلك سيتسبب في تفاعل مع عمليات أخرى و/أو سيكون مرئيًا للمستخدم وفضلت تجنب كليهما)

قررت بدلاً من ذلك إنشاء MediaSession واستدعاء setQueue(List<MediaSession.QueueItem> queue) عليه لإرسال الكائن إلى system_server ثم استرجاعه لاحقًا من خلال طريقة List<MediaSession.QueueItem> getQueue() الخاصة بـ MediaController (التي يمكن الحصول عليها من خلال MediaSession.getController()). بينما لا تبدو هذه الطرق وكأنها يمكن أن تقبل RemoteViews، إلا أنها تفعل ذلك في الواقع بفضل محو النوع في Java (Java Type Erasure) وحقيقة أنها تحت الغطاء يتم تنفيذها باستخدام عمليات تسلسل عامة على List

ومع ذلك، أنا لا أستخدم هذه الطرق من SDK، بل أكتب بيانات معاملات Binder الأساسية يدويًا (لأنني بحاجة إلى كتابة وقراءة بيانات متسلسلة مشوهة لاحقًا)، لذا دعونا نلقي نظرة على كيفية عمل هذه الطرق

كان على كل من هاتين الطريقتين مراعاة حقيقة أن الحجم الإجمالي لقائمة الانتظار قد يتجاوز الحد الأقصى لحجم معاملة Binder لذلك يمكن تقسيم النقل إلى معاملات متعددة

إرسال "قائمة الانتظار" إلى system_server يتم عادةً على النحو التالي:

  1. MediaSession.setQueue() أولاً يستدعي ISession.getBinderForSetQueue()
  2. في جانب system_server، تقوم تلك الطريقة ببناء وإعادة كائن ParcelableListBinder
  3. بعد ذلك، MediaSession.setQueue() يستدعي ParcelableListBinder.send() والذي سيرسل محتويات القائمة إلى Binder المقدم، ربما عبر معاملات متعددة:
    • المعاملة الأولى تحتوي في البداية على العدد الإجمالي للعناصر التي ستكون في القائمة قبل محتويات الجزء الأول
    • ثم، لكل عنصر يتم نقله يتم كتابة 1 ويتم كتابة العنصر الفعلي من خلال (والذي يكتب اسم الفئة التي يتم إرسالها ثم يستدعي لإرسال البيانات)

من ناحية أخرى، استرجاع "قائمة الانتظار" يتم بشكل مختلف قليلاً:

  1. MediaController.getQueue() فقط يستدعي ISessionController.getQueue() ويفك تغليف ParceledListSlice المستلم
  2. في جانب system_server، getQueue() فقط يغلف mQueue في ParceledListSlice ويعيده
  3. منطق التقسيم عبر معاملات متعددة بأكمله موجود داخل طرق ParceledListSlice.writeToParcel() و createFromParcel()، على وجه الخصوص، writeToParcel() عند الوصول إلى حد الحجم الآمن يكتب كائن يسمح باسترجاع الأجزاء التالية

أما عن سبب اختلاف هذه الأمور: هناك جهد مستمر للتأكد من أن system_server لا يقوم باستدعاءات Binder متزامنة صادرة لتطبيقات أخرى، لأن هذه الاستدعاءات إذا تعلقت قد تعلق system_server بأكمله. هذا يعني أن system_server لا ينبغي أن يستقبل ParceledListSlices. بينما يوجد كود يحذر من المعاملات المتزامنة الصادرة من system_server، إلا أنه لم يتم تطبيقه بشكل إجباري بعد لأنه لا تزال هناك حالات يقوم فيها system_server بمثل هذه الاستدعاءات، على سبيل المثال من خلال استقبال ParceledListSlice فعليًا

اختيار هدف التسريب

لدينا الآن الأوليات اللازمة لجعل system_server يقوم بـ parcel_that_will_be_sent_to_us.appendFrom(some_recycled_parcel, somewhat_controlled_position, controlled_size)

يمكننا إما محاولة سحب بيانات Parcel من النظام بشكل عشوائي أو ترتيب الأمور لأخذ شيء محدد

هناك الاعتبارات التالية:* عند استدعاء Parcel.recycle()، يتم مسح محتويات ذلك Parcel. وهذا يعني أن Parcel الذي نرغب في نسخ البيانات منه يجب ألا يتم recycle()، مما يعني تقريبًا أنه لا يمكننا أخذ البيانات من معاملة Binder التي انتهت.

  • بدلاً من ذلك، يمكننا أخذ البيانات من بعض Parcel في بعض Bundle موجود في النظام (وهذا يشمل Intent extras و savedInstanceState في Activity). عادةً لا يتم recycle() لهذه الكائنات على الإطلاق (يتم تنظيفها بواسطة Garbage Collector ولا تعود إلى المجمع، وعندما ينضب المجمع يقوم Parcel.obtain() بإنشاء كائنات Parcel جديدة. بالطبع، كائنات Parcel التي نحتفظ بمرجع لها لن يتم GC عليها، حتى لو لم يكن لدى النظام استخدام آخر لها).
  • تستخدم كائنات Parcel المستخدمة في معاملات Binder الواردة مجمعًا منفصلاً عن كائنات Parcel الأخرى في النظام. عند إجراء معاملة Binder صادرة، يقوم Bundle بنسخ البيانات إلى ثانوي، أو يستخدم التطبيق لأغراضه الخاصة، فإنها تستدعي . من ناحية أخرى، عند وجود معاملة واردة، يقوم ، والذي . في كلتا الحالتين يتم استخدام بعد ذلك ويعتني . وهذا يعني أن الاستغلال يجب أن يكون لديه يقرأ من ينتمي إلى نفس المجمع الذي نرغب في تسريب البيانات منه. قبل أن أقرر أي متغير معين، قمت بكتابة كليهما، لذا يمكنك العثور على طريقتي و في

في النهاية، قررت محاولة انتزاع IApplicationThread Binder، الذي يتم إرساله بواسطة التطبيق إلى system_server عند بدء تشغيل عملية التطبيق ويستخدمه system_server لإخبار التطبيق بالمكونات التي يجب تحميلها

عند بدء تشغيل عملية التطبيق لأول مرة، فإن أحد أول الأشياء التي يفعلها هو إرسال IApplicationThread إلى system_server من خلال استدعاء attachApplication() وهذه هي المعاملة التي سأستخلص منها هذا Binder. هناك أماكن أخرى يتم فيها وضع IApplicationThread في Parcel، مثل تمريره لتحديد هوية المتصل بواسطة النظام عند بدء النشاط (لكن لم يكن لدي سيطرة كبيرة على متى يفعل التطبيق الهدف ذلك) أو يتم إرساله بواسطة النظام إلى التطبيق كـ جزء من إدارة دورة حياة Activity (ولكن هذا يتم في معاملة oneway صادرة من system_server وفرص الفوز في سباق ضد Parcel.recycle() ستكون ضئيلة)

ومع ذلك، فإن انتزاع Binder الذي يستقبله system_server أثناء معاملة attachApplication() هو أيضًا ليس بالأمر السهل وكانت هناك بعض المشكلات التي يجب التغلب عليها

إعادة لف Parcel

المشكلة الأولى في انتزاع IApplicationThread Binder من Parcel الذي يتم استقبال بيانات attachApplication() منه هي أن هذا Binder موجود في dataPosition() مبكرة/منخفضة جدًا، أقل بكثير من قيمة LazyValue في Bundle في RemoteViews

تتكون بيانات معاملة attachApplication() فقط من رأس RPC متبوعًا بـ IApplicationThread Binder. يتكون رأس RPC (المكتوب عبر Parcel.writeInterfaceToken()) من عدد قليل من ints واسم الواجهة، في هذه الحالة "android.app.IActivityManager"

وفي الوقت نفسه، لقراءة Bundle المضمن في RemoteViews سنحتاج إلى تجاوز على الأقل (تم تخطي بعض العناصر الصغيرة):

  • علامة وجود العنصر لبدء readParcelable
  • اسم Parcelable: "android.view.RemoteViews"
  • كائن ApplicationInfo كبير موجود في RemoteViews (يجب أيضًا ألا يكون null وأن يكون له packageName غير null أو سيفشل RemoteViews.writeToParcel() عندما نحاول الحصول على هذا الكائن ليتم إرساله مرة أخرى)
  • وأخيرًا نصل إلى ، والذي ، والذي يقوم ببناء ، والذي بعد يقوم أخيرًا

الآن في Bundle، نحتاج فقط إلى وضع مفتاح String وتبدأ قراءة LazyValue، يتم تذكر الموضع في Parcel، ولكن في هذه المرحلة يكون الموضع بعيدًا عن الموضع الذي سيكون فيه IApplicationThread Binder

هل يمكننا عند الوصول إلى هذه النقطة إعادة لف الموضع في Parcel؟ بعبارة أخرى، هل يمكننا جعل Parcel.setDataPosition() يُستدعى بقيمة تشير إلى موضع سابق للموضع الحالي؟

اتضح أننا نستطيع، بفضل خطأ آخر في LazyValue. هذا هو الكود المستخدم لقراءته:```java public Object readLazyValue(@Nullable ClassLoader loader) { int start = dataPosition(); int type = readInt(); if (isLengthPrefixed(type)) { int objectLength = readInt(); int end = MathUtils.addOrThrow(dataPosition(), objectLength); int valueLength = end - start; setDataPosition(end); return new LazyValue(this, start, valueLength, type, loader); } else { return readValue(type, loader, /* clazz */ null); } }

root@kitploit:~
([الأصل في AOSP](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4376-4388;drc=03c34f57c05feecfb090de3917787f049cb5f804)، `LazyValue` constructor just assigns parameters to fields)

الشيء هو أن `MathUtils.addOrThrow()` يتحقق من التجاوز، [لكنه يتعامل تمامًا مع القيم السالبة](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/MathUtils.java;l=276;drc=290055a76e6ef80dd8ad7bc812d78f4fc0c5be86)

إذا حاولنا القيام بـ `Parcel.writeValue()` على `LazyValue` مع `mLength` سالب (مملوء من معامل `valueLength`) فإن [ذلك سيؤدي إلى رمي خطأ في `appendFrom()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4446;drc=03c34f57c05feecfb090de3917787f049cb5f804)، ولكن بما أننا أثناء قراءة `Bundle` مع `Parcel.hasReadWriteHelper()` بقيمة `true`، فإن جميع `LazyValue`s يتم فك تغليفها بعد القراءة وكان علينا أن نضع عمدًا `Parcelable` معيبًا بداخلها لجعلها تبقى كـ `LazyValue`. إذا وضعنا بيانات صالحة ومغلفة في الموضع الذي توجد فيه `LazyValue`، فسيتم فك تغليفها وكما ذكر سابقًا فإن الطول غير المتطابق سيؤدي فقط إلى ظهور رسالة في `logcat`. هذا الاستغلال المحدد يضبط النوع على `VAL_MAP` وعدد أزواج المفتاح-القيمة على صفر. في `logcat` عند قراءة تلك القيمة يمكننا رؤية الرسالة التالية: "`E Parcel  : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP  consumed 4 bytes, but -540 expected.`"

(كما يمكن استخدام `LazyValue` مع طول سالب محدد (بدون استخدام الأخطاء الأخرى الموصوفة في هذا التقرير) لإنشاء `Bundle` ذاتي التغيير، الشيء الذي تم إنشاء `LazyValue` للقضاء عليه. لكن هذه قصة أخرى (وأُبلغ عنها منفصلة لجوجل)، وفي هذا الاستغلال أهدف إلى المزيد)

إذن، كم نريد أن نتراجع؟

بعد حدوث استدعاء `setDataPosition()`، ستستمر القراءة إلى زوج المفتاح-القيمة التالي في `Bundle`، لذا نحتاج إلى اختيار موضع سيكون لدينا فيه:

1. مفتاح `Bundle`، يُقرأ باستخدام `Parcel.readString()`، يمكن أن يكون أي شيء تقريبًا، بما في ذلك الإشارة إلى طول غير صالح (سالب أو يتجاوز الحجم الكلي لـ `Parcel`)، وفي هذه الحالة ستعيد `readString()` قيمة `null` وهي مفتاح صالح في `Bundle`
2. نوع القيمة، يجب أن يكون واحدًا من [الأنواع التي تعيد منها `isLengthPrefixed()` القيمة `true`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4695-4712;drc=03c34f57c05feecfb090de3917787f049cb5f804)
3. طول القيمة، يجب أن يكون أيضًا قيمة نتحكم بها، `Parcel.appendFrom()` سيفشل إذا كان الطول غير متسلسل أو يتجاوز الحجم الكلي لـ `Parcel` المصدر

إذن ما هو الموضع في `Parcel` الذي يمكن أن يكون، مع الأخذ في الاعتبار أن نفس البيانات قد تمت قراءتها بالفعل وهي ضرورية للوصول إلى هذه النقطة:

* ليس قبل اسم `Parcelable` (`"android.view.RemoteViews"`)، لأنه لا توجد مساحة كافية
* ليس داخل اسم `Parcelable`، لأننا غير قادرين على ضبط النوع والطول
* ليس بعد اسم `Parcelable` مباشرة، لأن أول شيء في `RemoteViews` هو `mode` الذي [يجب أن نضبطه على `MODE_NORMAL` للوصول إلى الكود الخاص بنا](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/RemoteViews.java;l=3799;drc=03c34f57c05feecfb090de3917787f049cb5f804)
* ليس بعد ذلك، لأن ذلك بعد النقطة التي يوجد فيها `IApplicationThread` `Binder`

همم، لا يوجد أي مكان جيد عندما يكون `RemoteViews` هو الكائن الأكثر خارجية في البيانات المغلفة

نحتاج إلى العثور على بعض `Parcelable` أخرى:

1. تحتوي على مكان في البداية أو بالقرب منها حيث يمكننا وضع بيانات عشوائية (مثل `int`s أو `String`s التي هي مجرد بيانات ولا تؤثر على عملية التسلسل)
2. يمكنها احتواء `RemoteViews` (إما مباشرة أو عبر `readParcelable` عشوائي)
3. ليس لديها اسم فئة مؤهل بالكامل طويلًا جدًا، لأننا ما زلنا محدودي الحجم حسب الموضع الذي يبقى فيه `IApplicationThread` في `Parcel` الهدف

لذا أخذت قائمة بفئات `Parcelable` في النظام، وقمت بترتيبها حسب الطول التصاعدي لاسم الفئة المؤهل بالكامل، وبدأت في فحص العناصر في تلك القائمة لمعرفة ما إذا كانت تحقق الشرط 2

بهذه الطريقة وصلت إلى [`"android.os.Message"`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=afdb23ab6f909c5438fa69aad458a11497cff216)، وهو ما يستخدمه هذا الاستغلال. الآن عملية قراءة العنصر الذي أعددناه من `Parcel` تتم كالتالي:

* [علامة وجود العنصر](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577) لبدء `readParcelable`
* [اسم `Parcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4858;drc=03c34f57c05feecfb090de3917787f049cb5f804): `"android.os.Message"`
* [عدد قليل من `int`s يمكننا ضبطها على أي قيم نريدها](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=652-656;drc=afdb23ab6f909c5438fa69aad458a11497cff216) تُقرأ في الحقول
* نصل إلى استدعاء `readParcelable()`، الذي يمر بكل الطريقة الموصوفة أعلاه عبر `RemoteViews` ويبدأ في قراءة `Bundle` مع `Parcel.hasReadWriteHelper` بقيمة `true`
* يعلن هذا `Bundle` عن وجود زوجين من المفتاح-القيمة. في القيمة الأولى لدينا `LazyValue` بطول سالب، مما يؤدي إلى تشغيل `Parcel.setDataPosition()` إلى الموضع الذي توجد فيه سلسلة `"android.os.Message"`
* تستمر القراءة إلى زوج المفتاح-القيمة الثاني، المفتاح هو `"android.os.Message"` ونوع `LazyValue` وطوله وبياناته مأخوذة من `int`s الموصوفة في النقطة الثالثة. حصلت على `LazyValue` مع `mPosition` و `mLength` اللذين أردتهما. مرحى!
* بعد قراءة `LazyValue`s يتم فك تغليفها. يتم فك تغليف التي بالحجم السالب بنجاح واستبدالها بـ `Map` فارغة، بينما تفشل الأخرى في إلغاء التسلسل، ولكن يتم التقاط هذا الاستثناء ويظل `LazyValue` في `Bundle`
* ينتهي `readParcelable()`، ولكن هذه ليست نهاية بيانات `Message`. `Message.readFromParcel()` الآن يستمر في قراءة البيانات بعد التراجع ويرى بيانات تم كتابتها في البداية كجزء من `RemoteViews`. إذا ألقى أي شيء استثناءً في هذه النقطة، فسيتم إفساد الخطة بأكملها
* أول استثناء محتمل، [هناك استدعاء `readBundle()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=660;drc=afdb23ab6f909c5438fa69aad458a11497cff216). [`Bundle` لديه قيمة سحرية وإذا كانت خاطئة فسيتم إلقاء استثناء](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1809-1815;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91). لكن هذه القيمة السحرية غير موجودة إذا كان الطول [صفرًا](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1800-1804;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91) أو [سالبًا](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3323-3327;drc=03c34f57c05feecfb090de3917787f049cb5f804) وقد حدث ذلك عندما تم ضبط طول بيانات `LazyValue` على القيمة التي احتجتها لالتقاط `IApplicationThread`. لذا كنت محظوظًا هنا
* المشكلة التالية المحتملة يمكن أن تكون [استدعاء `Messenger.readMessengerOrNullFromParcel()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=661;drc=afdb23ab6f909c5438fa69aad458a11497cff216). هذا هو في الواقع كائن `Binder` مغلف. فشلت قراءة هذا `Binder`، لأن `Binder` هو كائن خاص في `Parcel` ويجب أن يتم تمييزه خارج النطاق ليتم قراءته. يتم [كشف هذه المشكلة وتسجيلها بواسطة `Parcel` على الجانب الأصلي، ومع ذلك لا يتم نشرها كخطأ ويتم ببساطة إرجاع `null`](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp;l=2473-2476;drc=8b12e8333bbe17ccf4b30efb825244e1923d3a71)

# تعطيل `attachApplication()`

حسناً، في الخطوة السابقة قمنا بنجاح بإنشاء كائن سيسمح لنا بالتقاط كائن `IApplicationThread` أثناء تشغيل طريقة `attachApplication()`

الشيء هو أن هذه الطريقة تكتمل بسرعة، وفرصنا في سباق عادل ضد اكتمالها ستكون ضئيلة نوعًا ما

ومع ذلك، تقوم هذه الطريقة بالحصول على عدد قليل من الأقفال المتبادلة (mutexes) (من خلال استخدام كتل `synchronized () {}` في جافا)، إذا تمكنا من الحصول على أحد هذه الأقفال والتعطل هناك، فستتوقف هذه الطريقة أيضًا

الآن دعنا نعود إلى بعض الأشياء التي قيلت بالفعل في هذا التقرير وستصبح مفيدة لهذا الغرض:

* يقوم `Bundle` بإجراء إلغاء تسلسل القيم الموجودة فيه عندما يتم الوصول إلى هذه القيم
* هناك فئة `ParceledListSlice` والتي ستقوم أثناء إلغاء التسلسل بإجراء استدعاء `Binder` صادر وحظر إلى الكائن المحدد داخل البيانات المسلسلة

عند جمع كل هذه الأشياء معًا: إذا وجدنا في `system_server` مكانًا يتم فيه الوصول إلى محتويات `Bundle` المقدمة من التطبيق تحت قفل متبادل يستخدم أيضًا بواسطة `attachApplication()`، فسنكون قادرين على تعطيل `attachApplication()` حتى تنتهي معاملة `Binder` التي تتم إلى عمليتنا

[`ActivityOptions`](https://developer.android.com/reference/android/app/ActivityOptions) هي فئة تصف معلمات مختلفة تتعلق ببدء `Activity` (على سبيل المثال الرسوم المتحركة). على عكس الفئات الأخرى التي تصف المعلمات التي تمر إلى `system_server`، فإن هذه الفئة لا تطبق `Parcelable` ولكنها بدلاً من ذلك توفر طريقة لتحويلها إلى `Bundle`

على جانب `system_server`، يتم [تحويل هذا `Bundle` مرة أخرى إلى `ActivityOptions`، مما يؤدي إلى تشغيل إلغاء التسلسل](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityOptions.java;l=1096-1204;drc=03c34f57c05feecfb090de3917787f049cb5f804). لقد وجدت مكانًا [يتم فيه تنفيذ هذه العملية بينما يتم الاحتفاظ بقفل `ActivityTaskManagerService.mGlobalLock` في `ActivityTaskManagerService.moveTaskToFront()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=2088;drc=03c34f57c05feecfb090de3917787f049cb5f804)

لذا أستدعي [`ActivityManager.moveTaskToFront()`](https://developer.android.com/reference/android/app/ActivityManager#moveTaskToFront(int,%20int,%20android.os.Bundle))، مررًا `Bundle` الذي يحتوي على `ParceledListSlice` بدلاً من القيمة المتوقعة النوع. يقوم [`ParceledListSlice` بإجراء استدعاء `Binder` إلى عمليتي](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=82-105;drc=23c7543b8e608ebcbb38b952761b54bb56065577) وحتى أعود من هذا الاستدعاء، سيبقى قفل `ActivityTaskManagerService.mGlobalLock` مقفلاً

# إنشاء عدة `LazyValue`s تشير إلى `Parcel`s مختلفة

تعمل `Parcel.recycle()` و `Parcel.obtain()` بطريقة [آخر ما يدخل يخرج أولاً](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))

هذا يعني أنه إذا قمت بإنشاء `LazyValue` مزيفة عندما لا تكون هناك معاملة `Binder` أخرى قيد التشغيل إلى `system_server`، فسأحصل على `LazyValue` ستشير إلى `Parcel` التي تُستخدم دائمًا عندما تكون هناك معاملة واحدة واردة فقط إلى `system_server` (حتى يحدث أن تبدأ وتنتهي معاملة متزامنة واردة إلى `system_server` بترتيب غير مكدس)

نظرًا لأنني لا أتحكم في المعاملات الأخرى الواردة إلى `system_server`، ولتحسين موثوقية الاستغلال، قمت بإنشاء عدة `LazyValue`s تشير إلى `Parcel`s مختلفة

بما أن لدي القدرة على تشغيل معاملة `Binder` متزامنة إلى عمليتي من `system_server`، فقد استخدمت هذه القدرة لإنشاء `LazyValue` على مستويات مختلفة من التكرار بين عمليتي و `system_server` (على الرغم من أنني هذه المرة فعلت ذلك دون الاحتفاظ بقفل متبادل عام)

إذن:

* أقوم بإنشاء `LazyValue`
* أقوم بتشغيل استدعاء إلى `system_server`، يتصل بي `system_server` مرة أخرى
    * أقوم بإنشاء `LazyValue`
    * أقوم بتشغيل استدعاء إلى `system_server`، يتصل بي `system_server` مرة أخرى
        * أقوم بإنشاء `LazyValue`
        * أقوم بتشغيل استدعاء إلى `system_server`، يتصل بي `system_server` مرة أخرى
            * ...

ثم بمجرد أن أحصل على عدد كافٍ من `LazyValue`s، أنتهي من فعل ذلك، أعود من كل هذه الاستدعاءات ويتم `recycle()` لجميع `Parcel`s التي تم حجزها بواسطة هذه الاستدعاءات

كل `LazyValue` قمت بإنشائها مغلفة في [`ParceledListSlice` منفصل تم إنشاؤه بواسطة `getQueue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804) ويمكنني استدعاء `ParceledListSlice` `Binder` لجعل `system_server` يقوم بتسلسلها وإرسالها إلى عمليتي

(طريقة بديلة لفعل ذلك ستكون إنشاء عدة `MediaSession`s)

# بدء عملية التطبيق الهدف

الآن لدينا كل ما هو مطلوب لالتقاط `IApplicationThread` من `attachApplication()` عندما يحدث، لكننا ما زلنا بحاجة إلى جعل `attachApplication()` يحدث

بشكل عام [هناك عدة أنواع من مكونات التطبيق التي يمكن لتطبيق آخر التفاعل معها](https://developer.android.com/guide/components/fundamentals#Components)، كل منها يتطلب بدء عملية التطبيق

أردت بدء تطبيق الإعدادات للنظام (الذي [يعمل تحت uid النظام](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=5;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) وبالتالي لديه حق الوصول إلى [كل شيء خلف أذونات Android](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityManager.java;l=3948-3952;drc=16c119018a80b8630c7736a5c6dc35ebef130c5b))

في البداية حاولت تشغيله عبر `startActivity()`، ولكن عندما جربت ذلك، لم تبدأ العملية حتى قمت بتحرير قفل `ActivityTaskManagerService`. التفاصيل حول سبب ذلك موجودة في قسم "ملاحظة إضافية: استدعاءات `Binder` وإعادة الدخول للأقفال المتبادلة"، ولكن كحل، قررت أن أطلب من النظام [`ContentProvider` من ذلك التطبيق](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=4031-4034;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) بدلاً من `Activity`. كان لهذا ميزة إضافية تتمثل في تجنب التداخل مع واجهة المستخدم الخاصة بي

لم أستخدم [واجهة برمجة التطبيقات الرسمية `ContentResolver` التي تعرضها SDK](https://developer.android.com/reference/android/content/ContentResolver)، ولكن بدلاً من ذلك استخدمت [واجهة برمجة تطبيقات داخلية للنظام، لأنني كنت بحاجة إلى واجهة برمجة تطبيقات غير متزامنة](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IActivityManager.aidl;l=153-154;drc=3d7be31a284d295af4c118c5675896e6fcc28907) لأن الربط بـ `ContentProvider` لن يكتمل حتى `attachApplication()` الذي أعلق فيه، على الرغم من أن بدء مؤشر ترابط آخر يمكن أن يكون بديلاً

(لا يهم ما يقدمه هذا `ContentProvider` المعين، الشيء الوحيد المهم هو أنه يمكنني إنشاء اتصال به)

هذه هي الطريقة التي أبدأ بها عملية تطبيق الإعدادات. أتأكد من أنه لا يعمل بالفعل في المقام الأول باستخدام [الطريقة المتاحة رسميًا `ActivityManager.killBackgroundProcesses()`](https://developer.android.com/reference/android/app/ActivityManager#killBackgroundProcesses(java.lang.String))

# تجميع كل شيء معًا

تم الآن وصف الأساسيات، لذا إليك كيف يعمل كل شيء معًا (هذا إلى حد كبير نسخة مطابقة لطريقة `MainActivity.doAllStuff()` من هذا الاستغلال):

1. تمكين الوصول إلى واجهات برمجة التطبيقات المخفية (واجهات برمجة التطبيقات المخفية ليست حدودًا أمنية وهناك [حلول بديلة متاحة علنًا بالفعل](https://www.xda-developers.com/bypass-hidden-apis/)، على الرغم من أنني استخدمت هنا طريقة تعتمد على [`Property.of()`](https://developer.android.com/reference/android/util/Property#of(java.lang.Class%3CT%3E,%20java.lang.Class%3CV%3E,%20java.lang.String))، والتي لم أرها في مكان آخر)
2. (فقط إذا كنا نعيد تشغيل الاستغلال بعد المحاولة الأولى) تحرير الاتصال بـ `ContentProvider` الذي أنشأناه في الخطوة 6 أثناء التنفيذ السابق. يجب أن نفعل ذلك وإلا فلن تعتبر `ActivityManager.killBackgroundProcesses()` العملية الهدف "خلفية" ولن تقتلها
3. قتل عملية التطبيق الضحية باستخدام `ActivityManager.killBackgroundProcesses()`، لأن `attachApplication()` يتم استدعاؤه فقط عند بدء العملية
4. طلب `system_server` لإنشاء مجموعة من الكائنات التي تحتوي على `LazyValue` تشير إلى `Parcel` التي سيتم إعادة تدويرها لاحقًا. أحصل على مرجع `ParceledListSlice` `Binder` لكل كائن يحتوي على `LazyValue` ويمكنني إجراء معاملة `Binder` إليه لتشغيل النظام لكتابته مرة أخرى. يتم إنشاء كل كائن `LazyValue` على أعماق مختلفة من [الاستدعاءات المتبادلة العودية](https://en.wikipedia.org/wiki/Mutual_recursion) بين `system_server` وتطبيقي لجعل من المحتمل أن يكون لكل من هذه `LazyValue`s مرجع معلق لكائن `Parcel` مختلف
5. أغلق `ActivityTaskManagerService.mGlobalLock` عن طريق إجراء استدعاء إلى `ActivityTaskManagerService.moveTaskToFront()` مع تمرير `Bundle` الذي يقوم عند إلغاء التسلسل بإجراء معاملة `Binder` متزامنة إلى عمليتي. يتم تنفيذ الخطوات التالية من هذا الاستدعاء وبالتالي يتم تنفيذها مع الاحتفاظ بهذا القفل
6. أطلب من `ActivityManagerService` الاتصال بـ `ContentProvider` لتطبيق الضحية (لاحظ عدم وجود "`Task`" في الاسم، `ActivityTaskManagerService` هي فئة تركز في الغالب على معالجة مكونات `Activity` للتطبيقات، بينما `ActivityManagerService` تتعامل مع [مكونات التطبيق الأخرى](https://developer.android.com/guide/components/fundamentals#Components) (بالإضافة إلى بدء العملية بشكل عام)، هذا [التقسيم حدث في Android 10، سابقًا كان التعامل مع كل من `Activity` ومكونات التطبيق الأخرى في `ActivityManagerService`](https://android.googlesource.com/platform/frameworks/base/+/595070969de0a7334d251d5448b641e856e052bc))
7. أنام قليلاً (`sleep()`) لإعطاء العملية المنشأة حديثًا وقتًا لبدء استدعاء `attachApplication()`
8. بينما القفل ما زال محتفظًا به، أطلب من جميع كائنات `ParceledListSlice` التي تم إنشاؤها سابقًا إرسال محتوياتها المتبقية (التي لم تتناسب مع المعاملة الأولية)، وهي كائنات تحتوي على `LazyValue` تشير إلى `Parcel` المعاد تدويرها. ثم من الإزاحة المشفرة التي تطابق موضع `IApplicationThread` الذي تم تمريره إلى `attachApplication()`، أقرأ كائن `Binder`. في الوقت الحالي أقوم فقط بحفظ `Binder`s المستلمة في `ArrayList` لتجنب القيام بالكثير مع الاحتفاظ بالقفل
9. هذه هي نهاية الكود الذي أقوم به من الاستدعاء الذي بدأ في الخطوة 5. يصبح `ActivityTaskManagerService.mGlobalLock` غير مقفل
10. لقد حصلت على `IApplicationThread` `Binder`. الآن يمكنني ببساطة استخدامه لتحميل الكود الخاص بي في تطبيق الضحية كما هو موضح في القسم التالي

# كيف أستخدم `IApplicationThread`

كما ذكر سابقًا، [يرسل التطبيق `IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl;drc=45f4b4aaa5fd8af2d0c685b2fef8acf75ed37452) `Binder` إلى `system_server` عندما تبدأ عملية التطبيق، ثم يستخدمه `system_server` لإخبار التطبيق عن المكونات التي يجب تحميلها

من المفترض أن يتم تمرير هذا الكائن فقط إلى `system_server` وبالتالي لا توجد فحوصات تعتمد على `Binder.getCallingUid()` هناك، لذا يمكننا ببساطة استدعاء الطرق التي تقدمها تلك الواجهة مباشرة

لقد [وصفت في تقريري السابق كيف أحصل على تنفيذ الكود عن طريق التلاعب بوسائط `scheduleReceiver()`](https://github.com/michalbednarski/ReparcelBug2#what-then-happens-within-handlereceiver). الوضع الآن هو نفسه باستثناء أنني هذه المرة أستدعي `scheduleReceiver()` بنفسي بينما كنت في ذلك الوقت أتلاعب بتفسير وسائط استدعاء تم إجراؤه بواسطة `system_server`

# ملاحظات إضافية

في هذا القسم أصف بعض الأشياء التي في النهاية لم تثبت فائدتها في هذه الحالة، على الرغم من أنها قد تكون ميزات تستحق الوعي بها أو أخطاء محتملة

## ملاحظة إضافية: `Bundle.clear()`للبساطة، قمت هنا بوصف `Bundle` المحدث دون [الالتزام الذي تم تقديمه لاحقًا، والذي يسمح بإعادة استخدام `Parcel` المستخدم في `Bundle` لدعم `LazyValue`s عن طريق استدعاء `Bundle.clear()`](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/)

كما هو مذكور في رسالة الالتزام، يتم تتبع ما إذا تم نسخ `Bundle` وفي هذه الحالة لن يقوم `clear()` بإعادة تدوير `Parcel`

ومع ذلك، فإن هذا الالتزام يغير أيضًا دلالات معامل/متغير `recycleParcel` لـ [`BaseBundle.initializeFromParcelLocked()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=408-457;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)

سابقًا، كان `recycleParcel` بقيمة `false` يشير إلى أنه لا ينبغي إعادة تدوير `Parcel`، إما لأن [المتصل ضبط `recycleParcel` على `false` للإشارة إلى أن `Parcel` ليس مملوكًا لـ `Bundle`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1842-1847;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1) أو لأنه تم [ضبطه على `false` بناءً على نتيجة `parcelledData.readArrayMap()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=441;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)

الآن أسباب كون `recycleParcel` بقيمة `false` هي نفسها، لكن تفسير ذلك تغير، الآن لا يعني ذلك "لا تقم بإعادة تدوير هذا `Parcel`"، بل يعني "أجل إعادة تدوير `Parcel` حتى استدعاء `Bundle.clear()`"

هذا يعني أنه إذا تم استدعاء `clear()` على `Bundle` تم إنشاؤه مع `Parcel.hasReadWriteHelper()` بقيمة `true`، فإن ذلك سيؤدي إلى إعادة تدوير `Parcel`، بينما الكود الذي استدعى إنشاء ذلك `Bundle` سيقوم أيضًا بإعادة تدوير ذلك `Parcel`، مما يؤدي إلى `recycle()` مزدوج، والذي يؤدي إلى سلوك مشابه لـ double-free: الاستدعاءات التالية لـ `Parcel.obtain()` ستعيد نفس الكائن مرتين

ومع ذلك، لم أجد طريقة لاستدعاء `clear()` على مثل هذا `Bundle`

منذ أن كتبت هذا أصلاً، [تم تغيير سلوك `recycle()` والآن إعادة التدوير الإضافية هي عملية no-op مع احتمال تعطل من خلال `Log.wtf()`](https://android.googlesource.com/platform/frameworks/base/+/64ff38669a0e1f945b54c4c62ed9316282a6588d%5E%21/) ([اعتمادًا على التكوين، ولكن لا يؤدي أبدًا إلى تعطل `system_server`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8619-8648;drc=ab23aee04d50b9bdabd52481e77265e976558056)). أود أن أقول أن السلوك الجديد قد لا يزال خطيرًا، خاصة عندما نمتلك القدرة على تأخير إلغاء التسلسل برمجيًا الذي يحدث في عملية أخرى، ولكن لا توجد حقًا طريقة جيدة للتعامل مع إعادة التدوير المزدوجة

## ملاحظة إضافية: استدعاءات `Binder` وإعادة الدخول إلى المزامنات (mutexes)

ميزة غير معروفة جيدًا لـ `Binder` هي أنه يدعم إرسال الاستدعاءات المتكررة إلى الخيط الأصلي

أي إذا قامت العملية A باستدعاء `Binder` متزامن إلى العملية B، ثم أثناء معالجته على نفس الخيط، قامت العملية B باستدعاء `Binder` متزامن إلى العملية A، فسيتم إرسال ذلك الاستدعاء في العملية A على نفس الخيط الذي ينتظر اكتمال الاستدعاء الأصلي للعملية B

الشيء الآخر هو أن أقسام `synchronized () {}` في Java هي مزامنات قابلة لإعادة الدخول، مما يعني أنه إذا دخلت إليها مرتين من نفس الخيط، فسوف تسمح لك بالدخول ولن تتسبب في deadlock

هذا يعني أنه نظريًا بينما نحتفظ بقفل `ActivityTaskManagerService.mGlobalLock`، لا يزال بإمكاننا تشغيل تطبيق الإعدادات باستخدام `startActivity(new Intent(Settings.ACTION_SETTINGS))` وسندخل بنجاح إلى [`synchronized` block الذي نعطله](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=626;drc=8854e6eb5960c1a9b233fd0fa6e36a366b2f802d)، ومع ذلك، فإن بدء ذلك النشاط (`Activity`) يتضمن أيضًا إنشاء مهمة (`Task`)، والذي يتضمن استدعاء [`notifyTaskCreated()`، الذي يرسل رسالة](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=424-429;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) إلى [`DisplayThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/DisplayThread.java;l=25-31;drc=92b9365f9e1ea5d735e8acb06f790604036ee547)، و[معالجة ذلك](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=207-208;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) تحاول [الحصول على القفل الذي نعطله من خيط آخر](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=320;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f). لذا حتى نقوم بتحرير `ActivityTaskManagerService.mGlobalLock`، سيبقى خيط `DisplayThread` محظورًا. لاحقًا، تتضمن إجراءات بدء النشاط (`Activity`) [إرسال رسالة إلى نفس الخيط لبدء عملية التطبيق](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=4659-4664;drc=c76db81ef9d400ffed200f69c7bbda923cdb941c). كل هذا يعني أنه في هذه الحالة لن يتم بدء عملية التطبيق حتى نقوم بتحرير القفل وكان&nbsp;السبب الذي جعلنا نحتفظ بذلك القفل في المقام الأول هو منع إتمام معاملة `attachApplication()` حتى نتمكن من الحصول على المقابض (handles) منها، ولكن في هذه الحالة لن تبدأ تلك المعاملة فعليًا

حتى إذا قمنا بتشغيل نشاط (`Activity`) سيكون جزءًا من نفس المهمة (`Task`) مثل النشاط الحالي (أي أننا سنقوم بتشغيل نشاط مختلف من تطبيق الإعدادات، نشاط لا يحدد `android:launchMode="singleTask"`)، ستظل تلك الإجراءات تتضمن [`notifyTaskDescriptionChanged()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=444-450;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)، والذي له نفس التأثير هنا مثل `notifyTaskCreated()`

لذا بينما يمكن لخيطي استدعاء الأساليب التي تستخدم `synchronized (ActivityTaskManagerService.mGlobalLock) {}`، فإن بدء عملية تطبيق جديدة بعد `startActivity()` تضمن استخدام ذلك القفل من خيط مختلف ولم يكن ذلك مفيدًا في هذه الحالة، لذلك اخترت تشغيل بدء عملية التطبيق من خلال `ContentProvider` بدلاً من ذلك

## ملاحظة إضافية: طرق أخرى لاستخدام `IApplicationThread`

`IApplicationThread` هو مقبض (handle) مميز جدًا، لذا أعتبر استخدامه بعد الحصول عليه مرحلة ما بعد الاستغلال (post-exploitation)

في هذا الاستغلال، استخدمته مباشرة لطلب تنفيذ الكود في العملية المستهدفة، مستفيدًا من حقيقة أن الوصول إلى تلك العملية مقيد بالقدرة (امتلاك كائن `Binder`، والذي سربناه هنا) وليس بواسطة `Binder.getCallingUid()`

إضافة فحص `Binder.getCallingUid()` في [`ApplicationThread.scheduleReceiver()` (الذي استخدمناه هنا لطلب تنفيذ الكود)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=985-997;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) والطرق الأخرى لـ `ApplicationThread` (حيث أن `scheduleReceiver()` ليست الطريقة الوحيدة في `IApplicationThread` التي تسمح بتحميل الكود) لن يمنع استخدام `IApplicationThread` لتحميل الكود في عملية تطبيق آخر، حيث يمكن للمهاجم تمرير `IApplicationThread` المسرب بدلاً من الخاص به إلى `attachApplication()`

بالإضافة إلى تحميل الكود في العملية، فإن امتلاك `IApplicationThread` يسمح بتنفيذ [`grantUriPermission()` باستخدام صلاحيات العملية التي ينتمي إليها ذلك المقبض](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5631-5632;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)
تنزيل الأداة
تستدعي initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative);
source
mParcelledData
Parcel
Bundle
recycleParcel
true
Parcel
Bundle
Parcel.recycle()
  • initializeFromParcel تستدعي recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader) من أجل قراءة محتويات خريطة المفتاح-القيمة. المفاتيح هي String ويتم قراءة القيم باستخدام readLazyValue()، مما ينشئ كائنات LazyValue للقيم من الأنواع التي يتم كتابتها مع بادئة الطول. ترجع readArrayMap() قيمة تشير إلى ما إذا كان من المسموح إعادة تدوير Parcel. إذا كانت هناك أي كائنات LazyValue موجودة، يتم تعيين recycleParcel على false ولن يتم إعادة تدوير Parcel الذي تشير إليه LazyValues (هناك استثناء لذلك، لكنه غير ذي صلة هنا، سأصفه في قسم "ملاحظة إضافية: Bundle.clear()")
  • بمجرد الانتهاء من unparcel()، يتم تعيين mMap (ليس null) ويربط مفاتيح String إما بالقيم الفعلية إذا كانت جاهزة أو بكائنات LazyValue
  • بعد ذلك، يتم استدعاء getValue()، والتي تربط المفتاح (String) بالفهرس (int) وتمرره إلى getValueAt()
  • getValueAt() تكتشف LazyValue من خلال instanceof BiFunction وتستدعي apply() لإلغاء تسلسلها
  • LazyValue.apply() يرجع Parcel إلى موضع LazyValue.mPosition ويستدعي Parcel.readValue() العادي الذي تحدثت عنه بالفعل
  • عند نجاح إلغاء التسلسل، يتم استبدال LazyValue في mMap، بحيث أن استدعاء Bundle.get*() التالي لنفس المفتاح سيعيد القيمة مباشرة ولن يتم تكرار إلغاء تسلسل LazyValue. عندما يتم تمرير Bundle للأمام، سيتم إعادة تسلسل تلك القيمة مرة أخرى بدلاً من نسخ البيانات الأصلية حرفيًا (ولكن بعد قراءة Bundle المُمرر، ستكون تلك القيمة LazyValue مرة أخرى ولن تؤثر أي اختلافات محتملة في writeToParcel/createFromParcel على القيم الأخرى)
  • Parcel
    maybeWriteSquashed()
    system_server
    Bundle
    Parcel.writeParcelable()
    Parcelable.writeToParcel
  • إذا اقتربنا من حد حجم معاملة Binder، يتم كتابة 0 للإشارة إلى أنه لا توجد المزيد من العناصر في هذه المعاملة وسيتم إرسال العناصر التالية في معاملة أخرى
  • بمجرد أن يستلم ParcelableListBinder عدد العناصر الذي تم تحديده في المعاملة الأولى، يقوم باستدعاء lambda الممررة إلى منشئه، والتي في هذه الحالة تعين القائمة المستردة إلى MediaSessionRecord.mQueue
  • Binder
  • عند قراءة ParceledListSlice من Parcel، يقرأ الجزء الأول مباشرة من Parcel ثم إذا لم تكن جميع العناصر قد كتبت بشكل مضمن، فإنه يستدعي Binder الذي تم كتابته إلى Parcel من أجل استرداد تلك العناصر
  • Parcel
    Parcel
    Parcel.obtain()، الذي يستخدم Parcel.sOwnedPool
    Binder
    النظام باستدعاء Parcel.obtain(long obj)
    يستخدم Parcel.sHolderPool
    Parcel.recycle()
    بإرجاع كائن Parcel إلى المجمع المناسب
    RemoteViews
    Parcel
    makeOwnedLeaker
    makeHolderLeaker
    فئة ValueLeakerMaker الخاصة بي
    RemoteViews.readActionsFromParcel()
    يستدعي getActionFromParcel()
    ReflectionAction
    قراءة معلمات BaseReflectionAction الشائعة
    ببناء Bundle