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

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

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

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

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
ReparcelBug2 — # مقالة واستغلال لتطبيق مثبت لتصعيد الامتيازات إلى مستوى النظام على أندرويد 12 بيتا عبر CVE-2021-0928، وهو عدم تطابق في تسلسل `writeToParcel`/`createFromParcel` في `OutputConfiguration` | Kitploit
أدوات/GitHubGitHub/michalbednarski/reparcelbug2
أمان أندرويدتصعيد الامتيازاتتحليل الثغرات الأمنيةالاستغلالأمن الجوالتحليل الملفات الثنائية
GitHubmichalbednarski/reparcelbug2

ReparcelBug2

# مقالة واستغلال لتطبيق مثبت لتصعيد الامتيازات إلى مستوى النظام على أندرويد 12 بيتا عبر CVE-2021-0928، وهو عدم تطابق في تسلسل `writeToParcel`/`createFromParcel` في `OutputConfiguration`

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

الأكثر شعبية

عرض الكل →

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

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

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

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

CVE-2021-0928، عدم تطابق في تسلسل writeToParcel/createFromParcel في android.hardware.camera2.params.OutputConfiguration

هذا استغلال يستخدم تلك الثغرة الأمنية لتصعيد الامتيازات من تطبيق Android مثبت إلى تطبيق إعدادات Android (أو أي تطبيق آخر مثبت يمكنه الإرسال إلى <receiver> المُعلن في AndroidManifest.xml، وكان تصعيد الامتيازات بالإرسال إلى <activity> ممكنًا أيضًا، وإن لم يُعرض هنا)

لقد وجدت المشكلة في الأصل على Android 12 Developer Preview 3

نسخة الاستغلال الموجودة في هذا المستودع تعمل على Android 12 Beta 2 و 3

تم إصلاح الثغرة الأمنية في أول إصدار رسمي من Android 12

الشرح أدناه كُتب في الأصل لشركة Google للنظر في هذا التقرير كسلسلة استغلال كاملة

في وقت كتابة هذا الشرح، لم يكن Android 12 متاحًا في AOSP (إصدارات Android Developer Preview/Beta ليست مفتوحة المصدر)

لقطة شاشة لإشعار Android من تطبيق الإعدادات: Hello from 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

مقدمة إلى Parcel

معظم الاتصالات بين العمليات (IPC) على Android تتم من خلال فئة تُسمى Parcel

الاستخدام الأساسي لـ Parcel هو كما يلي:```java Parcel p = Parcel.obtain(); p.writeInt(1); p.writeString("Hello");

root@kitploit:~
ثم يتم إرسال `Parcel` إلى عملية أخرى [عبر `Binder`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). بدلاً من ذلك، ولأغراض الاختبار، يمكن استدعاء [`p.setDataPosition(0)`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)) لإعادة توجيه الـ parcel إلى موضعه الأولي والبدء في القراءة:```java
int a = p.readInt(); // a = 1
String b = p.readString(); // b = "Hello"

يجب ملاحظة أن Parcel يحتفظ داخليًا بالموضع الذي تُنفَّذ منه عمليات القراءة. تقع على عاتق مستخدم فئة Parcel مسؤولية ضمان تطابق طرق read* مع طرق write* المستخدمة سابقًا، وإلا ستتم القراءات اللاحقة من مواضع خاطئة في المخزن المؤقت

كما يوفر Parcel إمكانية كتابة كائنات مخصصة، والطريقة المفضلة للقيام بذلك هي عبر تنفيذ واجهة Parcelable

