
Android chaîne d'exploitation complète permettant une élévation de privilèges d'une application locale non fiable vers root/noyau, combinant CVE-2026-49881 et CVE-2026-43284
Une chaîne d'exploitation complète qui permet une élévation de privilèges d'une application locale non fiable jusqu'à root/noyau. Elle n'implique ni corruption mémoire ni conditions de course, les attaquants n'ont donc pas besoin d'effectuer de complexes opérations de heap spraying ou de contourner les mitigations contre les vulnérabilités de corruption mémoire telles que KASLR, MTE ou CFI, ce qui rend cette chaîne d'exploitation efficace à 100 % sur les appareils vulnérables.
Testé sur Pixel 10 exécutant la version officielle initiale d'Android 17. Notez que cela ne fonctionne pas sur Pixel 6a et que ce problème peut également se produire sur d'autres appareils exécutant les arbres de noyau 6.1.xxx-android14 en raison d'un autre bug dans ces noyaux.
Utilisation : Installez l'application KernelSU, ouvrez cette application, cliquez sur « Run userspace exploit » puis « Run kernel exploit and load KernelSU ». Après une exploitation réussie, KernelSU sera activé et vous pourrez l'utiliser pour accorder un accès root à d'autres applications. Problème connu : si vous avez déjà exécuté l'exploit du noyau et souhaitez le réexécuter, vous devez redémarrer l'appareil.
Enregistrement d'écran : cliquez ici
La chaîne est composée de deux vulnérabilités distinctes : l'une est un 0-day dans le service Telecom, tandis que l'autre est un 1-day du noyau divulgué il y a 3 mois. Mais les appareils AOSP et Pixel (à l'exception de ceux exécutant les versions bêta QPR) restent vulnérables au moment de la rédaction.
La première vulnérabilité de la chaîne est un simple bug logique introduit dans Android 17. Il provient d'un changement fou, qui ajoute le code suivant à 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;
}
}
}
La méthode serviceClassExists() pertinente est définie comme suit :
/**
* 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;
}
}
C'est la vulnérabilité la plus incroyable que j'aie jamais vue. Le code utilise Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY pour charger du code depuis une application arbitraire ; bien qu'il semble exister certaines mesures destinées à empêcher l'exécution de code arbitraire, comme le passage de false à Class.forName() pour empêcher l'initialisation de la classe, l'application peut toujours déclarer un AppComponentFactory personnalisé qui est invoqué lorsque getClassLoader() est appelé.
D'autre part, le bug existe dans InCallController.java, qui fait partie du package com.android.server.telecom plutôt que de com.android.phone. Il convient de noter que le package déclare android:sharedUserId="android.uid.system" et android:process="system" dans AndroidManifest.xml, il s'exécute donc dans le processus system_server, l'un des processus userspace les plus privilégiés d'Android. Par conséquent, nous avons désormais la capacité d'exécuter du code Java arbitraire à l'intérieur de system_server.
C'est une surprise qu'un ingénieur Google puisse commettre une si grosse erreur à l'ère de l'IA. Nous l'avons trouvée et signalée à l'équipe de sécurité Android le 23 juillet 2026. Ils nous ont dit qu'il s'agissait d'un doublon. Google est passé à une publication trimestrielle du bulletin de sécurité mensuel, ce qui peut expliquer pourquoi la vulnérabilité n'a pas été corrigée 3 mois après la sortie d'Android 17.
La vulnérabilité a reçu le numéro CVE-2026-49881 et a été corrigée en septembre 2026 par Remove serviceClassExists logic to address security vulnerability.
Le premier bug nous permet d'élever les privilèges jusqu'à system, mais nous sommes encore loin de root. Un root complet nécessite au moins l'UID 0 et de ne pas être restreint par SELinux.
Il est maintenant temps de présenter le 1-day du noyau : les vulnérabilités DirtyFrag. Les principes sous-jacents ne seront pas détaillés ici ; veuillez vous référer au writeup du rapporteur d'origine. Il existe 2 variantes : CVE-2026-43500 nécessite RxRPC qui est désactivé pour les noyaux génériques Android ; CVE-2026-43284 nécessite xfrm-ESP et est exploitable sur Android. Cependant, SELinux interdit aux applications non fiables d'utiliser cette fonctionnalité :
# Privileged netlink socket interfaces.
neverallow { appdomain -network_stack }
domain:{
netlink_tcpdiag_socket
netlink_nflog_socket
netlink_xfrm_socket
netlink_audit_socket
netlink_dnrt_socket
} *;
Les seuls domaines autorisés sont system_server, network_stack et netd. Pour exploiter DirtyFrag, les attaquants doivent d'abord compromettre l'un des processus privilégiés de la liste blanche.
Combinons les deux bugs. Alors que le bug userspace nous permet d'exécuter du code Java à l'intérieur de system_server, SELinux interdit également à system_server de charger des bibliothèques natives depuis /data ou de mapper de la mémoire exécutable anonyme. Cela rend impossible l'utilisation de code natif, augmentant la difficulté de l'exploitation. Il serait donc préférable d'exécuter du code à l'intérieur de com.android.networkstack, qui peut charger du code natif depuis notre APK et dispose de privilèges suffisants pour exploiter DirtyFrag.
Heureusement, system_server est le processus où s'exécute ActivityManager. ActivityManager stocke les handles IApplicationThread de chaque processus d'application dans une map Java et comme nous nous exécutons dans le même processus qu'ActivityManager, nous pouvons les récupérer en utilisant la réflexion Java. Avec cela, nous pouvons envoyer des commandes arbitraires à com.android.networkstack pour le forcer à charger notre code. Pour plus d'informations sur cette astuce, veuillez vous référer à mon exploit précédent pour CVE-2026-0091.
DirtyFrag permet d'écraser des fichiers en lecture seule. C'est une primitive puissante dans le monde Linux, car nous pouvons écraser le binaire su qui possède le bit SUID. Cependant, nous n'avons pas de su dans le monde Android. Nous nous référons à l'exploit de polygraphene pour DirtyPipe pour transformer DirtyFrag en exécution de code noyau sur Android :
libc.so, libc++.so et /vendor/lib64/libstagefright_aidl_bufferpool2.so via DirtyFrag. libstagefright_aidl_bufferpool2.so est étiqueté comme domaine vendor_file et ne peut donc pas être accédé depuis le processus network stack. La solution consiste à patcher /apex/com.android.runtime/bin/crash_dump64 d'abord, à l'exécuter, et une fois que nous avons transité vers le domaine crash_dump, nous pouvons ouvrir libstagefright_aidl_bufferpool2.so.init. Parce que libc++.so est patché, notre code s'exécute avec l'UID 0 dans le domaine init. Nous exécutons ensuite /vendor/bin/modprobe pour transiter vers le domaine .vendor_modprobemodprobe est exécuté, parce que libc.so est également patché, notre code s'exécute sous le domaine vendor_modprobe. Nous pouvons maintenant charger des modules noyau mais uniquement pour les fichiers ayant des étiquettes spécifiées. Nous chargeons libstagefright_aidl_bufferpool2.so qui a l'étiquette vendor_file.libstagefright_aidl_bufferpool2.so, le contenu réel de ce fichier a été remplacé par notre propre module noyau. Le module noyau est chargé, et maintenant nous pouvons tout faire, y compris ajuster la politique SELinux ou définir SELinux en mode permissif.