
CVE-2026-49881, un problema logico nella classe InCallController del servizio Telecom di Android 17 che consente a un'app senza privilegi di ottenere l'esecuzione arbitraria di codice come UID 1000 system_server.
Questo è un PoC e un writeup per CVE-2026-49881, un problema logico nella classe InCallController del servizio Telecom di Android 17 che consente a un'app senza privilegi di ottenere l'esecuzione arbitraria di codice come UID 1000 system_server senza alcuna interazione aggiuntiva da parte dell'utente. Mostriamo anche qui che l'esecuzione di codice in system_server può essere ancora facilmente adattata per ottenere la persistenza, anche nelle versioni moderne di Android.
Ho segnalato questa vulnerabilità all'Android Security Team il 2026-04-10, è stata confermata il 2026-05-06 ed è stata corretta nel bollettino sulla sicurezza Android di settembre 2026. (vedi qui per la patch)
Al momento della segnalazione, per quanto ne so, colpiva attivamente solo le build Pixel successive e successive ad Android 16 QPR3 (insieme alle build Beta 17), ma in seguito è arrivata alla release stabile AOSP di Android 17.
Per proteggerti da questo problema, assicurati di installare l'aggiornamento di sistema Google Play e la patch di sicurezza di sistema. (Telecom è un componente mainline da Android 17)
system_server utilizzando la vulnerabilità, registra id e stack trace su logcat e si re-installa come componente di system_server.build.sh incluso. In alternativa, puoi eseguire manualmente ./gradlew assembleSystemRelease, spostare l'app-system-release.apk risultante in app/src/poc/assets/system.apk, quindi eseguire ./gradlew assemblePocRelease.system_server disattiva anche forzatamente Play Protect impostando package_verifier_user_consent su -1 in Settings.Global perché a volte può intercettare la transazione di re-installazione a causa di firme sconosciute. Dovresti riattivarlo in Impostazioni dopo il test.system_server potrebbe richiedere una personalizzazione per ogni OEM perché si basa sull'attraversamento della struttura PMS utilizzando simboli che gli OEM a volte cambiano.L'output logcat previsto del PoC è:
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...
Questa è una vulnerabilità insolitamente diretta. Ogni volta che si verificano determinate azioni relative a Telecom, InCallController tenta di scoprire i servizi disponibili tramite getInCallServiceComponents. Questo si attiva naturalmente quando una chiamata viene registrata con il sistema, ma un'app può effettivamente attivarlo anche su richiesta grazie all'API di chiamate transazionali TelecomManager.addCall. (nota: questo è ciò che usa il pulsante Start Exploit nel PoC) Questa API richiede MANAGE_OWN_CALLS, ma è un'autorizzazione normale e invisibile all'utente concessa automaticamente al momento dell'installazione.
Questa enumerazione è implementata nelle versioni vulnerabili in questo modo:
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;
}
...
}
}
}
Nota come serviceClassExists venga eseguito su qualsiasi componente che dichiari un intent InCallService insieme al valore dei metadati android.telecom.CLASS_EXISTENCE_CHECK, non solo su InCallService validi/abilitati. (quel controllo è implementato più avanti nel ramo con getInCallServiceType e isServiceEnabled)
serviceClassExists è implementato come:
/**
* Verifies that the class for a given ServiceInfo exists within its package.
* This prevents a system crash if a service is declared in the manifest but its
* class was not included in the compiled code.
* @param serviceInfo The ServiceInfo of the service to check.
* @param userHandle The user under which to check for the service.
* @return {@code true} if the class exists, {@code false} otherwise.
*/
private boolean serviceClassExists(ServiceInfo serviceInfo, UserHandle userHandle) {
Log.i(this, "serviceClassExists check");
try {
Context packageContext = mContext.createPackageContextAsUser(
serviceInfo.packageName,
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY, userHandle);
ClassLoader classLoader = packageContext.getClassLoader();
Class.forName(serviceInfo.name, false, classLoader);
return true;
} catch (NameNotFoundException | ClassNotFoundException e) {
Log.w(this, "Skipping InCallService: class not found for " + serviceInfo.name);
return false;
} catch (Exception e) {
Log.e(this, e, "Error checking for existence of " + serviceInfo.name);
return false;
}
}
I pericoli di createPackageContext con CONTEXT_IGNORE_SECURITY sono ben documentati, e in questo caso è system_server stesso a eseguirlo su un componente non attendibile solo per verificare se una classe esiste nel DEX di quell'app. A prima vista, tuttavia, questo può sembrare sicuro perché il contesto ottenuto viene utilizzato in Class.forName con initialize=false - lo sviluppatore era molto probabilmente consapevole di questo rischio e ha specificato quell'argomento per garantire che la classe esterna non venga inizializzata in system_server.
Sfortunatamente, questa cautela arriva troppo tardi - il danno è già fatto da getClassLoader sul contesto esterno. Se l'app dell'attaccante definisce un AppComponentFactory nel suo manifest come android:appComponentFactory, il metodo getClassLoader su un LoadedApk corrispondente a un contesto creato con CONTEXT_INCLUDE_CODE e CONTEXT_IGNORE_SECURITY recupera prima il class loader predefinito dell'app esterna dal disco e poi esegue sia il costruttore della factory che il suo metodo instantiateClassLoader prima di restituire il class loader.
Poiché questa classe è controllabile dall'app dell'attaccante, questo porta immediatamente all'esecuzione arbitraria di codice nel contesto di Telecom.
Per mostrare le conseguenze dell'esecuzione riuscita di codice in system_server, il PoC dimostra la re-installazione di se stesso come componente persistente di system_server. (la tecnica è adattata dalla tecnica pubblicata nel PoC AbxOverflow / CVE-2024-34740 di Michał Bednarski)
Lo facciamo:
Signature della nostra app PoC riflettendo dinamicamente in PackageManagerService.mSettings direttamente una volta che siamo in system_server.SharedUserSetting corrispondente a "android.uid.system" e iniettando la Signature del PoC due volte nel suo getSigningDetails().mPastSigningCertificates con CertCapabilities.SHARED_USER_ID.android:sharedUserId="android.uid.system" e android:process="system" aggiunti al manifest. (che scarica anche la modifica volatile del passaggio precedente su packages.xml)Questo fa sì che canJoinSharedUserId() in PackageSignatures venga superato grazie alla corrispondenza della cronologia di rotazione e produce privilegi persistenti di system_server.
Una domanda che potresti avere è perché questa vulnerabilità esista affatto - in particolare, perché c'è un controllo di esistenza della classe opt-in protetto da un flag di metadati non documentato?
È impossibile dirlo con certezza, ma la risposta probabile a questo mistero può essere trovata scavando un po' più a fondo in AOSP: sia il controllo della classe che il confronto dei metadati sono stati probabilmente aggiunti per tenere conto di android.net.ConnectivityCallListenerService, che veniva implementato più o meno nello stesso periodo.
Questo servizio inizialmente era definito nel manifest del framework ma non era implementato da nessuna parte. (e quando è stato aggiunto, era dietro il flag di funzionalità Flags.FLAG_ENABLE_INCALL_SERVICE_API) Una volta osservati i crash nei test, qualcuno ha probabilmente deciso di implementare la correzione come controllo riutilizzabile di "difesa in profondità" piuttosto che come eccezione hardcoded - la definizione del manifest di questo servizio dice che definisce "android.telecom.CLASS_EXISTENCE_CHECK" per "indicare che la classe di questo servizio potrebbe non essere presente in tutte le build" e "dire a Telecom di verificare l'esistenza della classe prima di tentare il bind". (ed è per questo che siamo qui ora)