فيما يلي مثال على تنفيذ واجهة Parcelable (تمت إزالة الكود غير ذي الصلة، ويُستخدم فئة WindowContainerTransaction في الاستغلال كجزء من سلسلة الأدوات، ومع ذلك لا يوجد بها أي خطأ)```java package android.window; public final class WindowContainerTransaction implements Parcelable { private final ArrayMap<IBinder, Change> mChanges = new ArrayMap<>(); private final ArrayList mHierarchyOps = new ArrayList<>();

root@kitploit:~
private WindowContainerTransaction(Parcel in) {
    in.readMap(mChanges, null /* loader */);
    in.readList(mHierarchyOps, null /* loader */);
}

@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
    dest.writeMap(mChanges);
    dest.writeList(mHierarchyOps);
}

@NonNull
public static final Creator<WindowContainerTransaction> CREATOR =
        new Creator<WindowContainerTransaction>() {
            @Override
            public WindowContainerTransaction createFromParcel(Parcel in) {
                return new WindowContainerTransaction(in);
            }
        };

}

root@kitploit:~
كما يتضح أعلاه، يتم استخدام طريقة `writeToParcel()` أثناء الكتابة. ثم أثناء القراءة يتم استدعاء طريقة المصنع `CREATOR.createFromParcel()`. تقع مسؤولية تنفيذ `Parcelable` في ضمان أن `createFromParcel` يقرأ نفس كمية البيانات التي كتبها `writeToParcel`، وإلا فإن جميع القراءات اللاحقة من ذلك `Parcel` ستقرأ البيانات من إزاحة خاطئة

يمكن كتابة/قراءة مثل هذه الفئة من/إلى `Parcel` من خلال:

* استدعاء `obj.writeToParcel(parcel, 0)` / `obj = WindowContainerTransaction.CREATOR.createFromParcel()` مباشرة، وهذا يُستخدم غالبًا عندما يكون نوع الفئة معروفًا، على سبيل المثال عندما يحتوي `Parcelable` على حقل بنوع `Parcelable` مختلف أو في الكود المُولّد بواسطة AIDL عندما تحتوي طريقة RPC المعرّفة على `Parcelable` كوسيط
* من خلال `Parcel.writeParcelable`/`readParcelable`. [`writeParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=1909;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3) يكتب أولاً اسم الفئة ثم يستدعي طريقة `writeToParcel` من واجهة `Parcelable`. [`readParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3282;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3) يقرأ اسم الفئة المكتوب، ويجد الفئة بهذا الاسم في `ClassLoader` المقدم أو في `BOOTCLASSPATH` إذا تم توفير قيمة فارغة. بمجرد العثور على الفئة، يتم استخدام حقلها الثابت `CREATOR` للحصول على مثيل [`Parcelable.Creator`](https://developer.android.com/reference/android/os/Parcelable.Creator) وهو المصنع المستخدم لقراءة تلك الفئة. من المهم ملاحظة أنه عند استخدام طريقة `readParcelable` يمكنها قراءة أي `Parcelable` متاح في مسار الفئات حيث يتم قراءة اسم الكائن المراد إنشاؤه من نفس `Parcel`
* يتم استخدام `readParcelable` بواسطة العديد من طرق `Parcel` الأخرى، على سبيل المثال `readList` الذي شوهد في المثال أعلاه يقرأ العناصر من خلال `readValue`، وهي الطريقة الأكثر عمومية لنقل الكائنات في `Parcel` وإحدى الطرق التي تستخدمها هي من خلال `readParcelable`. أيضًا في المثال أعلاه، بسبب محو النوع في جافا (Type Erasure)، يمكن لحقل `ArrayList<HierarchyOp> mHierarchyOps` أن يحتوي فعليًا على أي كائنات يدعمها Parcel، وليس فقط تلك المتوافقة مع النوع المحدد في تعريف النوع العام

# عدم تطابق `writeToParcel`/`createFromParcel`

كما ذُكر أعلاه، تقع مسؤولية تنفيذ واجهة `Parcelable` في ضمان أن `createFromParcel` يقرأ نفس كمية البيانات من `Parcel` التي كتبها `writeToParcel` المطابق سابقًا. كلما وُجد في `BOOTCLASSPATH` عنصر `Parcelable` يمكنه انتهاك هذا العقد، فإنه يُنشئ ثغرة أمنية لأنه يسمح بالسيناريو التالي:

1. يرسل تطبيق خبيث إلى `system_server` حزمة `Bundle` أو عنصر `Parcelable` يحتوي على مثيل `Parcelable` معيب إلى جانب بيانات مُنشأة خصيصًا سيتم قراءتها فعليًا في الخطوة 3 ولكن يتم تمريرها حرفيًا خلال الخطوة 2
2. يتحقق `system_server` من أن `Bundle` آمن ثم يعيد توجيهه، أو يمرر `system_server` عنصر `Parcelable` المقدم إلى طريقة AIDL التي تحتوي أيضًا على بيانات حرجة مرسلة في المعامل التالي (إذا كان من الممكن تعديل البيانات المستلمة في ذلك المعامل فسيؤدي ذلك إلى مشكلة أمنية)
3. يستقبل تطبيق آخر البيانات من `system_server` ويثق بها، ومع ذلك بسبب التسلسل المعيب فإن البيانات التي يراها فعليًا تختلف عن البيانات التي قصد `system_server` إرسالها

استخدمت "أو" في الخطوات أعلاه لأن هذه الخطوات تصف كلاً من [نوع استغلال قديم يؤدي إلى تشغيل نشاط عشوائي والذي نشرته في 2017](https://github.com/michalbednarski/ReparcelBug) (على الجانب الأيسر من "أو") ونوع جديد سأصفه هنا في القسم التالي

# كيفية تنفيذ `BroadcastReceiver` في التطبيق

من وجهة نظر مطور التطبيق الذي يستخدم واجهات برمجة التطبيقات المتاحة في Android SDK، فإن طريقة عمل [`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts) هي أن أحد التطبيقات يستدعي [`sendBroadcast`](https://developer.android.com/reference/android/content/Context#sendBroadcast(android.content.Intent)) (على الرغم من أن التطبيقات غالبًا ما تريد استقبال البث من النظام، وليس من تطبيق آخر) ثم يتم مطابقة `Intent` المُبث مع `<receiver>` المعرّف في `AndroidManifest.xml`، وعند حدوث ذلك يبدأ النظام عملية التطبيق المستلم، ويُنشئ مثيلًا من فئة فرعية لـ `BroadcastReceiver` كما هو معرّف في سمة `<receiver android:name>` ثم يستدعي طريقة [`onReceive`](https://developer.android.com/reference/android/content/BroadcastReceiver#onReceive(android.content.Context,%20android.content.Intent))

دعنا نلقي نظرة على التواصل مع `system_server` الذي يحدث في عملية استقبال البث:

* عندما تبدأ عملية التطبيق في البداية، فإنها [تستدعي `IActivityManager.attachApplication()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7340;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c)، وبذلك تمرر مقبض [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl) الذي يستخدمه النظام لإخبار عملية التطبيق بما يجب فعله
* عندما يريد النظام تنفيذ `BroadcastReceiver` المسجل في البيان داخل عملية التطبيق، فإنه يستدعي طريقة [`scheduleReceiver`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=950;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c) باستخدام `IApplicationThread` الموصوف في النقطة السابقة. تحتوي هذه الطريقة على عدة وسائط ولكن أهمها هنا هما الأولان:
  1. `Intent intent`، والذي تم تمريره سابقًا إلى النظام عند استدعاء `sendBroadcast()`
  2. `ActivityInfo info`، والذي يحتوي على معلومات حول المكوّن الذي يجب تنفيذه. يتم أخذ قيمة هذا المعامل من قبل النظام من خدمة مدير الحزم (Package Manager Service). والأهم من ذلك أن البيانات المرسلة في هذا المعامل تتضمن المسار إلى الملف الذي سيتم تحميل فئة جافا المسؤولة عن معالجة البث المستلم منه

في هذه المرحلة ربما يمكنك تخمين مسار الاستغلال الجديد هذا: استدعاء `sendBroadcast()` مع تمرير `Intent` سيؤدي إلى أنه عندما يحاول النظام استدعاء `scheduleReceiver` سيتسبب في أن التطبيق الذي يُستدعى فيه `scheduleReceiver` سيرى `ActivityInfo` معدّلًا

تجدر الإشارة إلى أن مسار الاستغلال الجديد هذا أصبح ممكنًا في Android 12 حيث لم تكن هناك طريقة سابقًا لوضع عناصر `Parcelable` عشوائية في `Intent` ([إضافات Intent](https://developer.android.com/reference/android/content/Intent#putExtra(java.lang.String,%20android.os.Parcelable)) لا تُحتسب لأنها توضع في `Bundle` الذي يُكتب طوله بالكامل في Parcel ويتم [قراءته ككتلة واحدة](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1671-1681;drc=5d123b67756dffcfdebdb936ab2de2b29c799321)، لذا لا يمكن للإضافات أن تسبب سوء تفسير لكائن `Intent` الذي يحتويها)

# تفعيل عدم تطابق `writeToParcel`/`createFromParcel`

في معظم الأحيان، تكون حالات عدم تطابق `writeToParcel`/`createFromParcel` هي حالات يُنسى فيها أحد الحقول أو يُكتب مرتين في إحدى هاتين الطريقتين، وفي هذه الحالة سيتسبب إرسال مثل هذا الكائن دائمًا في عدم تطابق. (يحدث ذلك غالبًا عندما يكون الكائن على الرغم من كونه `Parcelable`، لا يُستخدم فعليًا عبر العمليات، وإلا لكان ذلك قد لُوحظ بسرعة أثناء الاستخدام العادي)

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

دعنا نلقي نظرة على الفئة المعرضة للخطر ([الأصل كان هنا](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/hardware/camera2/params/OutputConfiguration.java;drc=46c390a1c695e2dc458cb889e40559f259f60aed)، الأسطر المميزة بـ `// New in Android 12` أُضيفت يدويًا لأنها لم تكن موجودة في AOSP وقت الكتابة) ([إليك الالتزام الذي أدخل الثغرة في الأصل](https://android.googlesource.com/platform/frameworks/base/+/a69b1bc58b0838e06deefb190e226774a34671e6%5E%21/#F10)، ومع ذلك تم نشره بعد إصدار Android 12)```java
package android.hardware.camera2.params;

public final class OutputConfiguration implements Parcelable {
    private OutputConfiguration(@NonNull Parcel source) {
        int rotation = source.readInt();
        int surfaceSetId = source.readInt();
        int surfaceType = source.readInt();
        int width = source.readInt();
        int height = source.readInt();
        boolean isDeferred = source.readInt() == 1;
        boolean isShared = source.readInt() == 1;
        ArrayList<Surface> surfaces = new ArrayList<Surface>();
        source.readTypedList(surfaces, Surface.CREATOR);
        String physicalCameraId = source.readString();
        boolean isMultiResolution = source.readInt() == 1; // New in Android 12
        ArrayList<Integer> sensorPixelModesUsed = new ArrayList<Integer>(); // New in Android 12
        source.readList(sensorPixelModesUsed, Integer.class.getClassLoader()); // New in Android 12

		// SNIP: copy values from variables set above to fields of this class
    }

    public static final @android.annotation.NonNull Parcelable.Creator<OutputConfiguration> CREATOR =
            new Parcelable.Creator<OutputConfiguration>() {
        @Override
        public OutputConfiguration createFromParcel(Parcel source) {
            try {
                OutputConfiguration outputConfiguration = new OutputConfiguration(source);
                return outputConfiguration;
            } catch (Exception e) {
                Log.e(TAG, "Exception creating OutputConfiguration from parcel", e);
                return null;
            }
        }

        @Override
        public OutputConfiguration[] newArray(int size) {
            return new OutputConfiguration[size];
        }
    };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        if (dest == null) {
            throw new IllegalArgumentException("dest must not be null");
        }
        dest.writeInt(mRotation);
        dest.writeInt(mSurfaceGroupId);
        dest.writeInt(mSurfaceType);
        dest.writeInt(mConfiguredSize.getWidth());
        dest.writeInt(mConfiguredSize.getHeight());
        dest.writeInt(mIsDeferredConfig ? 1 : 0);
        dest.writeInt(mIsShared ? 1 : 0);
        dest.writeTypedList(mSurfaces);
        dest.writeString(mPhysicalCameraId);
        dest.writeInt(mIsMultiResolution ? 1 : 0); // New in Android 12
        dest.writeList(mSensorPixelModesUsed); // New in Android 12
    }

    private ArrayList<Surface> mSurfaces;
    private final int mRotation;
    private final int mSurfaceGroupId;
    private final int mSurfaceType;
    private final Size mConfiguredSize;
    private final int mConfiguredFormat;
    private final int mConfiguredDataspace;
    private final int mConfiguredGenerationId;
    private final boolean mIsDeferredConfig;
    private boolean mIsShared;
    private String mPhysicalCameraId;
    private boolean mIsMultiResolution; // New in Android 12
    private ArrayList<Integer> mSensorPixelModesUsed; // New in Android 12
}

إذًا ما المشكلة هنا ولماذا يُدخل هذا التغيير ثغرة؟ كما ذكرت أثناء مناقشة مثال WindowContainerTransaction الخاص بـ Parcelable، يمكن لـ readList في الواقع ملء القائمة بأي كائن مدعوم بواسطة Parcel، وليس فقط تلك المطابقة للتصريح العام (ArrayList<Integer>). ومع ذلك، نظرًا لأننا نستخدم هذه الفئة فقط كجزء من سلسلة أدوات التسلسل ولا نستخدمها فعليًا، فلن يُستخدم هذا الحقل لأي شيء آخر غير القراءة والكتابة إلى Parcel (ومحاولات استخدام عناصر من ArrayList التي تحتوي على عناصر لا تطابق تصريحها العام ستؤدي فقط إلى ClassCastException على أي حال)، فهذه ليست مشكلة في حد ذاتها

في هذه الفئة يوجد أيضًا try-catch داخل createFromParcel، مما يعني أنه إذا تم إلقاء Exception أثناء القراءة، فسيتم إيقاف قراءة OutputConfiguration وسيستمر قراءة الكائن الذي يحتوي على OutputConfiguration. عندما يحدث ذلك، سيتم كتابة OutputConfiguration بالكامل إلى Parcel ولكن سيتم قراءته فقط حتى النقطة التي حدث فيها Exception. هذا يخلق عدم تطابق حيث أن البيانات غير المستهلكة المكتوبة داخل OutputConfiguration.writeToParcel ستُقرأ فعليًا بواسطة الكائن الذي كان يستدعي OutputConfiguration.CREATOR.createFromParcel

الآن، الجمع بين هذين الأمرين (السماح بتداخل كائنات عشوائية مدعومة بواسطة Parcel وتغليف ذلك داخل try-catch دون إعادة الإلقاء) يعطي القدرة على بناء Parcelable يمكن كتابته بواسطة system_server ثم قراءته لاحقًا بطريقة يتحكم فيها التطبيق الذي بنى Parcelable في البداية والذي يتم إعادة توجيهه بواسطة system_server

حسنًا، لبناء مثل هذا Parcelable الآن نحتاج إلى إيجاد شيء يوضع في mSensorPixelModesUsed سيتم قراءته بنجاح في system_server (لأن هذا الكائن يُستقبل من تطبيق المهاجم عبر Parcel)، ويُكتب بنجاح بواسطة system_server ثم يفشل في إلغاء التغليف ويلقي Exception في تطبيق الضحية

إحدى الطرق للقيام بذلك هي استخدام فئة موجودة داخل system_server ولكن ليس في التطبيقات، بحيث تؤدي محاولة إلغاء تسلسلها إلى ClassNotFoundException. ومع ذلك، لا يمكنني اختيار Parcelable من system_server لأن readParcelable بدون تحديد ClassLoader صراحةً سيبحث فقط في BOOTCLASSPATH الذي لن يحتوي على فئات خاصة بـ system_server. حل هذه المشكلة هو استخدام إحدى فئات Serializable لأن ObjectInputStream سيختار ClassLoader من أول طريقة غير موجودة في BOOTCLASSPATH في تتبع المكدس

لقد اخترت PackageManagerException، ومع ذلك قبل استخدامها هناك شيء آخر نحتاج إلى القيام به. في مُنشئ OutputConfiguration عندما يتم استدعاء readList، يتم تعيين وسيط loader صراحةً إلى Integer.class.getClassLoader(). قيمة loader هذه تُنقل إلى readValue()، ثم إلى readSerializable() وداخل readSerializable() إذا لم تكن وسيط loader فارغة، يتم استخدامها بدلاً من من (فحص لا يفعل شيئًا لأنه عندما لا يجد الفئة، سيلقي استثناءً بدلاً من إرجاع null). الطريقة للالتفاف حول ذلك بسيطة جدًا، نحتاج فقط إلى تغليف في بعض الذي يقوم بـ دون تحديد . هذا هو المكان الذي تأتي فيه فئة الموصوفة أعلاه

إذًا، في هذه المرحلة لدينا الكائن التالي:

  • OutputConfiguration
    • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
      • mHierarchyOps.get(0) = PackageManagerException

الآن يمكن إلغاء تسلسل هذا الكائن بنجاح داخل system_server: عندما يستدعي WindowContainerTransaction readList، سيحاول العثور على فئة PackageManagerException باستخدام ClassLoader الخاص بخوادم النظام (وليس BootClassLoader)، لأنه يمكنه العثور عليها في تتبع المكدس. محمل الفئات هذا موجود في تتبع المكدس لأنه بينما لم تكن جميع الطرق التالية من مسار فئات خادم النظام: Binder#execTransact()، وIActivityManager$Stub#onTransact() المولدة بواسطة AIDL وطرق من جميع فئات Parcelable المستخدمة، كانت هناك طريقة معلنة داخل خادم النظام في تتبع المكدس: طريقة onTransact المُعاد تعريفها داخل ActivityManagerService. لذلك يمكن لـ system_server قراءة وكتابة هذا الكائن لاحقًا إلى وعندما يحاول التطبيق المستهدف قراءته، لن تكون فئة متاحة وبالتالي سيتم إلقاء ، ثم

لذا قمنا بتفعيل هذا عدم التطابق. حسنًا، ليس حقًا في هذه المرحلة بعد لأن الاستثناء تم التقاطه عندما لم تكن هناك بيانات غير مقروءة متبقية بواسطة OutputConfiguration.writeToParcel، ولكن يمكننا بسهولة إضافة عنصر آخر إلى قائمة mSensorPixelModesUsed وسيتم كتابة هذا العنصر عبر Parcel.writeValue ويُترك غير مقروء بعد قراءة OutputConfiguration

وضع ذلك في Intent

كما أشرت أعلاه، سأرغب في تفعيل عدم التطابق من كائن Intent، حيث سيتم تمريره بواسطة system_server إلى طريقة AIDL التي تحتوي على Intent في المعامل الأول ومعلومات التنفيذ في المعامل الثاني، بحيث يؤدي تسلسل/إلغاء تسلسل Intent الممرر في المعامل الأول إلى تعديل القيمة في المعامل الثاني

في Intent.readFromParcel() تتم قراءة جميع القيم عبر طرق مكتوبة مخصصة، لذا لا يمكننا تحديد فئة Parcelable مخصصة هناك

داخل Intent مع ذلك، يوجد ClipData متداخل ومنذ Android 12 في ClipData$Item يوجد حقل جديد ActivityInfo mActivityInfo (لم يكن موجودًا في AOSP وقت الكتابة الأولية، هنا الالتزام الذي يقدم هذا الحقل، تتم قراءة هذا الحقل عبر in.readTypedObject(ActivityInfo.CREATOR) داخل مُنشئ ClipData(Parcel in))

ثم داخل مُنشئ ActivityInfo(Parcel source) مرة أخرى لا توجد طريقة لوضع Parcelable مخصص، ولكن نظرًا لأن ActivityInfo يمتد من ComponentInfo، فإنه يحتوي على حقل applicationInfo

أخيرًا داخل ApplicationInfo يوجد حقل SparseArray<int[]> splitDependencies، والذي يتم قراءته عبر readSparseArray، والذي بدوره يستخدم readValue لقراءة عناصر SparseArray

في هذه المرحلة يمكننا وضع OutputConfiguration داخل splitDependencies، ومع ذلك فإن قراءة splitDependencies تليها بعض استدعاءات readString8() وسيكون من الجيد أن يكون لدينا تحكم كامل في البيانات غير المستهلكة بعد حدوث عدم التطابق حتى نتمكن من وضع سلاسل فارغة هناك مباشرة وعدم القلق بشأن تفسير مختلف للبيانات غير المستهلكة

للقيام بذلك، أولاً نحتاج إلى وضع بعض حاويات البيانات الخام داخل OutputConfiguration.mSensorPixelModesUsed التي سيتم كتابتها عبر writeValue. اخترت Bundle. بهذه الطريقة في البيانات غير المستهلكة سيكون لدينا ما يلي:

  1. وسم VAL_BUNDLE الخاص بـ writeValue
  2. طول البيانات الخام (ينطبق هذا الرابط أيضًا على العناصر المتبقية في هذه القائمة)
  3. BUNDLE_MAGIC
  4. البيانات الخام الممررة حرفيًا عبر Parcel.appendFrom

لذا لدينا ثلاثة عناصر Parcel.writeInt غير مستهلكة، يمكننا التخلص منها عن طريق تغليف OutputConfiguration داخل بعض Parcelable الذي يقرأ أثناء قراءته قيمة Parcelable عشوائية متبوعة بثلاثة أعداد صحيحة. وجدت ذلك في ZenPolicy CREATOR

لتلخيص ذلك، لدينا التسلسل الهرمي للكائنات التالي (الموجود في system_server والذي يحاول تمريره إلى scheduleReceiver)

  • Intent
    • mClipData = ClipData
      • mItems.get(0).mActivityInfo = ActivityInfo
        • applicationInfo = ApplicationInfo
          • splitDependencies.get(0) = ZenPolicy
            • mVisualEffects.get(0) = OutputConfiguration
              • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
                • mHierarchyOps.get(0) = PackageManagerException
              • mSensorPixelModesUsed.get(1) = Bundle

يتم كتابة ذلك بواسطة system_server. ثم يقرأ التطبيق المستلم كل شيء حتى (ويشمل بيانات readSerializable الخاصة بـ) PackageManagerException بشكل طبيعي، ومع ذلك بعد قراءة بيانات Serializable الخاصة بـ PackageManagerException يتم إلقاء استثناء ويتم إلغاء قراءة كل شيء أسفل OutputConfiguration، تاركًا Bundle غير مقروء. تستمر القراءة إلى ZenPolicy الذي يستهلك ثلاثة أعداد صحيحة تسبق البيانات الخام داخل Bundle. ثم تستمر قراءة ApplicationInfo بقراءة البيانات التي كانت سابقًا بيانات خام ممررة حرفيًا في Bundle. ستستمر قراءة تلك البيانات الخام مع الكائنات المتبقية في هذه المكدس (ApplicationInfo، ActivityInfo، و) ثم سيتم استخدام تلك البيانات الخام لقراءة معامل طريقة التالي

ما يحدث بعد ذلك داخل handleReceiver

كما قلت للتو أدناه، تتم الآن قراءة معاملات scheduleReceiver المتبقية من المخزن المؤقت الذي يتحكم فيه المهاجم.

دعنا نلقي نظرة على ما يحدث بمجرد استدعاء تلك الطريقة.

أولاً، scheduleReceiver يجمع القيم من جميع الوسائط ويستخدم sendMessage() لتمرير التنفيذ إلى الخيط الرئيسي

بعد ذلك، على الخيط الرئيسي يتم استدعاء handleReceiver

يستدعي handleReceiver getPackageInfoNoCheck، مررًا إليه ApplicationInfo الذي استلمه كجزء من ActivityInfo الذي تم تمريره إلى وسيط scheduleReceiver

يتحقق getPackageInfo مما إذا كانت الحزمة بالاسم المحدد موجودة بالفعل في ذاكرة التخزين المؤقت وإذا لم تكن كذلك، فإنه ينشئ مثيل LoadedApk جديدًا، مررًا إليه كائن ApplicationInfo المستلم سابقًا (نظرًا لأن المهاجم يريد التسبب في إنشاء LoadedApk جديد، يتم استخدام packageName لحزمة لم تظهر سابقًا في هذه العملية)

ثم يتم استخدام طريقة ContextImpl.getClassLoader()، والتي في التشغيل الأول تفوض إلى mPackageInfo.getClassLoader()، مع كون mPackageInfo هو LoadedApk الذي تم إنشاؤه في الفقرة السابقة

ثم هناك createOrUpdateClassLoaderLocked، والذي يستدعي makePaths لملء zipPaths بالمسارات التي سيتم استخدامها في ClassLoader، ثم يتم ضمها وتعيينها إلى متغير zip ويتم تمرير ذلك إلى createClassLoader

يملأ makePaths zipPaths باستخدام معلومات من ApplicationInfo، والأهم من ذلك يتضمن هذا sourceDir. يجعل تطبيق المهاجم ApplicationInfo المحقون مع تعيين sourceDir إلى مسار ملف apk الخاص به، لذلك سيتم تحميل فئة المستقبِل فعليًا من apk المهاجم. يؤدي هذا مباشرة إلى تنفيذ كود يتحكم فيه المهاجم داخل التطبيق الذي يستقبل البث

ملاحظة حول فحوصات واجهات برمجة التطبيقات المخفية

كان هناك شيء آخر يجب تجاوزه: فحوصات واجهات برمجة التطبيقات المخفية. لم تكن هذه أبدًا مخصصة لتكون حدودًا أمنية (لأن التطبيق يمكنه دائمًا استخدام NDK واستدعاء استدعاءات النظام الأساسية مباشرة)، ولكن في هذه الحالة تم تجاوزها عن طريق بناء ClipData مصمم عن طريق كتابة البيانات يدويًا إلى Parcel ثم استخدام readParcelable. يمكن بعد ذلك إرفاق مثل هذا ClipData بشكل طبيعي بـ Intent ثم تمريره إلى sendBroadcast() بحيث تم إرسال البث نفسه باستخدام واجهات برمجة التطبيقات العامة فقط

الإصلاحات

تم إرسال التقرير أعلاه في الأصل إلى Google ويبدو أنهم استفادوا منه حيث توجد إصلاحات متعددة ناتجة عنه (أعتقد ذلك، ليس لدي دليل على السببية المباشرة)

تم إصدارها مع Android 12:

  • تمت إزالة ابتلاع الاستثناءات من OutputConfiguration والفئات ذات الصلة
  • OutputConfiguration#mSensorPixelModesUsed لم يعد يُكتب عبر writeValue
  • ClipData#mActivityInfo لم يعد يُكتب إلى Parcel إلا إذا تم طلبه صراحةً أثناء الكتابة (لذلك لم يعد بإمكان Intent احتواء Parcelable عشوائية من BOOTCLASSPATH، مما يلغي تقنية الاستغلال هذه)

موجود فقط على فرع master وقت الكتابة، وليس في الإصدارات الصادرة، ربما سيظهر في Android 13 (وليس في 12L):

  • توجد طرق قراءة List جديدة على Parcel تتحقق من نوع العناصر وتم وضع علامة على الإصدارات غير المكتوبة على أنها مهجورة
  • توجد طريقة جديدة Parcel#enforceNoDataAvail() تتحقق من عدم وجود بيانات غير مقروءة متبقية في Parcel، والتي يبدو أنها ستُستخدم بواسطة AIDL بعد قراءة وسائط استدعاء RPC. عادةً ما اعتمدت استغلالاتي على حقيقة أنه بعد قراءة جميع البيانات من Parcel، تم تجاهل كل شيء آخر، ولن يكون هذا هو الحال بعد الآن، على الرغم من أنني أعتقد أنه في كثير من الحالات يمكن للمرء بناء بيانات تتسبب في النهاية في الانتقال إلى الموضع النهائي لذا فإن هذا ليس تخفيفًا قويًا. على أي حال، تحدث مثل هذه المشكلات أحيانًا بشكل طبيعي دون اكتشافها لذا سيلتقط ذلك تلك الحالات. مزيد من النقاش حول ذلك في المشكلة #3
  • سيتم حفظ طول كل عنصر في Bundle بشكل منفصل. هذا يقتل إلى حد كبير فئة الثغرات بأكملها التي كنت أبلغ عنها لـ Google بشكل خاص منذ 2014، الوصف المنشور في 2017 والكود نفسه بعد حوالي عام. إذا كنت أحسب بشكل صحيح، فسيكون ذلك 8 سنوات من عمر فئة الثغرات (بصراحة ليس لدي أي فكرة عما إذا كان ذلك كثيرًا، على الرغم من أنه قد لا تزال هناك متغيرات استغلال غير ، مثل هذا بالضبط (على الرغم من أن هذا بالضبط تم إصلاحه بالفعل))
تنزيل الأداة
resolveClass
ObjectInputStream
c != null
Class.forName
PackageManagerException
Parcelable
readList
ClassLoader
WindowContainerTransaction
Parcel
PackageManagerException
ClassNotFoundException
ملفوفة في RuntimeException
يتم التقاطها بواسطة CREATOR الخاص بـ OutputConfiguration
ClipData
Intent
handleReceiver
Bundle