
Android catena di exploit completa che consente l'escalation dei privilegi da un'app locale non attendibile a root/kernel, combinazione di CVE-2026-49881 e CVE-2026-43284
Una catena di exploit completa che consente l'elevazione dei privilegi da un'app locale non attendibile a root/kernel. Non coinvolge corruzioni di memoria o race condition, quindi gli attaccanti non devono eseguire complessi heap spraying o bypassare mitigazioni contro vulnerabilità di corruzione della memoria come KASLR, MTE o CFI, rendendo questa catena di exploit un tasso di successo del 100% sui dispositivi vulnerabili.
Testato su Pixel 10 con la release ufficiale iniziale di Android 17. Nota che non funziona su Pixel 6a e questo problema potrebbe verificarsi anche su altri dispositivi con kernel 6.1.xxx-android14 a causa di un altro bug in questi kernel.
Utilizzo: installa l'app KernelSU, aprilo, clicca su "Run userspace exploit" e poi su "Run kernel exploit and load KernelSU". Dopo uno sfruttamento riuscito, KernelSU verrà attivato e potrai usarlo per concedere accesso root ad altre app. Problema noto: se hai già eseguito l'exploit del kernel e vuoi eseguirlo di nuovo, devi riavviare il dispositivo.
Registrazione dello schermo: clicca qui
La catena è composta da due vulnerabilità distinte: una è uno 0-day nel servizio Telecom, mentre l'altra è un kernel 1-day divulgato 3 mesi fa. Ma AOSP e dispositivi Pixel (tranne quelli con versioni beta QPR) rimangono vulnerabili al momento della scrittura.
La prima vulnerabilità della catena è un semplice bug logico introdotto in Android 17. Ha origine da una modifica folle, che aggiunge il seguente codice a InCallController.java:
PackageManager packageManager = mContext.getPackageManager();
Context userContext = mContext.createContextAsUser(userHandle,
0 /* flags */);
PackageManager userPackageManager = userContext != null ?
userContext.getPackageManager() : packageManager;
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;
}
}
}
Il relativo metodo serviceClassExists() è definito come segue:
/**
* 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;
}
}
Questa è la vulnerabilità più incredibile che abbia mai visto. Il codice usa Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY per caricare codice da un'app arbitraria; sebbene sembrino esserci alcune misure pensate per prevenire l'esecuzione di codice arbitrario, come passare false a Class.forName() per impedire l'inizializzazione della classe, l'app può comunque dichiarare un AppComponentFactory personalizzato che viene invocato quando viene chiamato getClassLoader().
D'altra parte, il bug esiste in InCallController.java, che fa parte del pacchetto com.android.server.telecom piuttosto che di com.android.phone. Vale la pena notare che il pacchetto dichiara android:sharedUserId="android.uid.system" e android:process="system" in AndroidManifest.xml, quindi viene eseguito nel processo system_server, uno dei processi userspace più privilegiati in Android. Pertanto, ora abbiamo la capacità di eseguire codice Java arbitrario all'interno di system_server.
È una sorpresa che anche un ingegnere Google possa commettere un errore così grande nell'era dell'IA. L'abbiamo trovato e segnalato all'Android Security Team il 23 luglio 2026. Ci hanno detto che era un duplicato. Google ha spostato il bollettino di sicurezza mensile a una release trimestrale, il che potrebbe spiegare perché la vulnerabilità non è stata corretta 3 mesi dopo il rilascio di Android 17.
Alla vulnerabilità è stato assegnato CVE-2026-49881 ed è stata corretta in settembre 2026 tramite Rimozione della logica serviceClassExists per affrontare la vulnerabilità di sicurezza.
Il primo bug ci consente di elevare i privilegi a system, ma è ancora lontano da root. Un root completo richiede almeno UID 0 e di non essere limitato da SELinux.
Ora è il momento di introdurre il kernel 1-day: le vulnerabilità DirtyFrag. I principi sottostanti non verranno elaborati qui; fare riferimento al writeup del reporter originale. Ci sono 2 varianti: CVE-2026-43500 richiede RxRPC che è disabilitato per gli Android Generic Kernel; CVE-2026-43284 richiede xfrm-ESP ed è sfruttabile su Android. Tuttavia, SELinux vieta alle app non attendibili di usare quella funzionalità:
# Privileged netlink socket interfaces.
neverallow { appdomain -network_stack }
domain:{
netlink_tcpdiag_socket
netlink_nflog_socket
netlink_xfrm_socket
netlink_audit_socket
netlink_dnrt_socket
} *;
Gli unici domini consentiti sono system_server, network_stack e netd. Per sfruttare DirtyFrag, gli attaccanti devono prima compromettere uno dei processi privilegiati nella lista consentita.
Combina i due bug insieme. Mentre il bug userspace ci consente di eseguire codice Java all'interno di system_server, SELinux vieta anche a system_server sia di caricare librerie native da /data sia di mappare memoria eseguibile anonima. Questo rende impossibile usare codice nativo, aumentando la difficoltà dello sfruttamento. Sarebbe quindi preferibile eseguire codice all'interno di com.android.networkstack, che può caricare codice nativo dal nostro APK e ha privilegi sufficienti per sfruttare DirtyFrag.
Fortunatamente, system_server è il processo in cui viene eseguito ActivityManager. ActivityManager memorizza gli handle IApplicationThread di ogni processo app in una mappa Java e poiché eseguiamo nello stesso processo di ActivityManager possiamo recuperarli usando la riflessione Java. Con questo, possiamo inviare comandi arbitrari a com.android.networkstack per forzarlo a caricare il nostro codice. Per maggiori informazioni su questo trucco, fare riferimento al mio precedente exploit per CVE-2026-0091.
DirtyFrag consente di sovrascrivere file di sola lettura. Questa è una primitiva potente nel mondo Linux, perché possiamo sovrascrivere il binario su che ha il bit SUID. Tuttavia, non abbiamo su nel mondo Android. Facciamo riferimento all'exploit di polygraphene per DirtyPipe per trasformare DirtyFrag in esecuzione di codice kernel su Android:
libc.so, libc++.so e /vendor/lib64/libstagefright_aidl_bufferpool2.so tramite DirtyFrag. libstagefright_aidl_bufferpool2.so è etichettato come dominio vendor_file quindi non può essere accessibile dal processo network stack. La soluzione è patchare prima /apex/com.android.runtime/bin/crash_dump64, eseguirlo, e una volta che transitiamo al dominio crash_dump possiamo aprire libstagefright_aidl_bufferpool2.so.init. Poiché libc++.so è patchato, il nostro codice viene eseguito come UID 0 con il dominio init. Eseguiamo quindi /vendor/bin/modprobe per transitare al dominio .vendor_modprobemodprobe viene eseguito, poiché anche libc.so è patchato, il nostro codice viene eseguito sotto il dominio vendor_modprobe. Ora possiamo caricare moduli del kernel ma solo per file che hanno etichette specifiche. Carichiamo libstagefright_aidl_bufferpool2.so che ha l'etichetta vendor_file.libstagefright_aidl_bufferpool2.so, il contenuto reale di quel file è stato sostituito dal nostro modulo del kernel. Il modulo del kernel viene caricato, e ora possiamo fare qualsiasi cosa, incluso modificare la policy SELinux o impostare SELinux a permissive.