
adb-driven KernelSU loader pour Google Pixel d'origine : accès R/W temporaire au noyau via CVE-2026-43499 (GhostLock), puis chargement tardif d'un kernelsu.ko à signature correspondante pour le KMI en cours d'exécution. Indépendant du gestionnaire.
Un outil piloté par adb qui transforme un Google Pixel stock avec bootloader verrouillé en appareil rooté KernelSU sans déverrouiller le bootloader ni modifier l'image de boot. Depuis l'hôte, il exécute une exploitation de noyau en espace utilisateur non privilégiée sur l'appareil pour obtenir un accès lecture/écriture temporaire au noyau, utilise cette primitive pour patcher un identifiant root, puis charge tardivement un module noyau chargeable KernelSU (kernelsu.ko) dans le noyau GKI en cours d'exécution et confie le contrôle au gestionnaire KernelSU déjà installé (KernelSU, KernelSU-Next, SukiSU, ou une autre variante). Il cible les Pixel Android 17 sur les noyaux GKI 6.1 et 6.6 et pilote l'intégralité du flux via adb shell pour un séquencement déterministe et des journaux horodatés.
La charge utile côté appareil est l'exploitation LPE complète vendue pour CVE-2026-43499 (« GhostLock »), une use-after-free de pile dans l'héritage de priorité futex/rtmutex dans kernel/locking/rtmutex.c. Sur le chemin de rollback requeue-PI, remove_waiter() efface pi_blocked_on sur le requeueur (current) plutôt que sur le vrai waiter, laissant un pointeur pendant vers un emplacement de pile noyau libéré qui contenait un rt_mutex_waiter. Le bug est accessible depuis un processus non privilégié ordinaire :
owner, waiter, consumer) construisent une chaîne PI ; le waiter se gare sur FUTEX_WAIT_REQUEUE_PI, le thread principal déclenche FUTEX_CMP_REQUEUE_PI, et un sched_setattr du consumer pilote le rollback.pselect()/select() contrôlé (ou une route TCP_ZEROCOPY_RECEIVE sur certaines cibles 6.1) dont les mots fd_set atterrissent sur la structure waiter, écrivant un rt_mutex_waiter plat forgé afin que le pointeur pendant parcoure des champs rb-tree et lock contrôlés par l'attaquant — une primitive d'écriture de pointeur unique contrôlée.mm_struct// pulvérisés.L'état root et SELinux est ensuite patché via la primitive pipe : le cred de la tâche enfant root est mis à zéro pour uid/gid 0 avec des ensembles de capacités complets, son osid/sid SELinux défini sur SECINITSID_KERNEL, seccomp effacé, et selinux_state.enforcing défini sur 0.
La chaîne d'exploitation, l'oracle KASLR et le canal secondaire KernelSnitch proviennent de la recherche IonStack Part II — GhostLock de NebuSec (le PoC NebuSec/CyberMeowfia, Apache-2.0), adaptée ici pour Pixel/aarch64. Voir Attribution et licence.
Exactement une étape de la chaîne peut faire paniquer le noyau : la dérivation du slide KASLR, qui entre en course avec une page qu'elle espère avoir récupérée. Toutes les autres étapes peuvent être réessayées en toute sécurité, et la base du texte noyau est fixe pour la durée d'un seul boot. Le flux hôte se divise sur cette propriété :
KASLR_BASE dans son environnement. L'écriture du waiter forgé redirige le ctl_table.data du sysctl random_table vers un pointeur de texte noyau connu ; la lecture de /proc/sys/kernel/random/boot_id le divulgue via proc_do_uuid(), et la soustraction du décalage d'image donne _stext/la base KASLR. restore_slide_boot_id() répare le ctl_table.data corrompu. Comme une course perdue redémarre l'appareil, une attente de boot précède chaque tentative et une vérification de vivacité classe une disparition comme une panique. En cas de succès, le journal de l'appareil émet slide-kaslr-ok pid=<pid> base=<hex>, et la base est épinglée au boot actuel.KASLR_BASE=0x<base> exporté. Ce chemin ne panique jamais et est bouclé jusqu'à ce que id rapporte via le su temporaire.L'appareil connecté est résolu contre data/targets.json à l'exécution ; rien n'est codé en dur pour l'appareil. Deux résolutions indépendantes se produisent :
uname -r), soit depuis l'entrée cible correspondante, soit dérivé de la chaîne de version (par ex. android14-6.1).La réutilisation d'une charge utile sur de nombreux appareils découle de la structure GKI/KMI. Chaque appareil sur le même build GKI exécute le vmlinux identique octet pour octet, et les décalages de champs de structures (task_struct->cred, cred->uid, …) sont figés pour la durée d'une branche KMI par le contrat de type KMI et l'application CRC MODVERSIONS. Les adresses absolues de symboles noyau, en revanche, sont décidées par l'éditeur de liens par build ab<NNN>, donc les décalages fixes de l'exploitation appartiennent à un vmlinux spécifique ; des images noyau distinctes nécessitent donc des charges utiles distinctes même lorsque leur KMI correspond. data/targets.json encode exactement cela : de nombreux appareils se dédupliquent sur une charge utile clé par image noyau, tandis qu'une image noyau différente obtient la sienne.
Le chargement tardif du LKM nécessite un noyau GKI (5.10+) avec prise en charge des modules chargeables et un .ko correspondant au KMI. Le module noyau KernelSU authentifie son gestionnaire en vérifiant le bloc de signature v2 de l'APK du gestionnaire dans le noyau et en comparant le SHA-256 du certificat de signature contre une paire KSU_EXPECTED_SIZE/KSU_EXPECTED_HASH compilée dans le .ko. Le kernelsu.ko fourni dans une version du gestionnaire et l'APK de ce gestionnaire partagent donc une identité de signature ; un ksud incompatible charge le pilote mais ne définit jamais le bit d'autorisation du gestionnaire, laissant l'appareil sans root utilisable.
Le flux honore cette liaison : il résout le chemin de l'APK du gestionnaire installé (pm path <package du gestionnaire>), récupère l'APK, extrait lib/arm64-v8a/libksud.so comme binaire ksud, et si aucun gestionnaire n'est installé, il s'arrête. Avec le root temporaire détenu, ce ksud est mis en scène comme exécutable appartenant à root et invoqué comme ksud late-load --kmi <kmi> --package-name <package du gestionnaire>. Le chargement tardif détecte le KMI actuel, récupère "{kmi}_kernelsu.ko" depuis ses ressources intégrées, effectue une relocalisation manuelle des symboles (résolvant chaque symbole SHN_UNDEF contre /proc/kallsyms, réécrivant les entrées en SHN_ABS), et appelle init_module(2) sur le tampon patché. Il exécute ensuite le pipeline de boot restant que init ferait (installer ksud, restorecon, charger sepolicy.rule et les profils root, exécuter les scripts post-fs-data/stage, monter l'overlay de modules).
Le chargement tardif se démonifie et ré-applique SELinux dans son enfant forké, qui démonte le démon su temporaire de l'exploitation ; la vérification ne doit donc pas passer par su. À la place, le pilote chargé est interrogé directement via sa surface d'appels système, accessible depuis un shell simple sans root : ksud debug version est interrogé et la version du noyau rapportée est analysée. Une version non vide et non nulle confirme que le pilote est résident et répond. Le chemin d'installation du pilote est le mécanisme magique reboot(2) → fd d'installation → KSU_IOCTL_GET_INFO (reboot(0xDEADBEEF, 0xCAFEBABE, 0, &fd) installe un fd anonyme [ksu_driver] ; GET_INFO retourne {version, flags, features, uapi_version}), avec le canal hérité prctl(0xDEADBEEF, …) sondé comme solution de repli. La même sonde exécutée au démarrage court-circuite tout le flux lorsque le module est déjà résident pour le boot actuel.
L'exploitation met en scène son su temporaire à /apex/com.android.virt/bin/su, sur un tmpfs monté sur ce répertoire bin de l'apex dans l'espace de noms de montage d'adbd, car ce répertoire précède /system/bin dans le PATH du shell — donc un su nu dans adb shell atteint le temporaire pendant que le flux s'exécute. Le chargement tardif démonte ensuite le démon su temporaire (voir (e)) sans retirer l'ombre, ce qui laisse un adb shell su nu exécutant un client orphelin qui échoue avec su: connect daemon: Permission denied même si root fonctionne, et laisse les binaires réels de l'apex (crosvm, virtmgr, vm, …) cachés. Une fois que la vérification rapporte un pilote vivant, le flux démonte le tmpfs de mise en scène — via le propre /system/bin/su de KernelSU, puisque le démon de l'exploitation est déjà parti — supprime le client su temporaire, le socket et le journal, et rapporte quel su un simple résout désormais. C'est au mieux : en cas d'échec, il avertit avec la commande manuelle au lieu d'échouer l'exécution, et un redémarrage efface le montage de toute façon.
adb sur l'hôte, avec l'appareil autorisé (débogage USB activé).kernelsu.ko intégré.artifacts/exploits/ (voir Construction des charges utiles).# Un appareil sur adb ; gestionnaire installé ; charges utiles construites.
bin/pixel-ksu-root
Le pilote résout l'appareil contre data/targets.json, exécute le flux KASLR en deux phases, charge tardivement le module via le ksud dérivé du gestionnaire, et vérifie via l'appel système du pilote. Il sort avec un code non nul si aucune charge utile ne se résout pour l'appareil, si aucun gestionnaire n'est installé, ou si la vérification ne rapporte jamais un pilote vivant.
KASLR_BASE=0x<hex> — passée à la charge utile de l'appareil pendant la Phase B pour rejouer contre une base fixe déjà dérivée par boot. Non définie pendant la Phase A afin que la charge utile dérive la base elle-même.ANDROID_NDK_HOME — chemin vers le NDK Android, requis uniquement lors de la construction des charges utiles.API — niveau d'API Android pour la chaîne d'outils NDK lors de la construction des charges utiles (défaut 35).pixel-ksu-root/
├── bin/ Point d'entrée du pilote hôte (flux piloté par adb)
├── data/
│ └── targets.json Table de résolution appareil→charge utile et appareil→KMI
├── exploit/ Source de la charge utile CVE-2026-43499 vendue
│ ├── Makefile Construction NDK aarch64 par cible
│ ├── src/ Ensemble source de base android15-6.6
│ │ ├── main.c slide.c fops.c pipe.c root.c preload.c util.c
│ │ ├── su_daemon.c Helper su temporaire généré par la chaîne
│ │ ├── kernelsnitch/ En-têtes du canal secondaire d'occupation de hachage futex
│ │ └── targets/ target.h par appareil+build (décalages noyau)
│ └── src/61/ Ensemble source android14-6.1 (slide61.c, route TCP)
├── lib/ Fonctions shell/helpers partagées côté hôte
├── scripts/
│ └── build-payloads.sh Construit et déduplique l'ensemble de charges utiles
├── artifacts/
│ └── exploits/ Fichiers .so de charges utiles construits et dédupliqués
└── docs/ Notes de conception et d'analyse
scripts/build-payloads.sh enveloppe le exploit/Makefile par cible et émet l'ensemble de charges utiles dédupliqué nommé dans data/targets.json dans artifacts/exploits/. Il construit un .so par groupe de décalages unique (depuis la cible build_from de ce groupe) plutôt qu'un par appareil.
export ANDROID_NDK_HOME=/path/to/android-ndk # doit contenir la chaîne d'outils NDK aarch64
scripts/build-payloads.sh # construit chaque charge utile dans data/targets.json
Le Makefile sélectionne la chaîne d'outils Clang NDK aarch64 depuis ANDROID_NDK_HOME et compile une seule cible à la fois ; API (défaut 35) choisit le pilote aarch64-linux-android<API>-clang. L'ensemble source est choisi par famille de noyau — les cibles android15-6.6 compilent la base src/, les cibles android14-6.1 compilent src/61/ — et les décalages noyau absolus de chaque cible proviennent de src/targets/<nom-de-code>-<build>/target.h. Pour construire une seule cible directement :
make -C exploit TARGET=husky-CP2A.260705.006
data/targets.json liste 19 entrées appareil/build couvrant 18 modèles Pixel (bluejay apparaît sur deux builds de firmware), groupées en 5 charges utiles de décalages noyau. La sélection se fait par image noyau, donc les appareils partageant un vmlinux se dédupliquent sur une charge utile ; une image noyau différente obtient la sienne.
Les appareils sur l'image noyau partagée android14-6.1-a (6.1.157-android14-11-gbd23337e42e7-ab14791245) couvrent les familles Pixel 6/6 Pro/6a, 7/7 Pro/7a, 8/8 Pro et 9/9 Pro/9 Pro XL/9 Pro Fold ; android14-6.1-b et android14-6.1-akita isolent les modèles sur le même KMI dont le build noyau ou les décalages diffèrent ; android15-6.6 couvre la famille Pixel 10.
NebuSec/CyberMeowfia sous Apache-2.0 ; découverte créditée à l'outillage VEGA de NebuSec, divulguée le 2026-07-07.exploit/ vendue ajoute les décalages cibles android14-6.1 et android15-6.6 et un démon de chargement tardif KernelSU par-dessus l'exploitation NebuSec ; elle ne porte pas de licence séparée et hérite des termes Apache-2.0 en amont.ksud, et le modèle d'autorisation du gestionnaire que cet outil charge tardivement.Le code source vendu sous exploit/ conserve sa licence en amont (Apache-2.0 pour l'exploitation dérivée de NebuSec). Ce projet est agnostique au gestionnaire : il charge tardivement le gestionnaire de la variante KernelSU installée et ne cible ni ne regroupe de fork spécifique.
ksudinit_module et détection du piloteapk_sign.rssk_buffpipe_bufferashmem_miscs[0].fops avec un file_operations forgé dont chaque emplacement pointe vers une fonction noyau réelle compatible avec le prototype (configfs_bin_write_iter, configfs_read_iter, copy_splice_read, ashmem_ioctl, noop_llseek, …), de sorte que le CFI de bord avant est satisfait tandis que read/write/splice sur un fd ashmem produisent un R/W noyau contraint.pipe_buffer sur la page de slab divulguée (page pointant vers n'importe quelle cible via la conversion vmemmap↔carte directe, ops = anon_pipe_buf_ops, PIPE_BUF_FLAG_CAN_MERGE), donc un simple read()/write() sur le pipe déplace des octets vers et depuis des adresses noyau arbitraires — un R/W noyau arbitraire stable.uid=0/proc/sys/kernel/random/boot_id en direct avec le boot enregistré au moment de la capture ; tout changement écarte la base et revient à la Phase A. Une boucle externe répète dériver→rejouer à travers les redémarrages.adb shellumount| Charge utile | KMI | Construite depuis | Appareils |
|---|
android14-6.1-a | android14-6.1 | bluejay-CP2A.260705.006 | oriole, raven, bluejay (CP2A), panther, cheetah, comet, shiba, husky, lynx, caiman |
android14-6.1-b | android14-6.1 | komodo-CP2A.260705.006 | komodo, tegu, tokay |
android14-6.1-akita | android14-6.1 | akita-CP2A.260805.005 | akita |
android14-6.1-cp1a | android14-6.1 | bluejay-CP1A.260405.005 | bluejay (CP1A) |
android15-6.6 | android15-6.6 | blazer-CP2A.260705.006 | frankel, blazer, mustang, rango |