
PoC verificado e análise de um bypass de verificação de permissão no perf/core do Linux que vaza endereços de callchain do kernel para tarefas Android sem privilégios, permitindo a derivação do slide do KASLR.
Português | 简体中文
Bypass da verificação de permissão no perf/core do Linux: uma tarefa sem privilégios pode solicitar
callchains do kernel enquanto define exclude_kernel=1, ignorando a barreira
perf_allow_kernel() e divulgando frames de pilha do kernel através do ring buffer
do perf. Este repositório contém a análise e um PoC verificado; o bug
proporciona um vazamento de informações, não uma primitiva direta de escalada de privilégios.
| Item | Valor |
|---|---|
| CVE | CVE-2026-98937 |
| Classe | Bypass de permissão / divulgação de informações (CWE-284/CWE-269) |
| Alvo | Android arm64 (verificado em 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) |
| Corrigido | Não; corrigido upstream em 6.12.113 / 6.18.56 / 7.2.10 / 7.3+ |
| Status | PoC verificado; oráculo de KASLR slide validado em 6.12.38 e 5.15.178 |
| Pré-verificação | src/vuln_precheck.h |
Kernels anteriores à correção abaixo são afetados. Corrigido por
a4b016d0a29038a5b7c96bf62c054bf7832451d9
("perf/core: Check kernel access when kernel callchains are requested",
Dapeng Mi, mesclado em 2026-06-30) e os backports estáveis correspondentes.
perf_event_open() costumava restringir a criação de perfis do kernel apenas com base em
!attr.exclude_kernel:
if (!attr.exclude_kernel) {
err = perf_allow_kernel(&attr);
if (err)
return err;
}
perf_allow_kernel() contém tanto a verificação perf_event_paranoid > 1 quanto o
hook SELinux perf_event { kernel }. Uma tarefa pode definir exclude_kernel=1 enquanto
ainda solicita callchains do kernel (PERF_SAMPLE_CALLCHAIN com
exclude_callchain_kernel=0); perf_callchain() então coleta a parte do kernel
independentemente do filtro de amostragem. Eventos de PMU de hardware que contam apenas o modo
usuário podem ter sua interrupção de overflow no contexto do kernel (skid), então
perf_callchain_kernel() desenrola a pilha do kernel e grava endereços de texto do kernel
no ring buffer.
A correção chama perf_allow_kernel() quando amostragem do kernel ou callchains
do kernel são solicitadas:
if (!attr.exclude_kernel ||
((attr.sample_type & PERF_SAMPLE_CALLCHAIN) &&
!attr.exclude_callchain_kernel)) {
err = perf_allow_kernel();
if (err)
return err;
}
O trecho acima é a forma da árvore estável 6.12.113, na qual
perf_allow_kernel() não recebe mais um ponteiro de atributo; o commit
upstream usa a mesma condição com perf_allow_kernel(&attr).
Consulte docs/analysis.md para o caminho de chamada completo, a mecânica
do skid, as evidências de auditoria do SELinux e as medições do dispositivo.
O PoC executa três casos como um processo de shell sem privilégios:
| Caso | atributos de perf_event | Esperado em um kernel vulnerável |
|---|---|---|
| A | exclude_kernel=0 | EACCES (gate de kernel-perf ativo) |
| B | exclude_kernel=1, exclude_callchain_kernel=1 | evento permitido, sem frames de callchain do kernel |
| C | exclude_kernel=1, exclude_callchain_kernel=0 | evento permitido, frames de callchain do kernel divulgados |
No YLP-W00 verificado (kernel 6.12.38, SELinux Enforcing):
EACCES, com
avc: denied { kernel } ... tclass=perf_event no log de auditoria.0xffffffde....No Redmi K70 verificado (kernel 5.15.178, ROM Android 17 de terceiros, SELinux Enforcing), o mesmo padrão foi observado:
EACCES, mesma linha de auditoria SELinux { kernel }.0xffffffdd....Nos dispositivos verificados, um kernel corrigido rejeitaria o caso C em
perf_event_open() com EACCES porque a solicitação de callchain do kernel agora
chega a perf_allow_kernel().
Com um vmlinux correspondente, os frames de callchain vazados tornam-se um
oráculo de slide KASLR por boot. O slide virtual arm64 é alinhado a 2 MiB, e todo endereço de texto vazado
deve satisfazer _stext <= addr - slide < _etext; a interseção desses
intervalos normalmente deixa exatamente um 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
No boot verificado YLP-W00 6.12.38, três frames fornecem o único
slide=0x1DCFC00000 (runtime _text=0xffffffde4fc00000):
| Endereço vazado | Endereço em tempo de link | Símbolo |
|---|---|---|
0xffffffde50748c78 | 0xffffffc080b48c78 | local_daif_restore+0x4 (msr DAIF, x0; ret) |
0xffffffde50d406e0 | 0xffffffc0811406e0 | el0t_64_sync_handler+0x70 (após bl el0_svc) |
0xffffffde4fc11588 | 0xffffffc080011588 | el0t_64_sync+0x1a8 (após bl el0t_64_sync_handler) |
Estes são frames de texto do kernel principal, não módulos: a interrupção PMU
pendente é entregue exatamente no ret após msr DAIF, x0 no caminho de
entrada da syscall. Endereços brutos não podem ser simbolizados diretamente
sob KASLR; subtraia o slide primeiro. Se um frame for um endereço de módulo,
nenhum slide coloca todos os endereços em [_stext, _etext) e o script
reporta isso em vez de adivinhar. O slide muda a cada boot e não deve ser
armazenado em cache entre reinicializações.
O Redmi K70 (5.15.178) também foi validado. Sua imagem correspondente foi
extraída de /sdcard/boot.img (boot v4; a string de versão embutida corresponde
ao kernel em execução) e o vmlinux-to-elf recuperou 158.668 símbolos. Sete
frames únicos de uma execução com período 1000 fornecem slide=0x1D93800000
(runtime _text=0xffffffdd9b800000), mapeando para syscall_trace_enter,
el0_svc_common, do_el0_svc, el0_svc, el0t_64_sync_handler,
el0t_64_sync e syscall_trace_exit.
A base em tempo de link é específica do branch — não reutilize o valor de 6.12 no 5.15:
| Branch do kernel | KIMAGE_VADDR |
|---|---|
| Android 13-5.15 (K70) | 0xffffffc008000000 |
| Android 16-6.12 (YLP-W00) | 0xffffffc080000000 |
| upstream v5.15 (layout diferente) | 0xffffffc010000000 |
O vmlinux-to-elf detectou automaticamente 0xffffffc008000000 para a imagem
do K70, correspondendo ao AOSP android13-5.15. Se a base ELF detectada
automaticamente diferir do KIMAGE_VADDR do branch, passe --link-base (ou
SLIDE_LINK_BASE=): a simbolização permanece no espaço ELF, o slide reportado
é tornado relativo à base verdadeira, e o _text em runtime é invariante a um
deslocamento constante da base. Alimentar o vazamento do K70 na imagem 6.12.38
continua sendo um teste negativo deliberado e não produz nenhum candidato.
Requisitos:
/opt/android-ndk-r29 (substitua com 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
Os artefatos são gravados em build/ e são ignorados pelo git.
make check # push + read-only environment pre-check
make run # push + full three-case verification