
Recherche sur CVE-2026-43499 (GhostLock) concernant HUAWEI MatePad Pro 11 GOT-W29
Recherche d'élévation de privilèges pour CVE-2026-43499 (rtmutex/futex-PI UAF, « GhostLock ») sur GOT-W29 (HarmonyOS 4.0, kernel 4.19.157-perf+).
Conclusion principale :
sysctl_bootid du kernel avec l'aide de KPM, dérivation du slide KASLR).| Élément | Valeur |
|---|---|
| Modèle | HUAWEI MatePad Pro 11 GOT-W29 |
| SoC | Qualcomm kona (SM8250, Snapdragon 870) |
| Système | HarmonyOS 4.0 (104.0.0.136) |
| Kernel | 4.19.157-perf+ |
| VA | 39-bit, pages 4K, KASLR activé |
remove_waiter() dans kernel/locking/rtmutex.c utilise current au lieu de waiter->task lors du nettoyage dans le chemin de rollback de rt_mutex_start_proxy_lock(), ce qui laisse un pi_blocked_on pendant (UAF de pile). Impacte 2.6.39 ~ 7.1 (ce kernel est dans la plage). Correctif en amont : commit 3bfdc63936dd.
Confirmé sur cet appareil : source rtmutex.c:1110-1112, désassemblage de boot.elf, et déclenchement sur le matériel réel, tous vérifiés.
Créer un cycle PI pour que FUTEX_CMP_REQUEUE_PI retourne -EDEADLK ; le rollback déclenche le bug de remove_waiter, laissant un pi_blocked_on pendant (pointant vers le rt_waiter sur la pile du kernel du thread waiter).
L'ancien déclenchement faisait que le waiter possédait lui-même le futex cible du requeue (self-own), ce qui déclenchait précisément la vérification anticipée owner==task de task_blocks_on_rt_mutex de ce kernel (boot.elf 0x3808-0x3868), retournant avant l'écriture de pi_blocked_on → aucun pointeur pendant n'est jamais créé → le placement de l'overlay était un diagnostic erroné (pas de crash + boot_id inchangé).
Cycle PI : l'owner FUTEX_LOCK_PI(target) détient la cible du requeue ; le waiter détient le futex de chaîne ; l'owner se bloque ensuite sur la chaîne (cycle : waiter→target→owner→chain→waiter). Lors du requeue, la traversée de chaîne détecte rt_mutex_owner(chain)==top_task → -EDEADLK → le rollback nettoie le pi_blocked_on de la mauvaise personne → le pi_blocked_on du waiter est pendant. L'owner doit réduire sa priorité (nice=10) pour que la prio après boost diffère de owner_waiter->prio, sinon rt_mutex_waiter_equal sort prématurément.
Sous shell (uid 2000) avec perf_event_paranoid=-1, perf_event_open(PERF_SAMPLE_IP, exclude_user=1) échantillonne des clusters d'adresses du texte du kernel ; l'alignement sur les offsets de symboles connus donne le slide.
samples=27651 kernel_ips=1685 lo=0xffffff948728176c hi=0xffffff9488ebfc7c
KASLR slide=0x147f200000 (40/40 IP mappées dans la zone texte du kernel, vérifié)
runtime _stext=0xffffff9487280800
Outil : tools/perf_kaslr.c. Prérequis d'exécution : shell (Shizuku rish), pas d'interception seccomp.
[M] CMP_REQUEUE_PI ret=-1 errno=35 (EDEADLK!)
[W] WAIT_REQUEUE_PI ret=-1 errno=110 (ETIMEDOUT) ← retour du waiter
[M] waiter_returned=1 ← laisse un pi_blocked_on pendant
Outil : tools/edeadlk_probe.c (variante 8+2+1 = 11, ou 27).
rt_mutex_adjust_prio_chain step[7] effectue un rb_erase sur le fake waiter (chemin enfant gauche unique) : *(tree_left) = tree_pc + écriture incrémentale __rb_change_child. Tous les offsets dans target.h sont mesurés par désassemblage de boot.elf.
Voir exploit/ghostlock-source/src/target.h. Points clés :
Un KPM auto-écrit (tools/kpm-debug/rtmutex-dbg.c, KernelPatch 0.13.5 inline-hook) reconstruit l'overlay (tree/task/lock) lors du fake walk et réécrit le paramètre next_lock en empty_zero_page (KernelPatch _transit8 appelle la fonction d'origine avec les fargs modifiés), ce qui fait passer [3] next_lock==waiter->lock de rt_mutex_adjust_prio_chain, réussir le trylock zéro-verrou de [5], [6] sans owner, et exécuter [7] rt_mutex_dequeue (rb_erase enfant gauche unique) — sysctl_bootid est réécrit en &loggers[0][1], slide-kaslr-ok.
REPAIR3 waiter=0xffffff801ecdbc00 lock=0xffffffa7c4950000
slide boot_id_leaked_nfulnl_logger value=ffffffa7c4612320
slide-kaslr-ok base=ffffffa7c1280000 slide=00000027b9200000
La géométrie de la pile est confirmée par les tailles de frames de boot.elf : dans __arm64_sys_futex(0x70) + do_futex(0x60+0x1a0), rt_waiter est à sp+0xc0 → profondeur 0x1b0 ; stack_fds[0] du chemin pselect est à la profondeur 0x210, soit une différence de 0x60 = 12 mots. Ainsi, word_i tombe dans stack_fds[12+i] : les mots 0-2 sont dans la zone d'entrée ex[2..4] (directement contrôlables), les mots 6-7 (task/lock) dans res_in[3..4] (encodés avec in[3..4] + fd prêt pour POLLIN), les mots 3-5/8-10 restent à 0. pselect retourne immédiatement en raison du fd prêt → le waiter tourne en attente active en espace utilisateur (signaux désactivés, zéro syscall) jusqu'à ce que le consumer déclenche la fin.
Les mots task/lock de l'overlay tombent dans res_in[3]/[4] (encodés par fd prêts). Mesuré : res_in[4] (lock) est écrasé de manière déterministe sur le chemin de retour de pselect (résidu de frame rt_sigreturn), n'étant jamais égal au fake_lock du payload ; res_in[3] (task) est parfois intact. La contournement en espace utilisateur (opérations FP + sched_yield) ne peut réduire le taux de déclenchement de do_notify_resume qu'à ~21 %, mais l'écrasement du lock est quasi certain.
Deux autres faits connexes :
rt_mutex_adjust_pi() a if (!owner) return 0;, donc sans owner dans fake_lock, adjust_prio_chain n'est pas appelé. popsicle (6.12) n'a pas cette vérification. Un payload avec owner a été porté (fake_lock owner=fake_task|1), mais il ne peut pas être déclenché de manière fiable car le mot lock est toujours écrasé.Après désassemblage de __arm64_sys_ppoll / do_sys_poll de boot.elf, il est confirmé : pollfd est une structure de 16 octets (fd 4B + events 4B + revents 4B + pad 4B), incapable de porter les mots task/lock 64 bits du fake waiter — la valeur fd est limitée (doit être un vrai fd), events ne fait que 4 octets et n'est pas contigu, revents est écrit par le kernel (non contrôlable).
smt878u / popsicle peuvent être entièrement compromis : leur géométrie de pile permet aux mots de tomber dans les zones in/out/ex contrôlables par l'utilisateur (3 fd_set de pselect). La position du waiter de GOT-W29 (bits+0x60 → task/lock dans res_in[3]/[4]) n'a pas cette fenêtre. C'est une différence de géométrie de pile du kernel, pas un défaut d'implémentation.
Le domaine app (untrusted_app) n'a pas de canal KASLR existant (perf/kallsyms/pagemap/dmesg tous refusés) ; CMP_REQUEUE_PI retourne 1 (requeue réussi) sans passer par le rollback EDEADLK ; le major_only du cpuset non-root court-circuite durement le step[6] de la traversée de chaîne. La seule entrée pour modifier QOS, /dev/iaware_qos_ctrl, est refusée par SELinux. Par conséquent, les permissions d'une application normale ne peuvent pas déclencher cette CVE.
Module KernelPatch auto-écrit (rtmutex-dbg) pour observer le fake waiter et la primitive d'écriture sur le matériel réel, compilé à partir des en-têtes LyraVoid/KernelPatch 0.13.5 (même source que FolkPatch).
cd <KernelPatch>/kpms/rtmutex-dbg
make TARGET_COMPILE=aarch64-linux-android- \
CC=$PREFIX/bin/aarch64-linux-android-clang \
LD=$PREFIX/bin/aarch64-linux-android-ld
Produit rtmutex_dbg.kpm. Les flags de compilation doivent inclure
-fno-pic -fno-pie -fno-asynchronous-unwind-tables -fno-unwind-tables
(déjà intégrés dans le Makefile) : le PIC par défaut de clang produit des relocalisations GOT, et le .eh_frame par défaut (R_AARCH64_PREL32) n'est pas pris en charge par le chargeur KPM → échec de chargement -1.
La superkey de FolkPatch est su (pas le KernelPatch par défaut d'APatch). Utiliser l'outil sc_kpm_load (source sc_kpm_load.c) :
adb shell /data/local/tmp/sc_kpm_load su /sdcard/Download/rtmutex_dbg.kpm # charger
adb shell /data/local/tmp/sc_kpm_load unload rtmutex-dbg su # décharger
adb shell /data/local/tmp/sc_kpm_load ctl rtmutex-dbg counts su # compteurs
run_rtmdbg_test.sh (côté appareil /data/local/tmp/ghostlock-test/) : exécute GhostLock en identité shell (GOT_SLIDE_NO_RT=1 chemin réel), boucle de sync de 0,5 s pour éviter la perte de logs, dmesg -w écrit sur disque, collecte automatique après 90 s (SIGSTOP pour éviter le soft-lock et le redémarrage). Avant le test :
adb shell 'su -c "sh /sdcard/ghostlock-test/set_debug_no_reboot.sh"' # éviter le redémarrage
[RTMDBG])REPAIR3 : reconstruction de l'overlay et réécriture du paramètre next_lock lors de la validation de la primitive d'écritureFAKEWALK skip : le fake walk écrasé est sauté (pas d'écriture, pas de crash)prio_chain[N] / prio_chain_ret : appels de walk et valeurs de retour (0=terminé ;
4294967261=-EDEADLK)do_select n=320 : res_in[3]/[4] côté kernelfutex op=13/14 : CMP_REQUEUE_PI / WAIT_REQUEUE_PILes piles complètes des oops/panic de HUAWEI sont enregistrées dans /data/log/bbox/history.log.
tools/ Outils de validation (perf KASLR, sonde EDEADLK, chaîne d'outils KPM)
target/ Tous les offsets mesurés
exploit/ slide.c porté (avec modifications de déclenchement EDEADLK)
| hook | Rôle |
|---|
rt_mutex_adjust_pi | Enregistre les ajustements PI ; skip lors de l'écrasement de l'overlay (efface pi_blocked_on) |
rt_mutex_adjust_prio_chain | Skip inconditionnel du fake walk ; dump complet du waiter |
__arm64_sys_pselect6 / __arm64_sys_ppoll | Efface _TIF_WORK_MASK sur le chemin de retour ; observation fd_set |
do_select | Lecture côté kernel de res_in[3]/[4] |
__arm64_sys_futex | Suivi de WAIT_REQUEUE_PI / CMP_REQUEUE_PI |
rt_mutex_dequeue | Confirme l'exécution de la primitive d'écriture du step[7] et la forme de l'arbre |