soralis0912/CVE-2026-43499-pmg110-root

GitHub: soralis0912/CVE-2026-43499-pmg110-root

针对 OPPO PMG110(MediaTek MT6991 / ColorOS 16)的本地提权 exploit,利用 CVE-2026-43499 futex PI 内核漏洞在设备上获取 root 权限并部署 su 守护进程。

Stars: 0 | Forks: 0

# pmg110-root CVE-2026-43499 (futex PI `rt_mutex_waiter` use-after-free) 本地提权, 移植到 **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 ``` 该 exploit 和 `su` 安装均已在此设备上验证通过。通过该已获取 root 权限的 shell 读取回的信息,也独立于 exploit 确认了 `P0_KERNEL_PHYS_LOAD`、符号偏移量 以及 `KS_MTE_TAGGED=0` — 详见 [`targets/pmg110-16.0.9.400/NOTES.md`](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 设置为 permissive)和 Write 2(将 `cred` 替换为 `init_cred`),使 子进程获取 uid=0,并由此安装内置的 `su` 守护进程 (daemon)。 - **依然只推入一个文件。** `su` 并非第二个独立产物:`su_daemon.c` 会 被构建为独立的 aarch64 PIE,并通过 `.incbin` 嵌入到库的 `.rodata` 中,因此它会随 `preload.so` 一起加载,并在运行 时写回。沿用 warhol-root 的路径,未作更改。 - 没有 root 脚本,没有 `ksud`,也没有 KernelSU - 调用进程保持非特权状态 — 它通过*请求守护进程*来获取 root, 这与您之后在 shell 中所做的操作相同 - SELinux 被**保留为 permissive**,这与 warhol-root 的做法一致:守护进程必须 通过其 socket 为非特权客户端提供服务。重启设备以恢复为 enforcing 模式。 `su` 被安装在三个位置,因为其中必有一个是您实际能够访问到的: | 路径 | 原因 | |---|---| | `/apex/com.android.virt/bin/su` | 位于挂载在该目录上的 tmpfs 中;在 root shell 的 `PATH` 中 | | `/data/local/tmp/su` | 无需修改 `PATH` 即可从普通的 `adb shell` 访问 | | *adbd 挂载命名空间中的* `/apex/com.android.virt/bin/su` | 通过 `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 的关系 除了 exploit 核心之外,所有内容均来自 warhol-root,是直接采用而非重新发明的: - **布局** — 位于 `targets//` 下的设备专用头文件,在 构建时暂存到 `source/src/` 中,因此切换 `DEVICE` 不会残留 上一个设备的头文件 - **构建过程** — `source/Makefile` 的工具链选择(如果存在 NDK 则使用 NDK, 否则使用针对 NDK sysroot 的主机 clang)以及生成 `build/embed/su_daemon_aarch64_pie` 后再链接 `.so` 的两阶段嵌入 规则 - **su 路径** — `su_daemon.c` 和 `su_blob.S` 与 warhol-root 中的在字节级完全一致,且 `su_install.c` 即为其 `preload.c` 的安装程序 **exploit 核心并非来自 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_offsets.h` 暂存提供 | exploit 本身的每一行代码 — Write 1、Write 2、KernelSnitch、pselect 路径 — 在两棵代码树中是完全相同的代码。 ### su 安装的调用位置 这是唯一的结构性差异,并且是由两棵代码树获取 root 权限的 不同形式所决定的。 warhol-root 直接赋予 **exploit 进程本身** root 权限,因此直接 从 `run_direct_root()` 调用 `install_embedded_su()`。在这里,Write 2 替换了 **派生子进程** 的 `cred` 指针,而父进程仍然是那个非特权的 调用者,因此 `child_main()` 中的子进程是唯一能够执行 安装操作的环境 — 这就是它的运行位置。 两棵代码树都在 `util.c` 中包含了一个相同的弱符号 `install_embedded_su()` stub,它 返回 `ENOSYS`;提供强符号定义才是启用该路径的关键。 了解这一点很有价值,因为如果某个构建过程不小心遗漏了 `su_install.c`,它仍然会 链接并运行 — 只是会报告 `su=0/38` 且什么也不会 安装。 ## 构建说明 ``` make # = make preload -> out/preload-.so make DEVICE= # use targets// 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 安装位置,如果都 找不到,则使用针对 NDK sysroot 的主机 `clang`。仅在需要 覆盖搜索结果时设置 `ANDROID_NDK_HOME`。`make info` 会打印出它选择的工具链。 构建过程分为两个阶段,这是值得了解的关键部分: 1. `su_daemon.c` → `build/embed/su_daemon_aarch64_pie`,一个独立的 aarch64 PIE 2. `su_blob.S` 使用 `.incbin` 将该二进制文件嵌入到 `.rodata` 中,然后将整个内容链接 为一个单独的 `preload.so` 因此,`make clean` 并重新构建是更改嵌入式 `su` 的唯一方法 — 仅编辑 `su_daemon.c` 就足够了,依赖关系已经声明,但由于该 blob 是 构建产物,因此不受版本控制跟踪。 `targets//{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=` | 覆盖冲突阈值乘数 | | `GHOSTLOCK_MTE=1` | 同时扫描内核指针标签(速度慢 15 倍) | | `GHOSTLOCK_PHYS_LOAD=0x...` | 覆盖内核物理加载地址 | | `PSELECT_SHIFT=` | 覆盖栈覆盖偏移量(**替换**,而非叠加) | 在已验证的运行中均未用到这些变量。 ## 阅读日志 ``` [*] 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` 表示 exploit 成功;其后的所有内容都是安装过程。 两者被刻意分开报告,判定结论也是如此: | 判定 | 含义 | |---|---| | `EXPLOIT OK` | 已获取 root 权限,且 `su` 可响应 | | `EXPLOIT OK, SU INSTALL FAILED` | Write 1 和 Write 2 已成功写入;仅安装过程出错 | | `EXPLOIT FAILED` | 写入未成功 | | `ABORTED` | 运行在能够报告结果前即终止 — 阅读最后一行 `[!]` | 中间的那一项是需要掌握的重要区别:它表明 `target.h` 中的偏移量 对于此构建是正确的,问题出在安装过程的 某个地方,这完全是两码事,需要不同的调试方法。其中出现的 `su=0/38` (`ENOSYS`) 具体意味着链接到的是那个弱符号 stub。 **`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` | 构造函数:运行 exploit,报告结果,停止 | | `source/src/main.c` | exploit 本身(Write 1 / Write 2) | | `source/src/su_daemon.c` | `su` 二进制文件 — 独立构建为 aarch64 PIE,未链接到 `.so` 中 | | `source/src/su_blob.S` | 使用 `.incbin` 将该 PIE 嵌入 `.so` 的 `.rodata` 中 | | `source/src/su_install.c` | 将 blob 写回,启动守护进程,并进行探测 | | `source/src/target.h` | 暂存目标(已被 gitignore 忽略) | | `targets//target.h` | 编译时布局:结构体偏移量、physmap 常量、slab 和 futex 结构 | `targets//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 下启动内核:测量栈覆盖情况,检查 linear-map 稳定性 | | `tools/device_probe.sh` | 从非特权 adb shell 进行的预检 | ## 许可证 仅用于授权的安全研究和教育目的。
标签:Android, DSL, LD_PRELOAD, Mediatek, Web报告查看器, 内核漏洞, 客户端加密, 本地提权, 逆向工具