
Verified PoC and analysis for a Linux perf/core permission-check bypass that leaks kernel callchain addresses to unprivileged Android tasks, enabling KASLR slide derivation.
English | 简体中文
Linux perf/core 权限检查绕过:非特权进程可以在设置 exclude_kernel=1 的
同时请求内核调用栈,跳过 perf_allow_kernel() 门禁,并通过 perf ring
buffer 读取内核栈帧。本仓库包含分析与已验证的 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) |
| 补丁 | 缺失;上游修复于 6.12.113 / 6.18.56 / 7.2.10 / 7.3+ |
| 状态 | PoC 已验证;6.12.38 与 5.15.178 上验证 KASLR slide oracle |
| pre-check | 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 决定是否调用
perf_allow_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() 随即展开内核栈,
把内核文本地址写入 ring buffer。
修复后,只要请求内核采样或内核调用栈,都必须经过
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() 已改为无参);
上游提交使用相同条件,调用形式为 perf_allow_kernel(&attr)。
完整的调用链、skid 机制、SELinux 审计证据和设备测量见
docs/analysis.md。
POC 以非特权 shell 进程运行三组用例:
| 用例 | perf_event 属性 | 漏洞内核上的预期 |
|---|---|---|
| A | exclude_kernel=0 | EACCES(内核剖析门禁生效) |
| B | exclude_kernel=1, exclude_callchain_kernel=1 | 允许打开,内核调用栈帧为 0 |
| 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 ROM, SELinux Enforcing)上观察到同样的模式:
EACCES,同样的 SELinux { kernel } 审计行。0xffffffdd...
内核区间。在已验证设备上,修复后的内核会在 perf_event_open() 阶段对用例 C 返回
EACCES,因为内核调用栈请求现在会进入 perf_allow_kernel()。
持有匹配 vmlinux 时,泄漏的 callchain 帧可以变成每次启动的 KASLR slide
oracle。arm64 虚拟 slide 按 2 MiB 对齐,且每个泄漏的文本地址必须满足
_stext <= addr - slide < _etext;对这些区间求交通常只剩一个 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
在已验证的 YLP-W00 6.12.38 启动上,三个帧给出唯一
slide=0x1DCFC00000(运行期 _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 之后) |
这三帧是核心内核文本,不是模块:pending PMU 中断正好在 syscall 入口
local_daif_restore 执行 msr DAIF, x0 后的 ret 处投递。KASLR 下裸地址
不能直接符号化,必须先减 slide。若某帧是模块地址,没有任何 slide 能让所有
地址落在 [_stext, _etext),脚本会报错而不是猜一个值。slide 每次启动变化,
不能跨重启缓存。
Redmi K70(5.15.178)也已验证。匹配镜像来自 /sdcard/boot.img
(boot v4;镜像内版本字符串与运行内核一致),vmlinux-to-elf 恢复出
158668 个符号。其中一次周期 1000 运行的 7 个唯一帧给出
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 |
| torvalds v5.15(布局不同) | 0xffffffc010000000 |
vmlinux-to-elf 对 K70 镜像自动检测出的基址 0xffffffc008000000 与 AOSP
android13-5.15 一致。若自动基址与分支的 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 # 其他 ABI / API
make host # qemu-aarch64 静态构建
make emu # host 静态构建并运行
make help
产物写入 build/,已被 git 忽略。
make check # push 并执行只读环境 pre-check
make run # push 并执行完整三组验证
# 直接运行
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
参数:
--check 只做 pre-check,不打开 perf 事件
--force 在未列出的内核 release 上强制运行
--duration N 每例采样秒数(默认 2)
--period N 采样周期(cycle,默认 1000)
退出码:
| 退出码 | 含义 |
|---|---|
| 0 | 漏洞存在:门禁被绕过且观测到内核调用栈帧 |
| 2 | pre-check 拒绝(未知内核 release 或环境门禁失败) |
| 3 | 不确定:事件已打开但未采到内核帧 |
| 4 | open 阶段被拒(已修复或策略强制) |
| 5 | 本机本来就允许内核剖析,无权限边界可绕过 |
| 6 | 环境错误(无可用硬件 PMU 事件、异常 errno) |
使用 --check 时,退出码 0 表示 profile 门禁允许继续,2 表示拒绝;不会打开
任何 perf 事件。
POC 只读取硬件采样数据,不写内核状态、不修改 sysctl、不重启设备。
cycles/instructions)。exclude_kernel=1
的软件事件会在内核态被过滤,无法走 skid 路径。shell 域(uid 2000)。untrusted_app 是否拥有
perf_event { open } 取决于厂商策略;本次未构建 APK 验证。kptr_restrict=2,无法在设备上符号化;泄漏地址
通过高半区范围和内核态标志交叉验证。perf_event_paranoid;两台设备实际生效的门禁都是 SELinux。
被跳过的函数同时包含 paranoid 检查,因此 perf_event_paranoid >= 2 时
同样可绕过。python3、nm、该 release 的精确匹配 vmlinux,以及该
分支真实的 KIMAGE_VADDR;slide 每次启动变化。6.12.38 与 5.15.178 均
已验证。6.12.58 已按版本加入 pre-check,设备端运行时验证待升级后进行。.
├── Makefile # 构建入口(NDK 交叉编译)
├── README.md
├── README.zh-CN.md # 中文版
├── LICENSE
├── src/
│ ├── poc.c # 三组差分验证 POC
│ └── vuln_precheck.h # 内核 release / 环境门禁
├── scripts/
│ └── slide-from-leak.py # 从泄漏文本地址推导 KASLR slide
├── app/ # 可选 Android App(本仓库未使用)
├── docs/
│ └── analysis.md # 根因、补丁、测量记录
├── logs/ # 运行时日志(git 忽略)
├── dump/ # 拉取的 config/policy/二进制(git 忽略)
└── build/ # 构建产物(git 忽略)
| 项目 | 已验证设备状态 |
|---|---|
| KASLR | 开启(CONFIG_RANDOMIZE_BASE=y,CONFIG_ARM64_VA_BITS=39);泄漏可作为每次启动的 slide oracle |
| 帧指针 | 开启(CONFIG_FRAME_POINTER=y);内核 callchain 展开使用帧指针 |
| SELinux | Enforcing;shell 域 perf_event { kernel } 被拒 |
perf_event_paranoid | -1(厂商设置);实际门禁是 SELinux |
kptr_restrict | 2;无法在设备上符号化 |
仅用于授权的安全研究和教育。不要对不拥有或未获明确许可的设备使用。
MIT,见 LICENSE。