
Exploit du noyau Android GKI 6.12 pour CVE-2026-43499, enchaînant un bug de rollback rt_mutex avec un écrasement de pile pselect pour obtenir les droits root sur les appareils Samsung et Pixel.
Un exploit du noyau Linux pour la branche Android GKI 6.12 (appareils Samsung et Pixel)
ciblant CVE-2026-43499 : un rollback défectueux de remove_waiter() dans
rt_mutex_start_proxy_lock() qui utilise current au lieu de waiter::task,
laissant le pi_blocked_on d'un waiter pointer vers sa pile noyau (ensuite dépilée)
rt_mutex_waiter. Combiné à un écrasement de pile fd_set via pselect() et à un
parcours rb_tree factice piloté par sched_setattr côté consommateur, cela produit
une primitive d'écriture noyau déterministe et un root complet.
L'exploit s'exécute en tant que bibliothèque partagée LD_PRELOAD (preload.so) et installe
un démon su root ainsi qu'un fond d'écran comme artefacts post-root.
Statut : développement actif. La cible m1q (ZF1) est le point focal du bring-up. Le bootstrap pipei tmp_page-uname est la route active actuelle : le parcours est prouvé propre sur l'appareil (build #33 : régions rt_mutex seedées par enfant) et le balayage complet des 168 candidats est en cours. La route configfs CFI est une impasse sur m1q (ashmem Rust — aucun slot fops injectable) et reste la route pour les cibles C-ashmem. Les offsets par cible varient selon l'appareil — vérifiez-les par rapport au binaire noyau réel avant de leur faire confiance.
Lorsque FUTEX_CMP_REQUEUE_PI remet un waiter en file et que le parcours de
détection d'interblocage de la requeue renvoie -EDEADLK, __rt_mutex_start_proxy_lock()
effectue un rollback via remove_waiter() (rtmutex.c:1535). Le rollback retire correctement
le waiter de l'arbre d'attente mais efface le pi_blocked_on de l'appelant de la requeue
au lieu de waiter->task->pi_blocked_on. Le pi_blocked_on du WAITER reste
donc pendant sur son rt_mutex_waiter de pile, qui est dépilé une fois le futex expiré.
Après le timeout, la région de pile noyau du waiter est réutilisée : core_sys_select()
copie les trois fd_sets dans ce tampon de pile (le chemin de pile nfds < 344 sur
ZF1). Un tableau de mots fd_set forgé re-matérialise un faux rt_mutex_waiter /
fausse task / faux rt_mutex (avec une racine rb_tree dont les pointeurs sont contrôlés).
Un thread consommateur appelle alors sched_setattr_tid(waiter) →
rt_mutex_adjust_pi() → rb_erase_cached, ce qui produit une écriture à une adresse
arbitraire d'une valeur contrôlée.
Toute la primitive nécessite le cycle de chaîne PI : le propriétaire détient f_pi_target
et bloque également sur f_pi_chain (détenu par le waiter), de sorte que le parcours de chaîne atteint
owner → chain → waiter → target → owner et échoue avec -EDEADLK. Supprimer le
cycle fait que la requeue renvoie un succès sans aucun effet noyau.
sched_blocked_reason (m1q principal, prouvé sur l'appareil) : le PC de retour sauvegardé
d'un kworker bloqué (stack_trace_save_tsk) est lu depuis
le ring buffer et comparé à l'offset worker_thread compilé en dur. S'exécute avant
que les mots de la route boot_id ne soient utilisés, afin que les cibles d'écriture d'alias de données
(data_addr() = alias p0 + slide_p0_offset) restent correctes à un slide non nul.SLIDE_LOGGERS_0_1 dans les données sysctl boot_id via un alias de
linear-map ; la valeur fuitée reconstruit stext. Mise en pause sur m1q : sa cible W1
(SLIDE_RANDOM_BOOT_ID_DATA_OFF, RAM physique basse) a une écriturabilité dépendante de la page —
le slide déplace la cible à travers les frontières de page à chaque boot, donc ~6/7 exécutions
plantent. Exercée uniquement explicitement via SLIDE_FORCE_BOOTID (test de mécanisme)
avec tree_pc/tree_left redirigés vers la page sprayée.mm_struct et utilisation d'une collision
de hash de futex pour localiser un sur le tas, fuitant une adresse de page de tas noyau
utilisée comme base de spray d'objets factices. Les waiters sont détachés (non
joints) au nettoyage pour éviter l'OOM de pile noyau qui a fait planter les builds avant
#26.Les mots fd_set de pselect re-matérialisent un faux rt_mutex_waiter sur la
pile noyau du waiter ; le sched_setattr_tid(waiter) d'un thread consommateur le parcourt
via rt_mutex_adjust_pi(). Carte des mots ZF1 (vérifiée par désassemblage) :
PSELECT_WAITER_WORD_SHIFT = 0, mot 12 = task (@+0x50), mot 13 = lock
(@+0x58), mot 14 = wake_state (@+0x60 = 3). Avec un faux rt_mutex seedé,
le parcours se termine proprement : les rb_erase W1 *(tree_left)=tree_pc / W2
*(tree_pc&~3+8)=tree_left de [7] sont les seules écritures noyau ; [11] (setprio /
dequeue_pi) et le wake [9] sont tous deux ignorés (le faux waiter local à prio 100
garde le nœud de pile hors du top-waiter). Chaque complétion entrant dans le parcours depuis
le build #19 survit sur l'appareil ; les plantages déterministes antérieurs étaient une
erreur de placement de payload de 8 octets (SKB_DATA_DELTA), pas une faute du corps du parcours.
Phase optionnelle STAGE3=1 qui fork un enfant pont, échange son mm->pgd
contre une table de pages factice préparée (3 niveaux : PGD→L1→L2, feuille boucle RX + feuille pile RW),
et laisse l'enfant exécuter un blob assembleur registres uniquement qui patche son propre cred
via une fenêtre de scan physique de 2 Mo — phys R/W sur toute la RAM sans les primitives
configfs/pipe. Actuellement câblée uniquement dans la cible m1q et n'a pas été exécutée
sur l'appareil.
41 cibles dans src/targets/<codename>-<build>/, chacune nécessitant au minimum un
target.h (offsets de symboles noyau, KIMAGE_TEXT_BASE, base de direct-map, offsets de struct).
Les cibles Pixel (comet, tokay, tegu, caiman, komodo,
frankel, mustang, rango, stallion, blazer) surchargent les sources partagées ;
les cibles Samsung (m1q-*) ajoutent une logique spécifique à l'appareil.
make list-projects # liste complète
Les dispositions des structs noyau diffèrent selon l'appareil — les offsets doivent être vérifiés par rapport au binaire noyau réel / kallsyms pour chaque cible.
Sortie : build/<PROJECT>/bin/preload.so (bibliothèque partagée chargée via LD_PRELOAD).
# Projet par défaut
CC=clang make
# Cible d'appareil spécifique (défaut : blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Sans CC=clang : utilise le NDK si trouvé ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), sinon clang hôte + sysroot Android
make PROJECT=m1q-BP4A.251205.006
# Afficher la configuration de build
make info
# Nettoyer
make clean
Le build embarque un binaire PIE su_daemon (src/su_daemon.c, compilé vers
build/embed/su_daemon_aarch64_pie) et assets/wallpaper.webp dans
preload.so via src/su_blob.S / src/wallpaper_blob.S.
Poussez les sorties de build vers /data/local/tmp, puis exécutez l'exploit sous
LD_PRELOAD. Exécutez SANS tee sur une cible de bring-up active — tee bufferise son propre
stdio et perd la fin du log lors d'un panic noyau (la sortie stdout de l'exploit est
non bufferisée, donc rediriger directement vers un fichier préserve chaque ligne) :
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
En cas de succès, le démon écoute sur /data/local/tmp/temp_su.sock et su est
installé sous /apex/com.android.virt/bin.
tools/run_pipei.sh pousse le binaire, le vérifie par sha256 sur l'appareil (un
preload.so obsolète produit des verdicts silencieusement illisibles — l'échelle a déjà exécuté
un build < #22 dont le format de log diffère), puis exécute le balayage complet des 168 candidats
découpé en 3x56 afin qu'un panic ne perde au plus qu'un chunk (~10 min). Le stdout de l'exploit
streame directement via adb (pas de redirection sur l'appareil — un tee côté appareil perd la fin en cas de panic) ; une copie côté hôte de chaque chunk est
conservée à /tmp/m1q_pipei_chunk{1..3}_stream.log :
sh tools/run_pipei.sh
PASS = n'importe quel chunk affiche tmp_page uname changed + after uname='CatOS.
PIPEI_CHILD_REGIONS doit être défini au moins à la taille du chunk, sinon les régions
rt_mutex par enfant restent non seedées et le balayage plante au candidat 0. La couverture complète
nécessite également TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES=1 — sans cela, seule
une page du bloc mm libéré de 8 pages est balayée. run_pipei_slim.sh est la
variante diagnostique à candidat unique ; run_m1q_ladder.sh est la
batterie parcours/isolation (oracle B16-2, mécanisme de slide, étapes de balayage).
exploit/
Makefile
assets/ # wallpaper.webp (artefact post-root embarqué)
src/
main.c # point d'entrée run_exploit() (partagé)
preload.c # constructeur LD_PRELOAD, installation su/wallpaper
util.c # traduction d'adresses, préparation de page, helpers configfs
pipe.c # lecture/écriture physique basée sur pipe
root.c # patch de cred / parcours de task
slide.c # slide KASLR (routes tracefs + boot_id)
fops.c # corruption pselect + manipulation fops
faketables.c # constructeurs de tables de pages factices étape-3 (3 niveaux)
stage3.c # enfant pont pgd-swap (no-op sauf si configuré)
stage3_loop.S, stage3_poll.S # blob pont + trampoline d'entrée
su_daemon.c # démon root (compilé en PIE séparé)
su_blob.S, wallpaper_blob.S # binaires embarqués
offset.h # tire le target.h par cible via -DTARGET_CONFIG_H
kernelsnitch/ # bibliothèque de fuite mm_struct par hash de futex
targets/<codename>-<build>/ # target.h par appareil + surcharges optionnelles
poc/poc.c # POC autonome (séparé de l'exploit)
Les cibles surchargent les sources partagées via le mécanisme pick_src du Makefile :
si src/targets/<PROJECT>/<file>.c existe, il est utilisé à la place du
src/<file>.c partagé ; sinon la version partagée est utilisée.
-EDEADLK (errno 35). Un cycle manquant se manifeste par
success=1 sans aucun effet noyau.PSELECT_WAITER_WORD_SHIFT doit être 0 (profondeurs de pile ZF1 vérifiées
depuis l'ELF). Les autres cibles restent par défaut à 1 — vérifiez par appareil avant
de leur faire confiance.p0_data_alias() utilise DIRECT_MAP_BASE (la linear map), qui diffère
de P0_PAGE_OFFSET sur ZF1. Les alias de données pour les objets noyau déréférencés
doivent utiliser la linear map.pr_error() est fatal (exit(-1)), contrairement aux macros warning/info.SLIDE_LOGGERS_0_1, pas SLIDE_NFULNL_LOGGER ;
le mot est un alias de linear-map non randomisé, donc la route boot_id est
indépendante du slide (à utiliser uniquement à slide=0 — exécutez d'abord la fuite tracefs).Ceci est du code d'exploit pour appareils Android et est destiné à la recherche en sécurité, aux tests de vulnérabilité autorisés et au travail de défense sur du matériel que vous possédez ou pour lequel vous avez l'autorisation explicite de tester. Toute utilisation abusive sur des appareils que vous ne possédez pas est illégale. L'auteur n'est pas responsable des appareils brickés, des noyaux brickés ou des garanties annulées.
mm_structwrite_iter statique injectable et l'objet tas ASHMEM_SET_NAME est
un KVec dont la disposition ne s'aligne pas avec le private_data de
configfs_bin_write_iter. m1q pivote vers une écriture kmalloc à la place : récupérer le bloc
mm d'ordre 3 fuité en tant qu'objets pipe_inode_info et utiliser l'écriture pselect W1
(*(tree_left) = tree_pc) pour écraser le slot tmp_page d'un candidat
(+0x90) avec la page du namespace UTS (init_uts_ns). Une écriture fanout plante
un nom marqueur à l'offset sysname et uname() rapporte "CatOS" — une
écriture noyau vérifiable sans dépendance à un objet statique. Les cibles C-ashmem
conservent le chemin configfs.preload.c installe le démon su embarqué (monté en tmpfs dans
/apex/com.android.virt/bin, plus les variantes adbd-namespace et locales) et
échange le fond d'écran.| Variable | Défaut | Objectif |
|---|
PSELECT_ROUTE_SHIFT | compile-time | Décalage de mot fd_set A/B pour la route principale (m1q) |
SLIDE_SHIFT | compile-time | Décalage de mot fd_set A/B pour la route slide |
PSELECT_ROUTE_DELAY_USEC | 50000 | Délai consommateur avant sched_setattr (doit être > 0) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | Timing/pinning de la route slide |
SKIP_SLIDE | 0 | Utiliser le repli direct-map (slide = 0) |
SLIDE_ONLY | 0 | Exécuter uniquement le slide KASLR et quitter |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | Surcharger les offsets slide p0-alias / boot_id |
SLIDE_FORCE_BOOTID | 0 | Forcer la route boot_id pselect après le slide tracefs (test de mécanisme) |
KSNITCH_COLLISIONS | 4 | Nombre de collisions KernelSnitch |
STAGE3 | 0 | Activer la phase enfant pont pgd-swap (m1q) |
STAGE3_DRYRUN | 0 | Préparer/vérifier les tables puis abandonner avant le swap |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | Ajustements de spray/disposition fd_set |
TMP_PAGE_UNAME | 0 | Exécuter le bootstrap m1q pipei tmp_page-uname |
TMP_UNAME_PIPEI_SWEEP, TMP_UNAME_PIPEI_ALIGN_ORDER, TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES | — | Mode balayage ; split_order balaye les 8 pages du bloc mm libéré |
TMP_UNAME_PIPEI_SLOT, TMP_UNAME_PIPEI_SLOT_CANDIDATES | — | Plage de balayage (début de chunk / nombre) |
PIPEI_CHILD_REGIONS | 0 | Régions rt_mutex seedées par enfant ; DOIT être >= chunk de balayage sinon les régions non seedées plantent au candidat 0 |
PIPEI_RECLAIM_COUNT, PIPEI_DRAIN_COUNT | — | Récupérer/vider les paires pipe ; augmenter en cas d'échec de récupération |
PIPEI_SPONGE_MB | 3072 | Éponge d'inondation Order-0 maintenue pendant la récupération (build #29) |
PSELECT_W0_PRIO_OVERRIDE | 130 | Prio du top-waiter ; inerte avec régions seedées (garde arbre obsolète legacy, A/B uniquement) |
PSELECT_SKB_DELTA_OVERRIDE | 0 | Offset intra-page du payload A/B (uniquement si un appareil diffère de -0xe78) |
TMP_UNAME_NAME, TMP_UNAME_HOLD_SEC | "CatOS", 5 | Nom marqueur / durée de maintien après succès |
SKB_DATA_DELTA vaut -0xe78 (mesuré sur l'appareil, build #19) — la vraie
tête de frag skb est 8 octets de moins que le -0xe80 dérivé du désassemblage ; cette erreur de 8 octets
a causé chaque plantage pselect déterministe des builds #6-18
(entrée dans le parcours → déréférencement de données corrompues). Valeur partagée GKI 6.12 ; les copies util.c
par cible portent un bouton A/B d'environnement SKB_DATA_DELTA. Ne PAS empiler
PSELECT_SKB_DELTA_OVERRIDE par-dessus la constante intégrée.ASHMEM_SET_NAME ne s'aligne pas
avec configfs_bin_write_iter. Utilisez le bootstrap pipei tmp_page. Les
gardes PSELECT_WRITE_RIGHT_SPRAYED / PSELECT_PI_TOP_TASK_SELF dans le target.h m1q
redirigent les écritures [11] du parcours hors des fops statiques RO.payload+0x6000 + idx*0x40) à l'intérieur de la
page récupérée. Les balayages à fake_lock partagé plantaient au candidat ~9-12 (nœuds de pile noyau
obsolètes recyclés), et les régions vides du build #32 plantaient au
candidat 0 (wake [9] owner==NULL sur le fake_task rempli de 0x41).
PIPEI_CHILD_REGIONS doit être défini sinon les régions restent non seedées.