
PoC verificado y análisis de un bypass de comprobación de permisos en perf/core de Linux que filtra direcciones de la cadena de llamadas del kernel a tareas de Android sin privilegios, lo que permite derivar el desplazamiento de KASLR.
English | 简体中文
Omisión de la comprobación de permisos en perf/core de Linux: una tarea sin privilegios puede solicitar
callchains del kernel mientras establece exclude_kernel=1, saltándose la
barrera perf_allow_kernel() y divulgando marcos de pila del kernel a través del
ring buffer de perf. Este repositorio contiene el análisis y un PoC verificado; el fallo
produce una fuga de información, no una primitiva directa de escalada de privilegios.
| Elemento | Valor |
|---|---|
| CVE | CVE-2026-98937 |
| Clase | Omisión de permisos / divulgación de información (CWE-284/CWE-269) |
| Objetivo | Android arm64 (verificado en HONOR YLP-W00 y 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) |
| Parcheado | No; corregido upstream en 6.12.113 / 6.18.56 / 7.2.10 / 7.3+ |
| Estado | PoC verificado; oráculo de desplazamiento KASLR validado en 6.12.38 y 5.15.178 |
| Comprobación previa | src/vuln_precheck.h |
Los kernels anteriores a la corrección indicada a continuación están afectados. Corregido por
a4b016d0a29038a5b7c96bf62c054bf7832451d9
("perf/core: Check kernel access when kernel callchains are requested",
Dapeng Mi, fusionado el 2026-06-30) y los backports estables correspondientes.
perf_event_open() solía controlar el perfilado del kernel únicamente mediante
!attr.exclude_kernel:
if (!attr.exclude_kernel) {
err = perf_allow_kernel(&attr);
if (err)
return err;
}
perf_allow_kernel() contiene tanto la comprobación perf_event_paranoid > 1 como el hook de SELinux perf_event { kernel }. Una tarea puede establecer exclude_kernel=1 mientras sigue solicitando callchains del kernel (PERF_SAMPLE_CALLCHAIN con exclude_callchain_kernel=0); perf_callchain() entonces recopila la parte del kernel de forma independiente al filtro de muestreo. Los eventos de PMU de hardware que cuentan solo el modo usuario pueden tomar su interrupción de desbordamiento en contexto de kernel (skid), por lo que perf_callchain_kernel() desenrolla la pila del kernel y escribe direcciones de texto del kernel en el ring buffer.
La corrección llama a perf_allow_kernel() cuando se solicita muestreo del kernel o callchains del kernel:
if (!attr.exclude_kernel ||
((attr.sample_type & PERF_SAMPLE_CALLCHAIN) &&
!attr.exclude_callchain_kernel)) {
err = perf_allow_kernel();
if (err)
return err;
}
El fragmento anterior es la forma del árbol estable 6.12.113, en la que
perf_allow_kernel() ya no recibe un puntero de atributo; el commit upstream
usa la misma condición con perf_allow_kernel(&attr).
Consulte docs/analysis.md para ver la ruta de llamada completa, la
mecánica del skid, la evidencia de auditoría de SELinux y las mediciones del dispositivo.
El PoC ejecuta tres casos como un proceso de shell sin privilegios:
| Caso | atributos de perf_event | Esperado en un kernel vulnerable |
|---|---|---|
| A | exclude_kernel=0 | EACCES (puerta de kernel-perf activa) |
| B | exclude_kernel=1, exclude_callchain_kernel=1 | evento permitido, sin frames de callchain del kernel |
| C | exclude_kernel=1, exclude_callchain_kernel=0 | evento permitido, frames de callchain del kernel divulgados |
En el YLP-W00 verificado (kernel 6.12.38, SELinux Enforcing):
EACCES, con
avc: denied { kernel } ... tclass=perf_event en el registro de auditoría.0xffffffde....En el Redmi K70 verificado (kernel 5.15.178, ROM Android 17 de terceros, SELinux Enforcing), se observó el mismo patrón:
EACCES, la misma línea de auditoría de SELinux { kernel }.0xffffffdd....En los dispositivos verificados, un kernel corregido rechazaría el caso C en
perf_event_open() con EACCES porque la solicitud de callchain del kernel ahora
llega a perf_allow_kernel().
Con un vmlinux coincidente, los frames de callchain filtrados se convierten en un oráculo
del slide de KASLR por arranque. El slide virtual de arm64 está alineado a 2 MiB, y cada dirección
de texto filtrada debe satisfacer _stext <= addr - slide < _etext; la intersección de esos
intervalos normalmente deja exactamente un 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
En el arranque verificado YLP-W00 6.12.38, tres frames dan el único
slide=0x1DCFC00000 (en tiempo de ejecución _text=0xffffffde4fc00000):
| Dirección filtrada | Dirección en tiempo de enlace | Símbolo |
|---|---|---|
0xffffffde50748c78 | 0xffffffc080b48c78 | local_daif_restore+0x4 (msr DAIF, x0; ret) |
0xffffffde50d406e0 | 0xffffffc0811406e0 | el0t_64_sync_handler+0x70 (después de bl el0_svc) |
0xffffffde4fc11588 | 0xffffffc080011588 | el0t_64_sync+0x1a8 (después de bl el0t_64_sync_handler) |
Estos son frames del texto del núcleo, no de módulos: la interrupción PMU pendiente se
entrega exactamente en el ret posterior a msr DAIF, x0 en la ruta de entrada de la
llamada al sistema. Las direcciones sin procesar no pueden simbolizarse directamente bajo KASLR;
primero hay que restar el slide. Si un frame es una dirección de módulo, ningún slide coloca todas las
direcciones en [_stext, _etext) y el script lo informa en lugar de
adivinar. El slide cambia en cada arranque y no debe almacenarse en caché entre
reinicios.
El Redmi K70 (5.15.178) también ha sido validado. Su imagen correspondiente se
extrajo de /sdcard/boot.img (boot v4; la cadena de versión incrustada coincide
con el núcleo en ejecución) y vmlinux-to-elf recuperó 158.668 símbolos. Siete frames únicos
de una ejecución con período 1000 dan slide=0x1D93800000 (en tiempo de ejecución
_text=0xffffffdd9b800000), que se asignan a syscall_trace_enter,
el0_svc_common, do_el0_svc, el0_svc, el0t_64_sync_handler,
el0t_64_sync y syscall_trace_exit.
La base en tiempo de enlace es específica de cada rama — no reutilices el valor de 6.12 en 5.15:
| Rama del núcleo | KIMAGE_VADDR |
|---|---|
| Android 13-5.15 (K70) | 0xffffffc008000000 |
| Android 16-6.12 (YLP-W00) | 0xffffffc080000000 |
| upstream v5.15 (disposición diferente) | 0xffffffc010000000 |
vmlinux-to-elf detectó automáticamente 0xffffffc008000000 para la imagen del K70, coincidiendo con
AOSP android13-5.15. Si la base ELF detectada automáticamente difiere del
KIMAGE_VADDR de la rama, pasa --link-base (o SLIDE_LINK_BASE=): la simbolización permanece
en el espacio ELF, el slide informado se hace relativo a la base verdadera, y el
_text en tiempo de ejecución es invariante ante un desplazamiento constante de la base. Alimentar la filtración del K70 a
la imagen 6.12.38 sigue siendo una prueba negativa deliberada y no produce ningún candidato.
Requisitos:
/opt/android-ndk-r29 (sobrescribir con NDK=)