
Проверенный PoC и анализ обхода проверки прав в подсистеме perf/core ядра Linux, который приводит к утечке адресов цепочки вызовов ядра непривилегированным задачам Android, что позволяет вычислить смещение KASLR.
English | 简体中文
Обход проверки прав доступа в Linux perf/core: непривилегированная задача может запрашивать
цепочки вызовов ядра, устанавливая exclude_kernel=1, тем самым обходя
проверку perf_allow_kernel() и раскрывая кадры стека ядра через кольцевой буфер
perf. Этот репозиторий содержит анализ и проверенный PoC; ошибка
приводит к утечке информации, а не к примитиву прямой эскалации привилегий.
| Пункт | Значение |
|---|---|
| CVE | CVE-2026-98937 |
| Класс | Обход проверки прав доступа / раскрытие информации (CWE-284/CWE-269) |
| Цель | Android arm64 (проверено на HONOR YLP-W00 и Redmi K70) |
| Ядро | 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) |
| Исправлено | Нет; исправлено в upstream в 6.12.113 / 6.18.56 / 7.2.10 / 7.3+ |
| Статус | PoC проверен; оракул KASLR slide подтверждён на 6.12.38 и 5.15.178 |
| Предварительная проверка | src/vuln_precheck.h |
Уязвимы ядра до указанного ниже исправления. Исправлено коммитом
a4b016d0a29038a5b7c96bf62c054bf7832451d9
("perf/core: Check kernel access when kernel callchains are requested",
Dapeng Mi, объединён 2026-06-30) и соответствующими стабильными бэкпортами.
perf_event_open() ранее проверял профилирование ядра только по
!attr.exclude_kernel:
if (!attr.exclude_kernel) {
err = perf_allow_kernel(&attr);
if (err)
return err;
}
perf_allow_kernel() содержит как проверку perf_event_paranoid > 1, так и
хук SELinux perf_event { kernel }. Задача может установить exclude_kernel=1,
при этом всё ещё запрашивая цепочки вызовов ядра (PERF_SAMPLE_CALLCHAIN с
exclude_callchain_kernel=0); затем perf_callchain() собирает часть ядра
независимо от фильтра сэмплирования. События аппаратного PMU, подсчитывающие только
пользовательский режим, могут получить своё прерывание переполнения в контексте ядра (skid), поэтому
perf_callchain_kernel() разворачивает стек ядра и записывает адреса текста ядра
в кольцевой буфер.
Исправление вызывает perf_allow_kernel(), когда запрашивается либо сэмплирование ядра,
либо цепочки вызовов ядра:
if (!attr.exclude_kernel ||
((attr.sample_type & PERF_SAMPLE_CALLCHAIN) &&
!attr.exclude_callchain_kernel)) {
err = perf_allow_kernel();
if (err)
return err;
}
Приведённый выше фрагмент — это форма из стабильного дерева 6.12.113, в которой
perf_allow_kernel() больше не принимает указатель на атрибут; в upstream-коммите
используется то же условие с perf_allow_kernel(&attr).
Полный путь вызова, механика утечки (skid), данные аудита SELinux и измерения
на устройстве см. в docs/analysis.md.
PoC выполняет три случая как непривилегированный shell-процесс:
| Случай | Атрибуты perf_event | Ожидаемое поведение на уязвимом ядре |
|---|---|---|
| A | exclude_kernel=0 | EACCES (активен шлюз kernel-perf) |
| B | exclude_kernel=1, exclude_callchain_kernel=1 | событие разрешено, нет кадров цепочки вызовов ядра |
| C | exclude_kernel=1, exclude_callchain_kernel=0 | событие разрешено, кадры цепочки вызовов ядра раскрыты |
На проверенном YLP-W00 (ядро 6.12.38, SELinux Enforcing):
EACCES, с
avc: denied { kernel } ... tclass=perf_event в журнале аудита.0xffffffde....На проверенном Redmi K70 (ядро 5.15.178, сторонняя прошивка Android 17, SELinux Enforcing) наблюдалась та же картина:
EACCES, та же строка аудита SELinux { kernel }.0xffffffdd....На проверенных устройствах исправленное ядро отклонило бы случай C на
perf_event_open() с EACCES, поскольку запрос цепочки вызовов ядра теперь
доходит до perf_allow_kernel().
При наличии соответствующего vmlinux утёкшие кадры цепочки вызовов становятся
оракулом смещения KASLR для каждой загрузки. Виртуальное смещение на arm64
выровнено по 2 МиБ, и каждый утёкший адрес кода должен удовлетворять
_stext <= addr - slide < _etext; пересечение этих интервалов обычно оставляет
ровно одно смещение.
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
На проверенной загрузке YLP-W00 6.12.38 три кадра дают уникальное значение
slide=0x1DCFC00000 (runtime _text=0xffffffde4fc00000):
| Утёкший адрес | Адрес на этапе компоновки | Символ |
|---|---|---|
0xffffffde50748c78 | 0xffffffc080b48c78 | local_daif_restore+0x4 (msr DAIF, x0; ret) |
0xffffffde50d406e0 | 0xffffffc0811406e0 | el0t_64_sync_handler+0x70 (после bl el0_svc) |
0xffffffde4fc11588 | 0xffffffc080011588 | el0t_64_sync+0x1a8 (после bl el0t_64_sync_handler) |
Это кадры текста ядра, а не модулей: ожидающее прерывание PMU
доставляется ровно на ret после msr DAIF, x0 на пути входа в системный
вызов. Сырые адреса нельзя символизировать напрямую при KASLR;
сначала вычтите slide. Если кадр является адресом модуля, ни один slide не
помещает все адреса в [_stext, _etext), и скрипт сообщает об этом вместо
угадывания. Slide меняется при каждой загрузке, и его нельзя кэшировать между
перезагрузками.
Redmi K70 (5.15.178) также был проверен. Его соответствующий образ был
извлечён из /sdcard/boot.img (boot v4; встроенная строка версии совпадает
с работающим ядром), и vmlinux-to-elf восстановил 158 668 символов. Семь уникальных
кадров из одного прогона с периодом 1000 дают slide=0x1D93800000 (runtime
_text=0xffffffdd9b800000), что соответствует syscall_trace_enter,
el0_svc_common, do_el0_svc, el0_svc, el0t_64_sync_handler,
el0t_64_sync и syscall_trace_exit.
База на этапе компоновки зависит от ветки — не используйте значение 6.12 на 5.15:
| Ветка ядра | KIMAGE_VADDR |
|---|---|
| Android 13-5.15 (K70) | 0xffffffc008000000 |
| Android 16-6.12 (YLP-W00) | 0xffffffc080000000 |
| upstream v5.15 (другая раскладка) | 0xffffffc010000000 |
vmlinux-to-elf автоматически определил 0xffffffc008000000 для образа K70, что совпадает
с AOSP android13-5.15. Если автоматически определённая база ELF отличается от
KIMAGE_VADDR ветки, передайте --link-base (или SLIDE_LINK_BASE=): символизация остаётся
в пространстве ELF, сообщаемый slide вычисляется относительно истинной базы, а
runtime _text инвариантен к сдвигу базы на константу. Подача утечки K70
в образ 6.12.38 остаётся намеренным негативным тестом и не даёт кандидатов.
Требования:
/opt/android-ndk-r29 (переопределяется через 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
Артефакты записываются в build/ и игнорируются git.
make check # push + read-only environment pre-check
make run # push + full three-case verification
# directly
adb push build/poc /data/local/tmp/poc
adb shell chmod 755 /data/local/tmp/poc
adb shell /data/local/tmp/poc --check
adb shell /data/local/tmp/poc --duration 2 --period 1000
Параметры: