
Preuve de concept non privilégiée et élévation de privilèges locale x86_64 pour une use-after-free du Binder du noyau Linux (CVE-2026-64468), avec laboratoire KASAN et différentiel vulnérable/corrigé.
binder_free_transaction() du noyau Linux
Ce dépôt contient, pour le use-after-free du Binder du noyau Linux corrigé par
le commit amont
f223d27a546c1e1f48d38fd67760e78f068fe8c4 :
binder_chain_64468.c — une preuve de concept sans privilèges qui atteint
le bug et laisse le noyau le prouver, avec un laboratoire KASAN et une
différentielle vulnérable/corrigé (lab/, run.sh, verify.sh).exploit.c — une élévation de privilèges locale autonome pour x86_64.
Elle se compile avec gcc -O2 -pthread -o exploit exploit.c, s'exécute en tant
qu'utilisateur ordinaire et aboutit à un shell root.demo/ — un laboratoire qui démarre un véritable userland Debian 13 sur
un noyau non corrigé, afin que l'exploit puisse être compilé par le gcc propre
à la cible et exécuté sur la machine qu'il prend ensuite en main.Tout s'exécute en tant qu'utilisateur ordinaire (uid/gid 1000, sans capacités, sans espaces de noms) contre des noyaux amont standard sans aucun correctif.
Avertissement
Ce code fait délibérément la course sur les durées de vie des objets du noyau, puis détourne le flux de contrôle du noyau. Une course perdue corrompt l'état du tas du noyau et peut provoquer un panic ou un blocage de la machine. Exécutez-le uniquement dans une VM isolée et jetable qui vous appartient. Ne l'exécutez pas sur un hôte.
binder_free_transaction() lit le processus cible depuis la transaction
sous t->lock, relâche ce verrou, puis acquiert le verrou interne de la cible :```c
spin_lock(&t->lock);
target_proc = t->to_proc;
spin_unlock(&t->lock);
if (target_proc) {
binder_inner_proc_lock(target_proc); /* use after free */
Rien ne maintient `target_proc` en vie à travers cet intervalle. Un processus en cours de démantèlement
en parallèle peut atteindre `binder_proc_dec_tmpref() -> kfree()` entre les deux, de sorte
que le verrou est pris sur de la mémoire libérée. Le correctif en amont épingle `t->to_thread` pendant que
`t->lock` est toujours détenu, ce qui maintient le processus propriétaire en vie jusqu'à ce que le
verrou interne ait été utilisé et libéré.
La vulnérabilité a été signalée par **Alice Ryhl** et corrigée par **Carlos
Llamas**, tous deux de Google. Le
[rapport original](https://lore.kernel.org/all/[email protected]/)
contient la trace KASAN de référence.
### Atteindre l'accès vulnérable
Le seul appelant qui peut atteindre un `to_proc` *étranger* est
`binder_send_failed_reply()`, et il ne parcourt `t->from_parent` que lorsque
`t->from` est `NULL` :```c
target_thread = binder_get_txn_from_and_acq_inner(t);
if (target_thread) { ...; binder_free_transaction(t); return; }
next = t->from_parent;
binder_free_transaction(t);
t = next;
from_parent est assigné à un seul endroit, et cette assignation est protégée quelques lignes plus tôt par la vérification de la pile de transactions invalide du binder : un thread ne peut envoyer une transaction synchrone que si le sommet de sa pile est une transaction qu'il est en train de recevoir. Par conséquent, pour chaque maillon de cette chaîne, l'émetteur de l'enfant et le récepteur du parent sont le même thread.
Cela a une conséquence majeure. binder_thread_release() parcourt la pile du thread mourant avec proc->inner_lock maintenu, et écrit à la fois```
iteration j : [holds child->lock ] child->from = NULL ; unlock child->lock
spin_lock(parent->lock)
iteration j+1 : [holds parent->lock] parent->to_proc = NULL
dans **les itérations adjacentes d’une même marche**, chacune sous le
`t->lock` correspondant. Un marcheur ne découvre `child->from == NULL` qu’une fois que la marche a libéré
`child->lock`, et a besoin de `parent->lock` pour sa propre capture instantanée. Toute l’opportunité réside donc dans l’intervalle entre le `spin_unlock(&child->lock)` de cette marche et son
`spin_lock(&parent->lock)` — quelques instructions. La marche de libération détient un
spinlock et ne peut pas être interrompue par une préemption à cet endroit ; seule une interruption peut la retarder.
C’est pourquoi la course est étroite et pourquoi tant la preuve de concept que l’exploit sont probabilistes.
### Ce que construit la preuve de concept
`binder_chain_64468.c` construit la chaîne la plus courte qui atteint l’accès vulnérable, afin que le travail du marcheur dans cet intervalle soit aussi réduit que binder le permet — deux acquisitions de `t->lock` et un `kfree()`, sans `inner_proc_lock` étranger, sans `wake_up` et sans remise de réponse :```
B thread i --e2 (sync, code 0x4442414b)--> P thread Y_i
P thread Y_i --e1 (sync, code 0x54414c4c)--> B thread i (nested target)
B thread i stack: [ e2 outgoing , e1 incoming (top) ]
P thread Y_i stack: [ e2 incoming , e1 outgoing (top) ]
P abandonne ensuite son fd de binder, de sorte que binder_deferred_release() libère
Y_1..Y_K et finit par libérer le binder_proc, tandis que chaque thread B
émet simultanément BINDER_THREAD_EXIT :```
binder_thread_release(B_i) nulls e1->to_proc (own proc) and e2->from
binder_send_failed_reply(e1) e1->from == NULL once Y_i was released
-> binder_free_transaction(e1) target_proc already NULL, kfree(e1)
-> binder_free_transaction(e2) target_proc == P <-- vulnerable access
Un troisième processus est le gestionnaire de contexte du binder, utilisé uniquement pour distribuer les
handles dont les deux autres ont besoin. `exploit.c` réutilise exactement cette construction.
### Deux conditions indépendantes
Un rapport KASAN nécessite les deux éléments suivants :
* **l'accès vulnérable** — le parcoureur doit prendre `parent->lock` dans la
fenêtre étroite ci-dessus, afin de capturer un `to_proc` encore vivant ; et
* **l'atterrissage de la libération dans la fenêtre** — le parcoureur doit ensuite perdre son CPU
entre la libération de `t->lock` et la prise du verrou interne de la victime, et rester hors de celui-ci
jusqu'à ce que la libération différée soit terminée et ait libéré le `binder_proc`.
La première condition possède son propre oracle qui n'a pas besoin de KASAN : le binder imprime```
binder: binder_free_proc: Unexpected outstanding_txns -1
chaque fois que cela se produit, car le walker et binder_thread_release() décrémentent alors tous deux le même compteur pour une seule transaction. Notez que ce n'est pas en soi une différence vulnérable/corrigée — le correctif arrête la libération, pas la seconde décrémentation — elle apparaît donc sur les deux noyaux. Elle n'est utilisée ici que pour montrer le chemin de code vulnérable en cours d'exécution.
La preuve de concept s'arrête à une décrémentation de 4 octets sur de la mémoire libérée. Transformer cela en uid 0 nécessite quatre éléments, et aucun d'entre eux ne provient du bug lui-même : le bug ne fuit rien.
struct binder_proc fait 648 octets et est alloué avec un simple GFP_KERNEL kzalloc — pas __GFP_ACCOUNT. Il atterrit donc dans kmalloc-1k, avec toutes les autres allocations non comptabilisées de cette taille, et n'est pas isolé derrière kmalloc-cg-*. Ce seul fait est ce qui rend l'objet récupérable.
Le moment du kfree() n'est pas observable depuis l'espace utilisateur, pas plus que le moment où le walker touche à nouveau l'objet, il n'y a donc rien sur quoi se synchroniser. Le spray fonctionne donc comme une pompe : il alloue et libère des objets kmalloc-1k en continu, depuis le CPU qui a exécuté la libération différée, tant que les walkers sont en cours d'exécution.
Des messages System V sont utilisés pour cela. alloc_msg() est un simple kmalloc non comptabilisé d'un en-tête de 48 octets plus la charge utile, donc un message de 976 octets est une allocation de 1024 octets ; le quota est par file d'attente plutôt que par uid ; et msgrcv() libère de manière synchrone. Débit mesuré : ~198 000 allocations par seconde, zéro échec.
add_key/user_key_payload a été essayé en premier et c'est un piège. Sa charge utile est imputée à un quota d'octets par uid (kernel.keys.maxbytes, 20000 par défaut) qui n'est libéré que lorsque le garbage collector de clés détruit la clé, donc une boucle serrée d'allocation/libération l'épuise en millisecondes : mesuré 27 151 allocations réussies contre 2 121 009 échecs — 98,7 % du spray ne faisant silencieusement rien, ce qui ressemble exactement à un spray qui ne gagne jamais l'emplacement. KEYCTL_INVALIDATE a aggravé les choses (4 775 succès), car il met en file d'attente le travail du GC.
Le walker touche quatre champs du binder_proc libéré (décalages mesurés avec pahole sur la build cible) :
Avec outstanding_txns == 1 et is_frozen == 1, la décrémentation atteint zéro et le walker appelle wake_up_interruptible_all(&proc->freeze_wait). __wake_up_common calcule alors curr = head.next - 24 et appelle *(head.next - 8) : un pointeur de fonction lu depuis l'endroit où pointe head.next, qui est une valeur fournie par l'objet récupéré.
Ce pointeur doit atteindre une mémoire contrôlée par l'attaquant, à une adresse noyau, et le bug ne fuit rien. Les deux adresses proviennent plutôt du timing de prefetch — le même canal que KASLD (Brendan Coles, MIT), dont l'implémentation est dérivée ici :
Texte du noyau. Un prefetch d'une adresse noyau mappée se résout dans la marche de la table des pages et se retire de manière mesurablement plus rapide que celui d'une adresse non mappée, même si l'accès ne devient jamais visible architecturalement. Le balayage des emplacements de 2 MiB de la plage de texte montre l'image comme une suite d'emplacements rapides ; son premier emplacement est _text.
La carte directe. Avec CONFIG_RANDOMIZE_MEMORY, la carte directe est randomisée par unités de 1 GiB, elle doit donc aussi être localisée. Contrairement au texte, elle couvre toute la RAM, c'est donc la plus longue suite contiguë d'emplacements mappés. Deux raffinements ont été nécessaires pour la rendre utilisable :
Ce par quoi l'exécution commence de manière fiable n'est pas page_offset_base lui-même mais le premier emplacement que le noyau pourrait mapper avec une page de 1 GiB — celui couvrant les 4 GiB physiques. En dessous, le trou PCI et les réservations du firmware forcent des pages de 2 MiB dont la marche plus longue n'est pas séparable du non mappé ici. Cet emplacement est exactement ce dont le spray a besoin, c'est donc ce qui est utilisé.
Ensuite, ~60 % de la mémoire physique est remplie de copies d'une seule page de 4 KiB conçue sur mesure, de sorte qu'un décalage fixe depuis cette ancre est adossé à la page conçue, quelle que soit la disposition finale.
freeze_wait.head.next -> entry1 (in the sprayed page)
entry1.func = mov 0x28(%rdi),%rdi ; mov 0x18(%rdi),%rax ; jmp *0x58(%rax) RDI <- entry1+0x28 = &cred RAX <- cred+0x18 jmp *(RAX+0x58) = commit_creds -> commit_creds(cred)
entry2.func = mov $-1,%rax ; ret __wake_up_common breaks out on ret < 0
No stack pivot and no `iretq`: the hijacked thread returns out of its `ioctl()`
normally and is simply back in userspace, as root.
### The forged cred, and the bug that would have wasted it
The dispatcher gadget takes its `RAX` from `cred+0x18`, and `cred+0x18` is
`euid`/`egid`, so immediately after the chain `euid` is the low half of a kernel
pointer. That is cosmetic. What is not cosmetic is that `prepare_creds()` —
which **every later `fork()` and `execve()` calls** — dereferences three fields
with no NULL check:```c
get_group_info(new->group_info); /* refcount_inc(&gi->usage) */
get_uid(new->user); /* refcount_inc(&u->__count) */
new->ucounts = get_ucounts(new->ucounts);
Une identité forgée qui les laisse à NULL donne uid 0 puis fait paniquer la machine au
premier execve — c'est-à-dire que l'exploit signalerait un succès et détruirait
immédiatement la machine. La chaîne pointe donc user, ucounts et group_info vers
les globals réels du noyau root_user, init_ucounts et init_groups, dont
les adresses proviennent de la même base _text. Avec ceux-ci définis, le thread rooté
détient CAP_SETUID sur init_user_ns, il appelle donc setresuid(0,0,0) et le
noyau installe une identité root propre, allouée par le noyau, par-dessus l'identité forgée.
Ce n'est qu'ensuite que toute autre action est entreprise.
Le privilège est transmis au processus parent via une copie setuid-root du binaire de l'exploit, qui se supprime lui-même avant d'exécuter le shell, de sorte que le shell root s'exécute dans le processus principal sur un terminal propre et qu'aucun élément setuid n'est laissé derrière.
uname -r ne suffit pas. Les noyaux des fournisseurs intègrent couramment des correctifs binder sans
changer vers la version mainline correspondante ; inspectez la source ou le
journal des modifications du paquet.
Le correctif est marqué Cc: stable, donc les branches stable et fournisseur reçoivent des backports.
Ce sont des questions différentes, et la seconde est celle qui détermine l'impact.
Le code vulnérable est compilé partout où CONFIG_ANDROID_BINDER_IPC est défini.
Cela inclut les distributions à usage général — mais sur toutes celles examinées,
le pilote est un module qui n'est pas chargé par défaut, et même lorsqu'il est
chargé, init_binder_device() enregistre le périphérique misc sans définir
miscdev.mode, donc devtmpfs crée /dev/binder en 0600 root:root. Sur
Android, c'est ueventd qui l'ouvre en 0666, ce qui explique exactement pourquoi le bug
y est important et surtout pas ici.
Configurations lues depuis les paquets de noyau fournis par les distributions elles-mêmes :
L'exposition pratique sur une distribution de bureau est donc indirecte : tout ce qui charge binder et l'ouvre — Waydroid, Anbox, un émulateur Android ou un runtime de conteneurs — réintroduit exactement l'accessibilité Android sur une machine dont le noyau contient toujours le bug.
Celles-ci n'affectent pas la vulnérabilité ; elles affectent cet exploit.
Les deux noyaux sont des arbres upstream standard. Rien n'est patché.
CONFIG_KASAN_GENERIC dans le premier laboratoire est un détecteur, pas un facilitateur :
la course est identique sans lui. CONFIG_PREEMPT est une véritable condition préalable, et
c'est ce que fournit Android.
L'architecture n'est pas un facteur pour le bug — c'est une erreur de durée de vie dans du C indépendant de l'architecture. Elle est en revanche très importante pour l'exploit : les gadgets, le canal de prefetch et la disposition de la carte directe sont tous en x86_64.
Prérequis : clang, lld, make, cpio, gzip, qemu-system-x86_64,
docker (uniquement pour assembler le rootfs Debian), un clone local de l'arbre git Linux,
et gcc.
gcc -O2 -pthread -o exploit exploit.c ./exploit
Pour un noyau autre que celui dans `demo/`, extrayez d'abord ses offsets :```sh
./mkoffsets.sh /path/to/vmlinux > offsets.h
gcc -O2 -pthread -DEXPLOIT_OFFSETS='"offsets.h"' -o exploit exploit.c
Tunables, tous optionnels, tous lus depuis l’environnement :
CVE64468_SECONDS, CVE64468_THREADS, CVE64468_SPRAY_PERCENT,
CVE64468_CALL_OFFSET_MB, CVE64468_KASLR_ATTEMPTS, CVE64468_DELAY_MAX_US,
CVE64468_DELAY_STEP_US, CVE64468_STAGGER_US, CVE64468_VERBOSE,
CVE64468_SHELL.
LINUX_GIT=/path/to/linux ./lab/build.sh # x86_64 LINUX_GIT=/path/to/linux TARGET_ARCH=arm64 ./lab/build.sh # arm64 ./run.sh vulnerable ./verify.sh 600 16
Le script crée deux worktrees détachés aux commits ci-dessus, refuse de s'exécuter
si l'un des worktrees est sale, vérifie que l'arbre vulnérable ne contient pas le correctif et
que l'arbre corrigé le contient, compile les deux noyaux, et empaquette la preuve de concept
dans un initramfs.
### Le laboratoire d'exploitation```sh
./demo/build-kernel.sh # vulnerable kernel, KASAN off, hardening on
./demo/build-rootfs.sh # Debian 13 userland + gcc, as an initramfs
./demo/run-demo.sh # boot it; this is what the recording shows
./demo/verify.sh logs/ # unattended reliability run, one guest per trial
demo/run-demo.sh démarre l'invité et remet la console à l'uid 1000, qui
compile exploit.c avec le gcc propre à l'invité puis l'exécute.
Les images du noyau, les worktrees, les arborescences rootfs et les initramfs sont des artefacts de laboratoire et ne sont pas versionnés.
docs/example-output.txt est la transcription réelle du noyau vulnérable, y
compris le rapport KASAN ; docs/patched-negative-output.txt est le témoin du
noyau corrigé ; docs/e2e-results.json est le résultat lisible par machine.
La chaîne d'appels rapportée correspond exactement au rapport en amont :``` BUG: KASAN: slab-use-after-free in queued_spin_lock_slowpath+0x62/0x6a0 Read of size 4 at addr ffff888100282270 by task cve64468-B/91 CPU: 6 UID: 1000 PID: 91 Comm: cve64468-B Not tainted 7.2.0-rc1+ #2 PREEMPT _raw_spin_lock+0x55/0x60 binder_free_transaction+0x80/0x1f0 binder_send_failed_reply+0x98/0x340 binder_thread_release+0x528/0x5e0 binder_ioctl+0x1ca/0xeb0 __x64_sys_ioctl+0x8c9/0xcf0
Allocated by task 93: binder_open+0xb5/0x7b0
Freed by task 9: kfree+0x113/0x310 binder_deferred_func+0x104b/0x1180 process_scheduled_works+0x69f/0xbb0
`UID: 1000` est l'identité non privilégiée du proof of concept lui-même : la victime
`binder_proc` est allouée par `binder_open()`, libérée par la workqueue différée
de binder, et lue par le walker après la libération.
Exécution enregistrée, 2026-08-16, `./verify.sh 1200 16 3` — trois invités QEMU/KVM
concurrents par variante, 10 vCPU chacun, 16 threads, 1200 s par variante, **aucun
patch noyau d'aucun côté** :
| | Vulnérable `114a116aaa5f` | Corrigé `f223d27a546c` |
| --- | --- | --- |
| Tentatives | 158 384 | 158 471 |
| Walkers | 2 534 144 | 2 535 536 |
| Échecs de configuration | 0 | 0 |
| Accès vulnérable (`Unexpected outstanding_txns -1`) | 1 127 | 934 |
| **KASAN `slab-use-after-free`** | **8** | **0** |
Voilà la différence : la même charge de travail, le même nombre de tentatives à
0,06 % près, et l'use-after-free uniquement sur le noyau vulnérable non patché.
L'accès vulnérable apparaît sur *les deux* noyaux, et c'est attendu — le correctif
empêche le processus d'être libéré dans la fenêtre, pas la seconde décrémentation
de `outstanding_txns`. C'est pourquoi cette ligne n'est utilisée que comme un
oracle peu coûteux et jamais comme la différence.
### Élévation de privilèges

