
PoC verificato e analisi per un bypass del controllo dei permessi in perf/core di Linux che espone gli indirizzi della callchain del kernel a task Android non privilegiati, consentendo la derivazione dello slide KASLR.
English | 简体中文
Bypass del controllo dei permessi in perf/core di Linux: un task non privilegiato può richiedere
le callchain del kernel impostando exclude_kernel=1, aggirando il
gate perf_allow_kernel() e divulgando i frame dello stack del kernel tramite il
ring buffer di perf. Questo repository contiene l'analisi e un PoC verificato; il bug
consente una fuga di informazioni, non una primitiva diretta di escalation dei privilegi.
| Voce | Valore |
|---|---|
| CVE | CVE-2026-98937 |
| Classe | Bypass dei permessi / divulgazione di informazioni (CWE-284/CWE-269) |
| Target | Android arm64 (verificato su HONOR YLP-W00 e Redmi K70) |
| Kernel | 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) |
| Corretto | No; corretto upstream in 6.12.113 / 6.18.56 / 7.2.10 / 7.3+ |
| Stato | PoC verificato; oracolo per lo slide KASLR convalidato su 6.12.38 e 5.15.178 |
| Pre-check | src/vuln_precheck.h |
I kernel precedenti alla correzione seguente sono interessati. Corretto da
a4b016d0a29038a5b7c96bf62c054bf7832451d9
("perf/core: Check kernel access when kernel callchains are requested",
Dapeng Mi, unito il 2026-06-30) e dai corrispondenti backport stabili.
perf_event_open() controllava il profiling del kernel solo in base a
!attr.exclude_kernel:
if (!attr.exclude_kernel) {
err = perf_allow_kernel(&attr);
if (err)
return err;
}
perf_allow_kernel() contiene sia il controllo perf_event_paranoid > 1 sia
l'hook SELinux perf_event { kernel }. Un task può impostare exclude_kernel=1
pur richiedendo comunque le callchain del kernel (PERF_SAMPLE_CALLCHAIN con
exclude_callchain_kernel=0); perf_callchain() raccoglie quindi la parte del
kernel indipendentemente dal filtro di campionamento. Gli eventi PMU hardware
che contano solo la modalità utente possono ricevere il proprio interrupt di
overflow in contesto kernel (skid), quindi perf_callchain_kernel() esegue
l'unwinding dello stack del kernel e scrive gli indirizzi del testo del kernel
nel ring buffer.
La correzione chiama perf_allow_kernel() quando viene richiesto il
campionamento del kernel o le callchain del kernel:
if (!attr.exclude_kernel ||
((attr.sample_type & PERF_SAMPLE_CALLCHAIN) &&
!attr.exclude_callchain_kernel)) {
err = perf_allow_kernel();
if (err)
return err;
}
Lo snippet sopra è la forma del ramo stabile 6.12.113, in cui
perf_allow_kernel() non accetta più un puntatore ad attributo; il commit
upstream usa la stessa condizione con perf_allow_kernel(&attr).
Vedi docs/analysis.md per il percorso di chiamata completo, la meccanica
dello skid, le evidenze di audit SELinux e le misurazioni sul dispositivo.
Il PoC esegue tre casi come processo shell non privilegiato:
| Caso | attributi perf_event | Atteso su un kernel vulnerabile |
|---|---|---|
| A | exclude_kernel=0 | EACCES (gate kernel-perf attivo) |
| B | exclude_kernel=1, exclude_callchain_kernel=1 | evento consentito, nessun frame di callchain del kernel |
| C | exclude_kernel=1, exclude_callchain_kernel=0 | evento consentito, frame di callchain del kernel divulgati |
Sul dispositivo verificato YLP-W00 (kernel 6.12.38, SELinux Enforcing):
EACCES, con
avc: denied { kernel } ... tclass=perf_event nel log di audit.0xffffffde....Sul dispositivo verificato Redmi K70 (kernel 5.15.178, ROM Android 17 di terze parti, SELinux Enforcing), è stato osservato lo stesso schema:
EACCES, stessa riga di audit SELinux { kernel }.0xffffffdd....Sui dispositivi verificati, un kernel corretto rifiuterebbe il caso C a
perf_event_open() con EACCES poiché la richiesta di callchain del kernel ora
raggiunge perf_allow_kernel().
Con un vmlinux corrispondente, i frame di callchain divulgati diventano un
oracolo dello slide KASLR per ogni avvio. Lo slide virtuale arm64 è allineato a 2 MiB, e ogni indirizzo
di testo divulgato deve soddisfare _stext <= addr - slide < _etext; intersecando tali
intervalli normalmente rimane esattamente uno slide.
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
Sul boot YLP-W00 6.12.38 verificato, tre frame forniscono lo slide=0x1DCFC00000 univoco (runtime _text=0xffffffde4fc00000):
| Indirizzo divulgato | Indirizzo link-time | Simbolo |
|---|---|---|
0xffffffde50748c78 | 0xffffffc080b48c78 | local_daif_restore+0x4 (msr DAIF, x0; ret) |
0xffffffde50d406e0 | 0xffffffc0811406e0 | el0t_64_sync_handler+0x70 (dopo bl el0_svc) |
0xffffffde4fc11588 | 0xffffffc080011588 | el0t_64_sync+0x1a8 (dopo bl el0t_64_sync_handler) |
Questi sono frame di testo del kernel core, non moduli: l'interrupt PMU in sospeso viene
consegnato esattamente al ret dopo msr DAIF, x0 nel percorso di ingresso della syscall.
Gli indirizzi grezzi non possono essere simbolizzati direttamente sotto KASLR;
sottrarre prima lo slide. Se un frame è un indirizzo di modulo, nessuno slide colloca tutti
gli indirizzi in [_stext, _etext) e lo script lo segnala invece di
tirare a indovinare. Lo slide cambia a ogni avvio e non deve essere memorizzato tra
riavvii.
Anche il Redmi K70 (5.15.178) è stato validato. La sua immagine corrispondente è stata
estratta da /sdcard/boot.img (boot v4; la stringa di versione incorporata corrisponde
al kernel in esecuzione) e vmlinux-to-elf ha recuperato 158.668 simboli. Sette frame univoci
da una singola esecuzione con periodo 1000 forniscono slide=0x1D93800000 (runtime
_text=0xffffffdd9b800000), mappando su syscall_trace_enter,
el0_svc_common, do_el0_svc, el0_svc, el0t_64_sync_handler,
el0t_64_sync e syscall_trace_exit.
La base link-time è specifica del branch — non riutilizzare il valore 6.12 su 5.15:
| Branch del kernel | KIMAGE_VADDR |
|---|---|
| Android 13-5.15 (K70) | 0xffffffc008000000 |
| Android 16-6.12 (YLP-W00) | 0xffffffc080000000 |
| upstream v5.15 (layout diverso) | 0xffffffc010000000 |
vmlinux-to-elf ha rilevato automaticamente 0xffffffc008000000 per l'immagine del K70, corrispondente
ad AOSP android13-5.15. Se la base ELF rilevata automaticamente differisce dal
KIMAGE_VADDR del branch, passare --link-base (o SLIDE_LINK_BASE=): la simbolizzazione resta
nello spazio ELF, lo slide riportato viene reso relativo alla base reale, e il
runtime _text è invariante rispetto a uno spostamento costante della base. Fornire il leak del K70
all'immagine 6.12.38 resta un test negativo deliberato e non produce alcun candidato.
Requisiti:
/opt/android-ndk-r29 (sovrascrivere con 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
Gli artefatti vengono scritti in build/ e sono ignorati da git.