
PoC vérifié et analyse d'un contournement de vérification de permissions dans perf/core sous Linux, qui divulgue des adresses de chaîne d'appels du noyau à des tâches Android non privilégiées, permettant ainsi de dériver le décalage KASLR.
English | 简体中文
Contournement de la vérification de permissions dans perf/core de Linux : une tâche non privilégiée peut demander des callchains du noyau tout en définissant exclude_kernel=1, contournant ainsi le contrôle perf_allow_kernel() et divulguant des frames de pile du noyau via le tampon circulaire perf. Ce dépôt contient l'analyse et un PoC vérifié ; la faille permet une fuite d'informations, et non une primitive d'élévation de privilèges directe.
| Élément | Valeur |
|---|---|
| CVE | CVE-2026-98937 |
| Classe | Contournement de permissions / divulgation d'informations (CWE-284/CWE-269) |
| Cible | Android arm64 (vérifié sur HONOR YLP-W00 et Redmi K70) |
| Noyau | YLP-W00 : 6.12.38-android16-5-gfde7767f6ef6-abogki481467632-4k ; K70 : 5.15.178-android13-8-00021-g6f2f96be86b9-ab13729987 |
| SELinux | Enforcing (u:r:shell:s0) |
| Corrigé | Non ; corrigé en amont dans 6.12.113 / 6.18.56 / 7.2.10 / 7.3+ |
| Statut | PoC vérifié ; oracle de décalage KASLR validé sur 6.12.38 et 5.15.178 |
| Pré-vérification | src/vuln_precheck.h |
Les noyaux antérieurs au correctif ci-dessous sont affectés. Corrigé par
a4b016d0a29038a5b7c96bf62c054bf7832451d9
("perf/core: Check kernel access when kernel callchains are requested",
Dapeng Mi, fusionné le 2026-06-30) et les rétroportages stables correspondants.
perf_event_open() ne conditionnait auparavant le profilage du noyau qu'à
!attr.exclude_kernel :
if (!attr.exclude_kernel) {
err = perf_allow_kernel(&attr);
if (err)
return err;
}
perf_allow_kernel() contient à la fois la vérification perf_event_paranoid > 1 et le hook SELinux perf_event { kernel }. Une tâche peut définir exclude_kernel=1 tout en demandant encore des callchains noyau (PERF_SAMPLE_CALLCHAIN avec exclude_callchain_kernel=0) ; perf_callchain() collecte alors la partie noyau indépendamment du filtre d'échantillonnage. Les événements PMU matériels ne comptant que le mode utilisateur peuvent déclencher leur interruption de débordement en contexte noyau (skid), de sorte que perf_callchain_kernel() déroule la pile noyau et écrit des adresses de texte noyau dans le tampon circulaire.
Le correctif appelle perf_allow_kernel() lorsque l'échantillonnage noyau ou les callchains noyau sont demandés :
if (!attr.exclude_kernel ||
((attr.sample_type & PERF_SAMPLE_CALLCHAIN) &&
!attr.exclude_callchain_kernel)) {
err = perf_allow_kernel();
if (err)
return err;
}
Le snippet ci-dessus est la forme de la branche stable 6.12.113, dans laquelle
perf_allow_kernel() ne prend plus de pointeur d'attribut ; le commit amont
utilise la même condition avec perf_allow_kernel(&attr).
Voir docs/analysis.md pour le chemin d'appel complet, la
mécanique du skid, les preuves d'audit SELinux et les mesures sur appareil.
Le PoC exécute trois cas en tant que processus shell non privilégié :
| Cas | attributs perf_event | Attendu sur un noyau vulnérable |
|---|---|---|
| A | exclude_kernel=0 | EACCES (filtre kernel-perf actif) |
| B | exclude_kernel=1, exclude_callchain_kernel=1 | événement autorisé, aucune frame de callchain noyau |
| C | exclude_kernel=1, exclude_callchain_kernel=0 | événement autorisé, frames de callchain noyau divulguées |
Sur le YLP-W00 vérifié (noyau 6.12.38, SELinux Enforcing) :
EACCES, avec
avc: denied { kernel } ... tclass=perf_event dans le journal d'audit.0xffffffde....Sur le Redmi K70 vérifié (noyau 5.15.178, ROM Android 17 tierce, SELinux Enforcing), le même schéma a été observé :
EACCES, même ligne d'audit SELinux { kernel }.0xffffffdd....Sur les appareils vérifiés, un noyau corrigé rejetterait le cas C au niveau de
perf_event_open() avec EACCES, car la requête de callchain noyau atteint
désormais perf_allow_kernel().
Avec un vmlinux correspondant, les frames de callchain divulguées deviennent un
oracle de décalage KASLR par démarrage. Le décalage virtuel arm64 est aligné sur
2 MiB, et chaque adresse de texte divulguée doit satisfaire
_stext <= addr - slide < _etext ; l'intersection de ces intervalles ne laisse
normalement qu'un seul décalage.
make slide \
SLIDE_VMLINUX=/path/to/vmlinux-6.12.38.elf \
SLIDE_LINK_BASE=0xffffffc080000000 \
SLIDE_LOG=logs/poc-ylp-w00-20261027-020133-period1000.log
Sur le boot YLP-W00 6.12.38 vérifié, trois frames donnent le
slide=0x1DCFC00000 unique (runtime _text=0xffffffde4fc00000) :
| Adresse fuitée | Adresse au link-time | Symbole |
|---|---|---|
0xffffffde50748c78 | 0xffffffc080b48c78 | local_daif_restore+0x4 (msr DAIF, x0; ret) |
0xffffffde50d406e0 | 0xffffffc0811406e0 | el0t_64_sync_handler+0x70 (après bl el0_svc) |
0xffffffde4fc11588 | 0xffffffc080011588 | el0t_64_sync+0x1a8 (après bl el0t_64_sync_handler) |
Ce sont des frames de texte du noyau principal, pas des modules : l'interruption PMU en attente est
délivrée exactement au ret après msr DAIF, x0 dans le chemin d'entrée
d'appel système. Les adresses brutes ne peuvent pas être symbolisées directement sous KASLR ;
il faut d'abord soustraire le slide. Si une frame est une adresse de module, aucun slide ne place toutes
les adresses dans [_stext, _etext) et le script le signale au lieu de
deviner. Le slide change à chaque boot et ne doit pas être mis en cache entre
reboots.
Le Redmi K70 (5.15.178) a également été validé. Son image correspondante a été
extraite de /sdcard/boot.img (boot v4 ; la chaîne de version intégrée correspond
au noyau en cours d'exécution) et vmlinux-to-elf a récupéré 158 668 symboles. Sept frames uniques
issues d'une exécution de période 1000 donnent slide=0x1D93800000 (runtime
_text=0xffffffdd9b800000), correspondant à syscall_trace_enter,
el0_svc_common, do_el0_svc, el0_svc, el0t_64_sync_handler,
el0t_64_sync et syscall_trace_exit.
La base au link-time est spécifique à la branche — ne réutilisez pas la valeur 6.12 sur 5.15 :
| Branche du noyau | KIMAGE_VADDR |
|---|---|
| Android 13-5.15 (K70) | 0xffffffc008000000 |
| Android 16-6.12 (YLP-W00) | 0xffffffc080000000 |
| upstream v5.15 (disposition différente) | 0xffffffc010000000 |
vmlinux-to-elf a auto-détecté 0xffffffc008000000 pour l'image du K70, correspondant à
AOSP android13-5.15. Si la base ELF auto-détectée diffère du
KIMAGE_VADDR de la branche, passez --link-base (ou SLIDE_LINK_BASE=) : la symbolisation reste
dans l'espace ELF, le slide rapporté est rendu relatif à la vraie base, et le
_text runtime est invariant à un décalage constant de la base. Fournir la fuite du K70 à
l'image 6.12.38 reste un test négatif délibéré et ne produit aucun candidat.
Prérequis :
/opt/android-ndk-r29 (remplaçable avec NDK=)make # PoC, arm64, API 24
make ARCH=arm API=29 # other ABI / API level
make host # static build for qemu-aarch64
make emu # build and run the static PoC locally
make help
Les artefacts sont écrits dans build/ et sont ignorés par git.