`docs/lpe-output.txt` est une transcription réelle d'un invité de démonstration, et
`docs/lpe-demo.cast` est l'enregistrement Asciinema complet et non édité à partir
duquel l'animation ci-dessus a été rendue (`asciinema play docs/lpe-demo.cast` la
rejoue dans son intégralité). L'animation élide la longue partie de course au
milieu de cet enregistrement — la console série de l'invité diffuse le debug de
binder pendant toute la course d'environ 24 minutes, ce qui rendrait des dizaines
de mégaoctets de journal défilant — en conservant le préambule Debian et le shell
root ; la ligne `hit after 26661 attempts ... in 1437s` de l'exploit indique
exactement ce qui a été élidé. Les deux sont des exécutions uniques : un invité
qui ne gagne pas dans son budget s'éteint, et l'enregistrement est simplement
répété plutôt que monté pour obtenir une victoire.
Voir *Fiabilité* ci-dessous pour le taux de réussite mesuré.
## Fiabilité
La course est probabiliste sur les deux plans, donc une exécution échouée est le
comportement attendu parfois, pas un exploit cassé.
### Sécurité mémoire
Taux dérivés sur le noyau vulnérable, à partir de l'exécution ci-dessus :
| Quantité | Valeur |
| --- | --- |
| Taux de tentatives | ~44 tentatives/s par invité, ~132/s sur trois |
| Accès vulnérable | 7,1e-3 par tentative |
| Libération atterrissant dans la fenêtre, étant donné l'accès | 7,1e-3 |
| Rapport KASAN | ~1 pour 20 000 tentatives, soit environ un toutes les 2,5 minutes à ce rythme |
### Élévation de privilèges
Chaque invité est un essai indépendant : son propre KASLR, sa propre
randomisation de la direct-map, et un invité qui gagne arrête de courir. Le chiffre
est donc un **taux de réussite par démarrage**, pas par tentative.
Campagne enregistrée, 2026-08-16 23:32 UTC, `GUESTS=6 CPUS=8 MEMORY_MB=9216 ./demo/verify.sh`
— six invités QEMU/KVM concurrents, `114a116aaa5f` non patché, userland Debian 13,
budget de 60 minutes chacun, exploit compilé dans l'invité, démarré en uid 1000 :
| | |
| --- | --- |
| Invités ayant atteint uid 0 | **2 sur 6** |
| Temps jusqu'à root | 623 s et 1 344 s |
| Tentatives lors de la course gagnante | 13 839 et 31 125 |
| Tentatives de chaque invité n'ayant pas gagné | ~95 000 sur les 3 600 s complètes |
| Total des tentatives sur la campagne | 427 676 (6,8 M de walkers de pile) |
| Accès vulnérables observés (`Unexpected outstanding_txns -1`) | 256 |
| **Crashes noyau, oops ou panics** | **0**, en 9 heures-invité |
Deux choses méritent d'être relevées dans ce tableau.
**Essentiellement chaque course gagnée est devenue root.** Le laboratoire KASAN
mesure la probabilité que la libération atterrisse dans la fenêtre, étant donné
l'accès vulnérable, à 7,1e-3. Appliqué aux 256 accès observés ici, cela prédit
~1,8 use-after-free sur la campagne — et 2 roots ont été obtenus. La récupération,
la découverte d'adresse et la chaîne ne sont pas le goulot d'étranglement ; la
course l'est.
**Rien n'a crashé.** Aucun invité n'a pris d'oops en neuf heures-invité, y compris
les quatre qui n'ont jamais gagné. Soit la chaîne se déclenche contre une page
correctement localisée et sprayée, soit elle ne se déclenche jamais du tout —
c'est à cela que sert le refus par vote majoritaire à l'étape 2.
Ce refus se déclenche en pratique. Le démarrage de six invités à la fois sur un
hôte déjà chargé en a produit un qui a abandonné à l'étape 2 avec```
[*] direct map not found; refusing to fire at an unverified address
et ont quitté sans course. C'est le comportement voulu : un démarrage gaspillé est le résultat correct lorsque le canal temporel ne peut pas atteindre un consensus, et c'est bien mieux que l'alternative de déclencher la chaîne à une adresse qui n'a jamais été confirmée.
docs/lpe-results.json contient la forme lisible par machine, y compris le SHA-256
du noyau et de l'initramfs exacts utilisés.
Exécuter les invités en parallèle ne sert pas seulement au parallélisme. Sur un hôte
contentionné, KVM désordonnance les vCPU invités, et c'est exactement le délai dont la
deuxième condition a besoin : le marcheur doit perdre son CPU entre la libération de t->lock
et la prise du verrou interne de la victime. Un seul invité sur un hôte inactif a été mesuré à
environ un vingtième du taux d'accès vulnérable de trois invités concurrents.
Il y a cependant une limite supérieure. Avec huit invités de 8 vCPU chacun sur un hôte à 32 threads, avec ~5 Gio de spray de carte directe par invité, l'hôte est passé en swap et trois des huit invités n'ont fait aucun progrès du tout. Six est la valeur par défaut fournie.
Les threads d'aide à la préemption optionnels dans l'invité ont été mesurés comme coûtant environ trois fois le taux de tentatives sans améliorer le taux de réussite, et ne sont pas utilisés.
Un facteur environnemental s'est avéré plus important que prévu : la sortie de débogage
de binder elle-même. Avec binder.debug_mask à sa valeur par défaut, le pilote émet une grande
quantité de trafic pr_info limité en débit pendant la course, et la pression printk et
de verrou de console que cela crée allonge exactement la fenêtre de préemption dont la
deuxième condition a besoin. La réduire au silence avec binder.debug_mask=0 pour une console
plus propre — la chose évidente à faire pour un enregistrement — a mesurablement réduit le taux
de réussite lors des tests : les invités ont largement dépassé les compteurs de tentatives des deux
gagnants sans succès. demo/run-demo.sh laisse donc le débogage binder à sa valeur par défaut, et une
console silencieuse est une option explicite. C'est une propriété du laboratoire, pas
de l'exploit — mais c'est une bonne illustration de la mesure dans laquelle cette course dépend
de la gigue temporelle à l'échelle du système plutôt que de quoi que ce soit que l'exploit lui-même contrôle.
/dev/binder, envoie des transactions binder et
quitte les threads binder. Elle n'installe rien et ne laisse rien derrière elle.panic=1 oops=panic afin qu'une exécution se termine au lieu de continuer sur un
état corrompu. Redémarrez toujours à partir d'un démarrage propre.<[email protected]> (Twitter :
@aramosf)Vérifié le 2026-08-16 avec SearchSploit (copie locale d'Exploit-DB) et une recherche
web pour CVE-2026-64468, binder_free_transaction et
f223d27a546c. Aucun exploit public ni preuve de concept pour cette CVE n'a été trouvé ;
SearchSploit ne renvoie que des entrées binder Android plus anciennes et sans rapport. Il s'agit d'une
vérification ponctuelle, pas d'une garantie permanente.
| État | Affirmation |
|---|
| Confirmé | La vulnérabilité est réelle, est atteignable depuis un processus sans privilèges, et le correctif amont la supprime. |
| Démontré | La déréférence vulnérable d'un binder_proc mourant est atteinte naturellement et de manière répétée sur le noyau non corrigé, et jamais sur le noyau corrigé. |
| Démontré | Le use-after-free complet, signalé par KASAN, sur le noyau non corrigé. |
| Démontré | La récupération du binder_proc libéré avec des octets contrôlés par l'attaquant, le détournement du flux de contrôle du noyau, et l'élévation de privilèges vers uid 0 depuis un utilisateur sans privilèges, sur x86_64. |
| Non affirmé | Tout résultat sur un appareil ou un fournisseur spécifique. Seuls les noyaux amont listés ci-dessous ont été testés, sur x86_64. |
| Non affirmé | Que l'exploit fourni fonctionne sans modification contre un noyau de distribution. Voir Quels systèmes sont concernés : il nécessite des décalages propres à chaque noyau, et sur chaque distribution à usage général examinée, le périphérique binder n'est de toute façon pas accessible à un utilisateur sans privilèges. |
| Décalage | Champ | Ce que fait le walker |
|---|
| 108 | int outstanding_txns | le décrémente |
| 113 | bool is_frozen | le lit |
| 120 | wait_queue_head_t freeze_wait | le parcourt si outstanding_txns == 0 && is_frozen |
| 624 | spinlock_t inner_lock | l'acquiert et le libère |
| État | Commit | Notes |
|---|
| Lignée d'introduction | a370003cc301 | Désigné par la balise Fixes: en amont |
| Validé comme vulnérable | 114a116aaa5f | Parent direct du correctif ; porte le correctif voisin CVE-2026-64469, donc la paire isole CVE-2026-64468 seul |
| Mainline corrigé | f223d27a546c | Le correctif testé |
| Distribution | Noyau | ANDROID_BINDER_IPC | Périphérique | SLAB_BUCKETS | RANDOM_KMALLOC_CACHES | Accessible sans privilèges ? |
|---|
| Debian 13 (trixie) | 6.12.101 | m | ANDROID_BINDER_DEVICES="binder", BINDERFS désactivé | y | désactivé | Non — module non chargé ; /dev/binder est 0600 |
| Debian 12 (bookworm) | 6.1.0 | m | ANDROID_BINDER_DEVICES="binder", BINDERFS désactivé | n/d (avant 6.11) | n/d (avant 6.6) | Non — idem |
| Ubuntu 24.04 LTS | 6.8.0 | m | BINDERFS=m, ANDROID_BINDER_DEVICES="" | n/d (avant 6.11) | y | Non — nécessite root pour mount -t binder |
| Ubuntu 22.04 LTS | 5.15.0 | m | BINDERFS=m, ANDROID_BINDER_DEVICES="" | n/d | n/d | Non — idem |
| Android (AOSP / fournisseur) | 6.1, 6.6, 6.12 GKI | y | /dev/binder, /dev/hwbinder, /dev/vndbinder | — | — | Oui — le pilote est l'IPC de la plateforme et est accessible à tous |
| Option | Effet ici |
|---|
CONFIG_SLAB_BUCKETS (6.11+) | Fatal pour cette récupération. Il isole msg_msg dans ses propres buckets kmalloc, donc la pompe ne peut jamais atterrir dans l'emplacement de binder_proc. Debian 13 le définit. Une autre allocation non comptabilisée de 1 Kio devrait être trouvée. Le noyau 6.6, qui est celui que les appareils Android concernés exécutent, le précède entièrement. |
CONFIG_RANDOM_KMALLOC_CACHES (6.6+) | Divise kmalloc-1k en plusieurs caches par site d'appel, donc la pompe doit atteindre le même ; une taxe de 1 sur 16 sur la récupération, pas un mur. Ubuntu le définit, Debian non. |
Isolation des tables de pages (nopti non utilisé) | Fatal pour la découverte d'adresses. Le prefetch ne peut pas voir le texte du noyau avec PTI actif, et l'exploit détecte cela et s'arrête. PTI est compilé sur chaque distribution ci-dessus, mais le CPU décide s'il est actif : il est désactivé sur le matériel non affecté par Meltdown, c'est là que ces résultats ont été mesurés. |
CONFIG_SLAB_FREELIST_RANDOM, ..._HARDENED | Activés en laboratoire, comme les distributions les fournissent. Aucun effet mesurable : la pompe ne prédit pas l'ordre de la freelist, elle alloue simplement un très grand nombre d'objets. |
KASLR (RANDOMIZE_BASE, RANDOMIZE_MEMORY) | Activé. Contré par les étapes de prefetch ; pas de nokaslr. |
| Décalages par noyau | La chaîne a besoin de commit_creds, de trois globals liés aux identités et de deux gadgets, comme décalages depuis _text. mkoffsets.sh les extrait d'un vmlinux cible ; sans eux, l'exploit tire sur les mauvaises adresses. C'est une propriété propre à chaque compilation de tout exploit de noyau, pas une défense. |
Laboratoire de sécurité mémoire (lab/) | Laboratoire d'exploitation (demo/) |
|---|
| Version du noyau | 7.2.0-rc1+ | 7.2.0-rc1+ |
| Commit vulnérable | 114a116aaa5f0295376cdf12da743c5bce3b20ce | identique |
| Commit corrigé | f223d27a546c1e1f48d38fd67760e78f068fe8c4 | — (l'exploitation est mesurée uniquement sur le noyau vulnérable) |
| Architecture | x86_64 (KVM) et arm64 (TCG) | x86_64 (KVM) |
| Compilateur | Ubuntu clang 21.1.8 / LLD 21.1.8 | identique |
| KASAN | activé — c'est le détecteur | désactivé — il modifie la disposition des slabs et rendrait toute récupération non représentative |
| Durcissement des slabs | — | SLAB_FREELIST_RANDOM, SLAB_FREELIST_HARDENED activés ; SLAB_BUCKETS, RANDOM_KMALLOC_CACHES désactivés |
| KASLR | — | RANDOMIZE_BASE, RANDOMIZE_MEMORY activés |
| Userland | initramfs minimal | Debian GNU/Linux 13 (trixie), avec le gcc propre à la distribution |
| Identité de départ | uid 1000, gid 1000, aucune capacité, aucun namespace | identique |
| Ligne de commande de démarrage | console=ttyS0 rdinit=/init panic=1 oops=panic kasan_multi_shot=1 | console=ttyS0 loglevel=4 rdinit=/init — pas de nopti, pas de nokaslr, pas de mitigations=off |