Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2026-64468 — 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é. | Kitploit
Outils/GitHubGitHub/aramosf/cve-2026-64468
Escalade de PrivilègesFrameworks d'ExploitationAnalyse des VulnérabilitésExploitationApprentissage et ÉducationExploitation de Binaires
GitHubaramosf/cve-2026-64468

CVE-2026-64468

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é.

Voir le dépôt
1il y a 15 joursPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2026-64468 — Use-after-free de durée de vie de processus dans binder_free_transaction() du noyau Linux

Exécution QEMU/KVM en direct : le noyau vulnérable non corrigé signale le use-after-free, le noyau corrigé non

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.

État et portée

Vulnérabilité

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);

root@kitploit:~
if (target_proc) {
	binder_inner_proc_lock(target_proc);   /* use after free */
root@kitploit:~
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

root@kitploit:~
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

root@kitploit:~
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.

De l'use-after-free au root

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.

1. Un cache partagé

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.

2. Une pompe de récupération qui suit le rythme

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.

3. Ce que l'objet récupéré doit contenir

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é.

4. Deux adresses, depuis un canal auxiliaire

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 :

    • un seul prefetch ne sépare le mappé du non mappé que d'environ ~4 cycles sous KVM, ce qui ne survit pas au bruit, donc chaque échantillon chronomètre un lot de 400 prefetches (mesuré 311 contre 523 cycles — séparable) ;
    • un seul balayage n'est pas fiable sur un hôte chargé — 10/10 corrects avec un seul invité à la fois, 3/5 avec cinq invités simultanément — donc le balayage est exécuté cinq fois et une majorité est requise. Sans consensus, l'exploit signale un échec et s'arrête plutôt que de tirer sur une adresse non vérifiée.

    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.

La chaîne```

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

root@kitploit:~
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.

Quels systèmes sont concernés

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.

Où se trouve le bug, et où il est accessible

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.

Ce que coûte chaque option de durcissement à l'exploit

Celles-ci n'affectent pas la vulnérabilité ; elles affectent cet exploit.

Cibles validées

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.

Compilation et exécution

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.

L'exploit, sur un système déjà vulnérable```sh

gcc -O2 -pthread -o exploit exploit.c ./exploit

root@kitploit:~
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.

Le laboratoire de sécurité mémoire```sh

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

root@kitploit:~
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.

Exécutions réelles

Sécurité mémoire, vulnérable versus corrigé

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

root@kitploit:~
`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

![Exécution QEMU/KVM en direct : invité Debian 13, utilisateur non privilégié compile exploit.c avec le gcc de l'invité et aboutit à une invite root](https://assets.kitploit.com/production/public/readmes/54229/da1e333d7516671ffab64e10d89604ee2a8043de1c48306abc03aaeee88d0d62/205a1253ab216ae4b62a65f761fffb9f0329bcbf9e4feb38563647f68ad2c5e0-display-v1.webp)

`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.

Pourquoi plusieurs invités, et pourquoi un hôte sursouscrit

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.

Sécurité

  • Les deux invités sont des images initramfs jetables. Rien n'est écrit sur le disque de l'invité ou de l'hôte.
  • La preuve de concept ouvre uniquement /dev/binder, envoie des transactions binder et quitte les threads binder. Elle n'installe rien et ne laisse rien derrière elle.
  • L'exploit écrit un fichier : une copie setuid-root de lui-même, utilisée pour transmettre le privilège du thread gagnant au processus parent. Il supprime le lien de cette copie avant d'exécuter le shell, donc rien de setuid ne survit à l'exécution.
  • Une course perdue peut provoquer un panic ou un blocage de l'invité ; le laboratoire de sécurité mémoire démarre avec 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.

Crédits

  • Auteur de l'exploit : A. Ramos <[email protected]> (Twitter : @aramosf)
  • Découverte et rapport de la vulnérabilité : Alice Ryhl, Google
  • Correctif en amont : Carlos Llamas, Google
  • Canal latéral KASLR par préchargement : dérivé de KASLD, Copyright (c) 2019 Brendan Coles, sous licence MIT. La variante de carte directe est un nouveau travail ici.

Recherche d'exploit public

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.

Télécharger l’outil
ÉtatAffirmation
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écalageChampCe que fait le walker
108int outstanding_txnsle décrémente
113bool is_frozenle lit
120wait_queue_head_t freeze_waitle parcourt si outstanding_txns == 0 && is_frozen
624spinlock_t inner_lockl'acquiert et le libère
ÉtatCommitNotes
Lignée d'introductiona370003cc301Désigné par la balise Fixes: en amont
Validé comme vulnérable114a116aaa5fParent direct du correctif ; porte le correctif voisin CVE-2026-64469, donc la paire isole CVE-2026-64468 seul
Mainline corrigéf223d27a546cLe correctif testé
DistributionNoyauANDROID_BINDER_IPCPériphériqueSLAB_BUCKETSRANDOM_KMALLOC_CACHESAccessible sans privilèges ?
Debian 13 (trixie)6.12.101mANDROID_BINDER_DEVICES="binder", BINDERFS désactivéydésactivéNon — module non chargé ; /dev/binder est 0600
Debian 12 (bookworm)6.1.0mANDROID_BINDER_DEVICES="binder", BINDERFS désactivén/d (avant 6.11)n/d (avant 6.6)Non — idem
Ubuntu 24.04 LTS6.8.0mBINDERFS=m, ANDROID_BINDER_DEVICES=""n/d (avant 6.11)yNon — nécessite root pour mount -t binder
Ubuntu 22.04 LTS5.15.0mBINDERFS=m, ANDROID_BINDER_DEVICES=""n/dn/dNon — idem
Android (AOSP / fournisseur)6.1, 6.6, 6.12 GKIy/dev/binder, /dev/hwbinder, /dev/vndbinder——Oui — le pilote est l'IPC de la plateforme et est accessible à tous
OptionEffet 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, ..._HARDENEDActivé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 noyauLa 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 noyau7.2.0-rc1+7.2.0-rc1+
Commit vulnérable114a116aaa5f0295376cdf12da743c5bce3b20ceidentique
Commit corrigéf223d27a546c1e1f48d38fd67760e78f068fe8c4— (l'exploitation est mesurée uniquement sur le noyau vulnérable)
Architecturex86_64 (KVM) et arm64 (TCG)x86_64 (KVM)
CompilateurUbuntu clang 21.1.8 / LLD 21.1.8identique
KASANactivé — c'est le détecteurdé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
Userlandinitramfs minimalDebian GNU/Linux 13 (trixie), avec le gcc propre à la distribution
Identité de départuid 1000, gid 1000, aucune capacité, aucun namespaceidentique
Ligne de commande de démarrageconsole=ttyS0 rdinit=/init panic=1 oops=panic kasan_multi_shot=1console=ttyS0 loglevel=4 rdinit=/init — pas de nopti, pas de nokaslr, pas de mitigations=off