
Recherche LPE sur le noyau Android pour CVE-2026-98260 (UAF posix-cpu-timers) : PoC de déclenchement, chaînes d'exploitation shell uid-0 avec fake-cred et real-cred, ainsi qu'une application d'amorçage Shizuku/KernelSU.
Anglais | 简体中文
Recherche d'élévation de privilèges locale sur Android pour un use-after-free dans le chemin de sortie de posix-cpu-timers. Le dépôt fournit un PoC de déclenchement uniquement, un exploit de vérification à identifiants factices, une étape de shell uid-0 à identifiants réels, une application KernelSU basée sur Shizuku, et une pré-vérification de vulnérabilité.
Branche
magicos-11.0.0.109-6.12.58: cette branche est à double profil. L'APK et les binaires natifs sélectionnent un profil à partir de la valeur exacte deuname -rà l'exécution : MagicOS 10.0.0 / 6.12.38 est le profil vérifié, tandis que MagicOS 11.0.0.109 / 6.12.58 est adapté statiquement et sa validation d'exécution côté appareil est encore en attente. L'APK embarque les deux tablesko-symsde 226 symboles et sélectionne celle qui correspond.
| Élément | Valeur |
|---|---|
| CVE | CVE-2026-98260 |
| Classe | LPE (use-after-free, effacement dans l'arbre rb de timerqueue) |
| Cible | Honor MagicPad3 Pro 13.3 YLP-W00, MagicOS 10.0.0 / 11.0.0.109 (Android 16), branche magicos-11.0.0.109-6.12.58 |
| Noyau | 6.12.38-android16-5-gfde7767f6ef6-abogki481467632-4k (vérifié)6.12.58-android16-6-g3a7ac1674240-abogki541502914-4k (adapté) |
| État du profil | double profil ; 6.12.38 vérifié, validation d'exécution de 6.12.58 en attente |
| SELinux | Enforcing |
| Correctifs | absents sur la version testée |
| Contre-mesures | KASLR activé, SMEP/SMAP/PAN activés, KASAN compilé mais désactivé |
| Bootloader | verrouillé et non déverrouillable ; aucune écriture de partition |
| Composant | État | Résultat |
|---|---|---|
src/poc.c | vérifié sur 6.12.38 | déclencheur destructif ; le reap provoque un panic dans cleanup_timerqueue |
src/exp.c | vérifié sur 6.12.38 | identifiants factices ; getuid()==0 && geteuid()==0 ; impossible d'exec |
src/uid0.c | vérifié sur 6.12.38 | identifiants réels ; shell uid-0 dans u:r:shell:s0 |
app/ | vérifié sur 6.12.38 | amorçage Shizuku + KernelSU ; uid 0 dans u:r:ksu:s0 |
src/vuln_precheck.h | ajouté | contrôle profil/environnement ; --check / --force |
| Profil 6.12.58 | adapté | sites, selinux_state et sa table ko-syms de 226 symboles redérivés ; exécution sur appareil en attente |
| APK double profil | testé sur 6.12.38 | sélection du profil et de ko-syms par uname -r ; chemin KSU en direct 6.12.38 validé |
Version actuelle : v1.4.3-app.
La pré-version v1.4.2-app a été retirée le 2026-10-30 ; son tag est conservé pour
l'historique uniquement. Voir docs/release-notes.md pour le
registre des versions et les lacunes connues.
src/poc.c trigger only; the reap panics on the tested kernel
src/exp.c fake-cred chain; verifies uid 0 via getuid/geteuid
src/uid0.c real-cred chain; zeroes cred->uid/gid and execs sh
src/kernel_profile.h per-release call sites, symbols and structure offsets
src/trace_ids.h runtime kmem trace event IDs; unprivileged probe
src/vuln_precheck.h shared profile/environment gate
app/ Android root app (Shizuku + KernelSU bootstrap)
docs/analysis.md root cause, exploit chain and device notes
scripts/dump_device.sh root device dump: all .ko/modinfo, kernel/config/policy, perf/tracefs, vendor mount, harness, active boot.img
Makefile single build entry (NDK cross-compile, app APK)
README.zh-CN.md Chinese version of this document
Un thread non-leader qui a créé un timer posix CLOCK_THREAD_CPUTIME_ID puis
échoue à execve() après de_thread() garde le nœud du timer en file d'attente. Le
SIGKILL qui interrompt l'exec fait sortir le thread via
exit_itimers(), qui ne peut pas désarmer un timer dont la tâche est déjà en cours de sortie.
Le k_itimer est libéré alors que son timerqueue_node est toujours lié dans la
file posix_cputimers de la tâche. Lorsque le parent récupère le zombie,
release_task() -> posix_cpu_timers_exit() -> cleanup_timerqueue() appelle
timerqueue_del()/rb_erase() sur le nœud libéré.
L'effacement est la primitive d'écriture : rb_erase() stocke une valeur
__rb_parent_color contrôlée dans une cible rb_left contrôlée. L'exploit
orchestre la récupération des slabs/pages afin que le nœud libéré se trouve dans une page remplie
avec la charge utile de l'attaquant, puis choisit la cible et la valeur.
Voir docs/analysis.md pour la trace de panic observée, les
offsets YLP-W00, les fuites basées sur tracefs, la chorégraphie SLUB/RCU, la
chaîne de fake-cred en deux étapes et la chaîne de real-cred en une seule écriture.
Prérequis : Android NDK r29 dans /opt/android-ndk-r29 (remplaçable avec
NDK=). Les cibles sont aarch64, API 24 par défaut. L'application nécessite en plus
le SDK Android (build-tools 37.0.0, platform 37), un JDK, et les jars Shizuku
sous ~/.local/share/android-cve-env/cve98260-app (remplaçable avec
APP_DEPS=). Les artefacts de compilation restent dans build/ et sont ignorés par git. Les
jars Shizuku sont fournis par l'utilisateur ; scripts/fetch-deps.sh les vérifie par rapport à
deps.lock (SHA-256) et ne télécharge jamais rien.
| Cible | Sortie / action |
|---|---|
make / make poc | build/poc |
make exp | build/exp |
make uid0 | build/uid0 (statique ; l'étape shell se ré-exécute elle-même) |
make build-all | compile poc + exp + uid0 + apk |
make host | build/poc.static pour la validation qemu côté hôte |
make emu | exécute build/poc.static sous qemu (avec --force) |
make apk | app/CVE98260Root.apk |
make clean | supprime build/ |
make help | liste les cibles |
poc, exp et uid0 appellent un contrôle de profil avant de toucher au noyau. Il
accepte soit une version de profil exacte (6.12.38 vérifiée, 6.12.58 adaptée),
exige arm64 et des pages de 4 Kio, et pour exp/uid0 vérifie également la disponibilité
de tracefs/kmem et (pour la chaîne fake-cred) panic_on_warn=0. Toute autre
version est non confirmée et le programme se termine avec le code 2 :
build/uid0 --check # print the verdict, do not trigger
build/uid0 --check --force # same, overriding an unconfirmed environment
N'utilisez --force ou CVE98260_FORCE=1 qu'après avoir vérifié le noyau de manière indépendante. --force assouplit uniquement les vérifications de l'environnement ; exp, uid0 et l'application exigent toujours un profil exact issu de kernel_profile.h. Le contrôle n'est délibérément pas une sonde d'exécution : une sonde qui atteindrait le bug laisserait elle-même un nœud timerqueue obsolète derrière elle.
Sur YLP-W00, un shell non privilégié ne peut pas lire /proc/sys/kernel/panic_on_warn ni /proc/cmdline ; pour un profil connu, le contrôle suppose la valeur 0 validée pour 6.12.38. La même hypothèse est utilisée pour le profil adapté 6.12.58 jusqu'à ce que sa validation à l'exécution soit terminée. Les versions inconnues restent bloquées.
L'exploit démarre comme un processus shell ordinaire (uid 2000, SELinux u:r:shell:s0) et n'utilise pas su. Ne le lancez pas via su : la condition testée est un démarrage non privilégié.
make uid0
adb push build/uid0 /data/local/tmp/uid0
adb shell chmod 755 /data/local/tmp/uid0
adb shell /data/local/tmp/uid0 # runs id and prints the context
adb shell /data/local/tmp/uid0 shell # interactive sh -i
Avant la chaîne complète, utilisez les modes non destructifs :