
كتابة وتحليل استغلال لثغرة CVE-2025-22441: تصعيد الامتيازات من تطبيق مثبت إلى عملية SystemUI على أندرويد بسبب تمرير ApplicationInfo غير موثوق إلى LoadedApk
تم إصلاح هذه المشكلة في CVE-2025-22441: النشرة التصحيح المتابعة
ApplicationInfo حول النظامApplicationInfo هو بنية تُعرّف معلومات متنوعة حول التطبيق المثبّت، وأبرزها مسار ملف apk الذي يتم تحميل الموارد والكود منه
عادةً يتم تمريره من النظام إلى التطبيقات، ومع ذلك توجد حالات قد يمرر فيها مُستدعي غير تابع للنظام كائنًا خاصًا به. على سبيل المثال، في الماضي كانت هناك ثغرة في طريقة bindBackupAgent()، حيث يمكن للمهاجم تمرير كائن ApplicationInfo خاص به في المعامل مع قيم uid وsourceDir دون التحقق منها مقابل التطبيقات المثبّتة فعليًا في النظام، لأن تلك الطريقة كان من المفترض أن تُستدعى داخليًا بواسطة system_server، لكنها كانت مكشوفة لـ adb shell
لكن هذه المرة، نظرت عن كثب إلى حقل ApplicationInfo داخل RemoteViews
RemoteViews هو كائن يصف عرضًا يمكن أن يأتي من عملية أخرى. يُستخدم هذا بشكل أبرز لأدوات الشاشة الرئيسية، حيث يقوم التطبيق الذي يوفر الأداة ببناء RemoteViews ثم يتم "تطبيقه" داخل عملية الشاشة الرئيسية
الأماكن الأخرى التي تُستخدم فيها RemoteViews هي الإشعارات (يتم تطبيقها بواسطة عملية SystemUI) وحوارات الملء التلقائي (يتم توفيرها بواسطة خدمة الملء التلقائي، وتُطبق بواسطة system_server)
حقل RemoteViews.mApplication يتم تسلسله عبر Parcel وبالتالي قد يأتي من عمليات بعيدة، وكلما تم تطبيق RemoteViews يُستخدم بواسطة الطريقة التالية (مصدر المقتطف):```java
private Context getContextForResourcesEnsuringCorrectCachedApkPaths(Context context) {
if (mApplication != null) {
if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
&& context.getPackageName().equals(mApplication.packageName)) {
return context;
}
try {
LoadedApk.checkAndUpdateApkPaths(mApplication);
return context.createApplicationContext(mApplication,
Context.CONTEXT_RESTRICTED);
} catch (NameNotFoundException e) {
Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found");
}
}
return context;
}
الأكثر إثارة للاهتمام هنا هو استدعاء `LoadedApk.checkAndUpdateApkPaths()`، حيث أن هذه طريقة ثابتة وستقوم بتعديل بعض الحالة العامة [(مصدر المقتطف)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=2275-2302;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)```java
public static void checkAndUpdateApkPaths(ApplicationInfo expectedAppInfo) {
// Get the LoadedApk from the cache
ActivityThread activityThread = ActivityThread.currentActivityThread();
if (activityThread == null) {
Log.e(TAG, "Cannot find activity thread");
return;
}
checkAndUpdateApkPaths(activityThread, expectedAppInfo, /* cacheWithCode */ true);
checkAndUpdateApkPaths(activityThread, expectedAppInfo, /* cacheWithCode */ false);
}
private static void checkAndUpdateApkPaths(ActivityThread activityThread,
ApplicationInfo expectedAppInfo, boolean cacheWithCode) {
String expectedCodePath = expectedAppInfo.getCodePath();
LoadedApk loadedApk = activityThread.peekPackageInfo(
expectedAppInfo.packageName, /* includeCode= */ cacheWithCode);
// If there is load apk cached, or if the cache is valid, don't do anything.
if (loadedApk == null || loadedApk.getApplicationInfo() == null
|| loadedApk.getApplicationInfo().getCodePath().equals(expectedCodePath)) {
return;
}
// Duplicate framework logic
List<String> oldPaths = new ArrayList<>();
LoadedApk.makePaths(activityThread, expectedAppInfo, oldPaths);
// Force update the LoadedApk instance, which should update the reference in the cache
loadedApk.updateApplicationInfo(expectedAppInfo, oldPaths);
}
لنناقش ما يحدث في هذه الدوال
أولاً، نستدعي الدالة checkAndUpdateApkPaths ذات الثلاث معاملات مع تعيين cacheWithCode على كل من true و false. يمكن إنشاء كائن LoadedApk في وضعين، إما مع تعيين mIncludeCode على true أو false، وهو ما يُرتبط على مستوى SDK بـ Context مع تعيين علم CONTEXT_INCLUDE_CODE أو عدم تعيينه
ستستخدم تلك الدالة أولاً ActivityThread.peekPackageInfo()، والتي ستعيد مثيل LoadedApk المخزن مؤقتاً بالفعل، لذلك إذا لم يقم التطبيق سابقاً بإنشاء LoadedApk مع تطابق packageName و includeCode، فستعيد peekPackageInfo() قيمة null ولن تقوم checkAndUpdateApkPaths() بأي شيء
بالعودة إلى RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths()، لدينا context.createApplicationContext(mApplication, Context.CONTEXT_RESTRICTED) هناك، ولم يتم تحديد علم CONTEXT_INCLUDE_CODE (وكذلك الحال مع السياقات المُمررة كوسيط لتلك الدالة)، وبالتالي ستستخدم تلك الدالة دائماً LoadedApk مع mIncludeCode=false
لذلك، بالنسبة لإصدار RemoteViews، فإن تحديث الإصدار مع الكود غير ضروري، لأن RemoteViews تستخدم دائماً Context بدون كود. رسالة الالتزام تقول فقط أن هناك مكانين قد يتم فيهما تخزين ApplicationInfo مؤقتاً وأعتقد أن إزالة تحديث الإصدار مع الكود لن تسبب مشاكل هنا (حيث أن checkAndUpdateApkPaths() تُستخدم فقط بواسطة AppWidgetHostView و RemoteViews). ومع ذلك، فإن الحجة المضادة لذلك هي تجنب احتمالية عدم تزامن المخابئ المختلفة، وبينما أن إزالة الاستدعاء مع cacheWithCode=true يزيل التأثير الأشد خطورة لهذه الثغرة، فإنه لا يصلح المشكلة بالكامل، لأن تعديل الموارد فقط (بدلاً من الكود) قد يظل ذا قيمة للمهاجم
LoadedApk.updateApplicationInfo()حتى الآن، الكود المعروض يُستخدم فقط لـ RemoteViews (الأدوات، الإشعارات، إلخ)، لكننا الآن سندخل إلى LoadedApk.updateApplicationInfo() والتي تُستخدم أيضاً لتحديث عمليات التطبيق الجارية بعد تثبيت split جديد (على سبيل المثال عند استخدام Play Feature Delivery للتسليم عند الطلب)
الآن سيتم تمرير كائن ApplicationInfo غير الموثوق من RemoteViews إلى updateApplicationInfo()، لنلقِ نظرة على ما تفعله تلك الدالة (مصدر المقتطف)```java
public void updateApplicationInfo(@NonNull ApplicationInfo aInfo,
@Nullable List oldPaths) {
if (!setApplicationInfo(aInfo)) {
return;
}
دعنا نلقي نظرة على `setApplicationInfo()` [(مصدر المقتطف)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=392-422;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)```java
private boolean setApplicationInfo(ApplicationInfo aInfo) {
if (mApplicationInfo != null && mApplicationInfo.createTimestamp > aInfo.createTimestamp) {
Slog.w(TAG, "New application info for package " + aInfo.packageName
+ " is out of date with TS " + aInfo.createTimestamp + " < the current TS "
+ mApplicationInfo.createTimestamp);
return false;
}
// Snip: assign fields such as mAppDir and mResDir on this object from aInfo
return true;
}
createTimestamp field is normally set to SystemClock.uptimeMillis()، ولكن نظرًا لأن هذا الكائن يأتي من المهاجم، فهذا يعني أن المهاجم يمكنه توفير قيمة مستقبلية لمنع استدعاءات updateApplicationInfo() اللاحقة من التنفيذ
بالعودة إلى updateApplicationInfo() (مصدر المقتطف)```java
final List newPaths = new ArrayList<>();
makePaths(mActivityThread, aInfo, newPaths);
final List addedPaths = new ArrayList<>(newPaths.size());
// Snip: populate addedPaths with items that are in newPaths and not in oldPaths (passed in argument) synchronized (mLock) { createOrUpdateClassLoaderLocked(addedPaths);
قائمة `addedPaths` يتم بناؤها: في حالة تثبيت تقسيم جديد، ستحتوي `oldPaths` على قائمة المسارات التي كانت مستخدمة قبل تثبيت التقسيم الجديد، وستحتوي `addedPaths` على قائمة ملفات `.apk` التي يجب إضافتها إلى `ClassLoader` الموجود. ومع ذلك، في حالة `checkAndUpdateApkPaths()`، سيتم بناء `oldPaths` من نفس كائن `ApplicationInfo` تمامًا، وبالتالي ستكون `addedPaths` فارغة ولن يتمكن المهاجم من إضافة مسارات جديدة إلى `ClassLoader` الموجود هنا.
`createOrUpdateClassLoaderLocked()` هي طريقة طويلة، لكن القليل من الأمور فقط هي المثيرة للاهتمام هنا:
* [إذا كانت `mIncludeCode` بقيمة `false`، فإن الشيء الوحيد الذي يتم فعله هو إنشاء `ClassLoader` دون الإشارة إلى أي ملفات `apk`/`dex`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=968-990;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)
* أخطر شيء هو إنشاء `ClassLoader` جديد باستخدام مسارات تم تعيينها للتو باستخدام `ApplicationInfo` الذي يتحكم فيه المهاجم، لكن هذا سيحدث فقط إذا كانت [`mDefaultClassLoader` بقيمة `null`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1005;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)، مما يعني أن هذه هي أول استدعاء لـ `createOrUpdateClassLoaderLocked()` على مثيل `LoadedApk` هذا (يشير حقل `mDefaultClassLoader` إلى مثيل `ClassLoader` المستخدم قبل تطبيق [`AppComponentFactory.instantiateClassLoader()`](https://developer.android.com/reference/android/app/AppComponentFactory#instantiateClassLoader(java.lang.ClassLoader,%20android.content.pm.ApplicationInfo)))
* هناك أيضًا [إضافة مسارات جديدة إلى مسار البحث عن المكتبات الأصلية لذلك `ClassLoader`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1050-1058;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)، ومع ذلك لاستغلال ذلك، سيحتاج تطبيق الضحية إلى تنفيذ `System.loadLibrary()` وهو أمر غير موجود عادةً
* وهناك [إضافة مسارات `apk`/`dex` من وسيط `addedPaths` إلى `ClassLoader` الموجود](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1060-1065;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)، ومع ذلك في مسار الكود من `RemoteViews` ستكون `addedPaths` فارغة
بالعودة إلى `updateApplicationInfo()` مرة أخرى، لا يزال هناك شيء واحد ذو صلة تفعله هذه الطريقة، وهو [استبدال `LoadedApk.mResources` بمثيل يستخدم قيمة `mResDir` الجديدة](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=382;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1). تجدر الإشارة إلى أن هذا مثيل جديد، وليس تحديثًا للمثيلات الموجودة بالفعل. ستعيد `Context`s المنشأة حديثًا/ستستخدم مثيل `Resources` الجديد هذا، ومع ذلك فإن أي `Context`s تم إنشاؤها بالفعل ستستمر في استخدام `Resources` القديم.
# ملخص التأثير
لتلخيص الأقسام أعلاه، كلما طبق عملية الضحية `RemoteViews`، تسمح هذه الثغرة بـ:
* استبدال [`Resources` (سلاسل الترجمة، التخطيطات، إلخ)](https://developer.android.com/guide/topics/resources/providing-resources) للأنشطة المنشأة حديثًا داخل تلك العملية
* الإلحاق بمسار البحث عن المكتبات الأصلية المستخدم بواسطة `System.loadLibrary()`، ومع ذلك فإن استغلال ذلك سيتطلب من الضحية استدعاء `System.loadLibrary()` مع تمرير اسم مكتبة غير موجودة عادةً، وهو أمر غير مرجح
* تحميل كود Java عشوائي إذا كانت عملية الضحية قد استخدمت `createPackageContext(CONTEXT_INCLUDE_CODE)`، لكنها لم تستدعِ [`getClassLoader()` على ذلك `Context`](https://developer.android.com/reference/android/content/Context#getClassLoader())، ومع ذلك نظرًا لأن تحميل الكود هو سبب استخدام علامة `CONTEXT_INCLUDE_CODE`، فمن غير المرجح أيضًا أن يحدث هذا بشكل طبيعي
الآن، بينما من غير المرجح أن تحدث فرصة تحميل كود Java في هذه الحالة بشكل طبيعي، تمكنت من تشغيلها
# تحميل `WebView`
في إصدارات Android الحديثة، `WebView` ليس جزءًا من النظام، بل يتم تحميله من `apk` عادي معرف في [تكوين النظام](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/res/res/xml/config_webview_packages.xml) و[إما أن يكون تطبيق نظام أو لديه توقيع يطابق التوقيع المعرف داخل النظام](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java;l=688-705;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928)
يتم تحميل ذلك `apk` عن طريق [إنشاء `Context` جديد، مع تمرير علامات `Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/webkit/WebViewFactory.java;l=521;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928)
لنلقِ الآن نظرة على المتصل بتلك الطريقة [(مصدر المقتطف)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/webkit/WebViewFactory.java;l=541-563;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928)```java
// Overall snip: try/catch/finally, Trace, logging and timing measurement
webViewContext = getWebViewContextAndSetProvider();
if (android.content.res.Flags.registerResourcePaths()) {
Resources.registerResourcePaths(webViewContext.getPackageName(),
webViewContext.getApplicationInfo());
} else {
// Snip: old resource update path, won't be used on latest Android builds
}
ClassLoader clazzLoader = webViewContext.getClassLoader();
getWebViewContextAndSetProvider() هي طريقة تنشئ Context، وResources.registerResourcePaths() هي فرصتنا الوحيدة لإدخال تأخير يسمح لنا باستدعاء checkAndUpdateApkPaths() في خيط آخر وتحميل كود Java عشوائي، حيث أن webViewContext.getClassLoader() هو نهاية النافذة التي يوجد فيها LoadedApk مع mIncludeCode بقيمة true وmDefaultClassLoader بقيمة null
ستصل Resources.registerResourcePaths() إلى appendLibAssetsLocked()، والتي ستقوم بالتكرار على حقل mResourceImpls الخاص بالكائن المفرد، والذي يحتوي على جميع الموارد التي تم تحميلها في هذه العملية وبالتالي قد يحتوي على موارد مبنية باستخدام ApplicationInfo تم زرعها عبر RemoteViews
كانت فكرتي الأولية هي وضع عدد كبير من مسارات overlays في ApplicationInfo بحيث يكون لجميعها نفس hashCode()، مما يجعل إزالة التكرار بطيئة عبر استدعاء createNewResourceKeyIfNeeded()، وبينما كان هذا تأخيرًا كبيرًا عند استخدام المفسّر (على سبيل المثال عند استخدام مصحح الأخطاء أو داخل تطبيق مثبت حديثًا)، فعندما يقوم وقت التشغيل بالتحسينات لم يكن التأخير الناتج عن تصادمات التجزئة كبيرًا، ومع ذلك ظهر سبب آخر للتباطؤ: نظرًا لأن مسارات overlays هذه لم تكن تشير إلى ملفات موجودة، فلكل overlay يفشل في التحميل يتم طباعة رسالة سجل مع تتبع المكدس وهذا عمليًا أدى إلى التباطؤ، مما جعل استغلال حالة السباق هذه ممكنًا
يمكن للتطبيقات تمرير RemoteViews إلى SystemUI في الإشعارات. يطلب تطبيق الاستغلال الخاص بي إذن POST_NOTIFICATIONS، والذي أعتقد أنه تفاعل مستخدم معقول، على الرغم من أنه قد يكون من الممكن لبعض الإشعارات تجاوز هذا المطلب
عادةً لا يستخدم SystemUI WebView وفي الواقع لا يمكنه حتى القيام بذلك، لأن SystemUI يستخدم التخزين المحمي بالجهاز (أي التخزين غير المحمي ببيانات اعتماد شاشة القفل) وتنفيذ WebView يمنع ذلك
ومع ذلك، يسمح لي هذا الاستغلال بتعديل Resources، لذا قمت أولاً بتعديل التخطيط المستخدم بواسطة SlicePermissionActivity ليشمل عنصر <WebView />، ثم قمت بتشغيل هذا النشاط، مما يؤدي إلى تشغيل تهيئة WebView على الخيط الرئيسي لـ SystemUI
ثم يتعين عليّ استدعاء RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths() على خيط آخر من أجل استبدال المسار المستخدم لتحميل الكود
عادةً ما يتضمن نشر الإشعار خيوطًا متعددة في عملية SystemUI، بما في ذلك الخيط الرئيسي (لذلك بينما يتم تهيئة WebView لا يمكنني نشر إشعار آخر)، ومع ذلك فإن تطبيق RemoteViews يحدث على خيط منفصل ويمكنني تعليق هذه العملية من خلال وجود ImageView داخل RemoteViews وطلب تحميل صورة من ContentProvider الخاص بي
يقوم هذا الاستغلال بتحميل WebView عن طريق استبدال التخطيط المستخدم في SlicePermissionActivity. إذا لم يكن تحديث الكود ممكنًا، فإن مهاجمة SlicePermissionActivity قد تظل ذات قيمة، حيث يمكن للمهاجم استبدال التخطيط لإخفاء الرسالة الأصلية تمامًا وعلى سبيل المثال عرض سجل التغييرات، واستبدال زر الموافقة بعبارة "فهمت" وزر الرفض بسلسلة فارغة مما يجعله غير مرئي فعليًا. بينما إطار عمل Slices مهمل، لا تزال هناك شرائح الإعدادات والتي يمكنها على سبيل المثال تغيير إعداد بيانات الجوال دون تفاعل المستخدم
مطالبة إذن مهمة أخرى داخل SystemUI هي تأكيد Media Projection، ومع ذلك لم تكن هذه المرة عرضة للهجوم لأنها تستخدم سياق التطبيق لـ Dialog
كما أن أجزاء التفضيلات مثيرة للاهتمام، حيث يمكنك تحديد Intent ليتم تشغيله بواسطة Preference، ومع ذلك في حالة SystemUI فإن أنشطة التفضيلات الوحيدة هي تلك المتعلقة بـ SystemUI Tuner وDemo Mode، ولكن هذه تعمل في عملية مختلفة عن تلك التي تعرض الإشعارات