CVE-2026-43499(IonStack)本地权限提升漏洞,已移植到 小米 17T Pro(warhol) —— 联发科 MT6993,Android 16。
仅限 GKI 6.12 / android16。 waiter 覆盖区、
pselect栈布局以及所有结构体偏移量都固定在该分支上,因此这里的内容无法迁移到 6.6、6.1 或 5.10 —— 那些分支需要为它们专门构建的基础。generate_target.py会拒绝任何其他 banner,而不是生成一个会在设备上出错的头文件。
状态:可用。 于 2026-07-26 在设备上从干净启动验证通过,覆盖 两种 preloader 变体 ——
uid=0(root) context=u:r:kernel:s0,SELinux 宽容模式,su已安装。Root 并非持久:每次启动后需重新执行LD_PRELOAD那一行。pselect 竞态并非 100% 成功:一次失败的运行可能导致手机内核 panic,重启后重试是正常的。完整记录见WARHOL_PORT.md。
此设备需要内核 MTE 修复。 上游原版 popsicle 无法在此设备上获得 root —— 它会永远卡在
kernel page retry N/12。参见 内核 MTE。
构建分为两种 preloader 变体:PRELOADER=retail(默认)和 PRELOADER=eng。它们选择不同的目标目录,因此任何一方都不会覆盖另一方的地址 —— 参见 Preloader 变体。
以下事实来自零售版完整 OTA 包 —— 元数据、A/B payload 清单,以及从 payload.bin 中提取的 boot/vendor_boot/dtbo 镜像。
在这里,内核比 SoC 更重要:warhol 与上游 popsicle 位于同一条 GKI 分支(android16-5,4K 页),仅差一个补丁级别 —— 6.12.38 对比 popsicle 已验证的 6.12.23。结构体偏移量仍须针对每次构建重新生成;只有漏洞利用的整体形态得以沿用。不同的 OS3.0.x 构建需要自己的 target.h。
漏洞利用链是版本锁定于 GKI 分支的,而不是 SoC。rt_mutex waiter 布局、pselect 栈覆盖以及 pipe_buffer 结构体偏移量全都跟随内核,因此基于 6.12/android16 的方案胜过基于同厂商较旧分支的方案。
x-spy/CVE-2026-43499-popsicle 是唯一在 6.12/android16 GKI 系列(小米 17 / Pro / Ultra,内核 6.12.23-android16-5)上验证过的公开实现,因此 source/ 取自该仓库并尽可能保持与上游一致 —— 仅两行例外(参见 内核 MTE,此设备必须依赖它才能工作)。
MediaTek 的大部分差异集中在构建时的目标生成上,分为两部分:
xbl_config 分区读取 p0_phys_offset 和 p0_kernel_phys_load,而 warhol 没有该分区。generate_target.py --dtb 改从 vendor_boot FDT 的 /memory 节点读取 DRAM 基址,并采用与 arm64_memblock_init 相同的方式向下取整。内核物理加载地址通过 --preloader 从 preloader 的 mb_kernel 预留区读取;对于此设备,差值算出来为 0,而 lk 拒绝启动放在任何其他位置的内核。boot.img 内,而非裸露的 arm64 Image,因此生成器在分析前会先将其解压。WARHOL_PORT.md §2 包含完整推导过程。
MediaTek 的 lk 并非将内核加载到编译期常量地址 —— 它会查找名为 mb_kernel 的 DRAM 预留区,并断言内核恰好落在其上。该预留区由 preloader 创建,这正是手机当前运行的 preloader 构建被视为此处构建输入的原因:它是 boot.img 之外唯一能改变 P0_KERNEL_PHYS_LOAD 的东西,那里的值一旦出错,就意味着线性映射别名错误、手机变砖,而不是单纯的利用失败。
因此存在两种变体,分别存放于独立的目标目录中:
make preload # PRELOADER=retail -> out/preload-<device>.so
make PRELOADER=eng preload # -> out/preload-<device>-eng.so
make both # both of the above, and print their sha256
两个产物以不同的名称并排存放。在这套固件上,它们产出逐字节一致的结果 —— 这是下述分析的结果,而非绕开它的捷径,因此两者仍然分开构建并命名。
你可以自行读取两个 preloader 的内存布局表:
python3 tools/preloader_memlayout.py <preloader.bin> --diff <preloader_eng.bin>
在这套固件上,两张表完全相同,两者的 mb_kernel.start = 0x80000000,因此 eng 版的 target.h 与零售版逐字节一致,两个构建也会产出相同的 preload.so。
真正进入漏洞利用的是差值 P0_KERNEL_PHYS_LOAD − P0_PHYS_OFFSET,而它的两个项并非建立在同等强度的证据之上:
P0_KERNEL_PHYS_LOAD —— 从 eng 镜像中实测得出,即上面的 mb_kernel.start。P0_PHYS_OFFSET —— 是论证得出而非实测,因为它是 memstart_addr,内核在运行时从 lk 依据 preloader 在 DRAM 初始化后报告的内容所生成的 /memory 节点获取该值,而非从生成器读取的静态 FDT 获取。当时的论证是:两个构建共享整套 DRAM 路径 —— 相同的 dramc/emi/mblock/memory_layout 源码,eng 独有的字符串中没有内存布局相关代码 —— 并且 eng preloader 多出的那个预留区(一个带 mapping=1 的 3 MiB 动态 security_fe_rsv)既无法挤占固定地址条目,也无法在 DRAM 底部挖出空洞。设备实测了结此事:线性映射别名在 eng 下正确落地,因此该项是对的。完整推导见 targets/warhol-OS3.0.304.0.WPSJPXM-eng/NOTES.md。
来自设备实测的说明:
ro.boot.verifiedbootstate=green 和 flash.locked=1 均未改变。在亲自尝试之前,这曾是一个悬而未决的问题;它由 efuse 烧录的根密钥哈希决定,而且此构建与零售版使用相同的密钥签名。%s META DIS 是零售版独有的字符串,而 eng 带有零售版所没有的完整 META 实现(Enable fastmeta.、FAST META GPIO: %d、META_COM PORT: %d、read_meta_proinfo 等)。在这里它直接启动到了 Android,但如果刷了 eng 的手机将来落入其他状态,这很可能就是原因,且与任何 target.h 的值无关。mb_kernel,两个目标就会分道扬镳 —— 将它们作为独立目录分开存放,正是为了避免这种变化被悄然忽略。该内核运行 KASAN_HW_TAGS —— /proc/cmdline 带有 kasan.stack_ring_size=524288 —— 因此 slab 指针在第 59:56 位携带分配标签。上游 popsicle 假定指针不带标签,因此完全无法让此设备获得 root:它会无限循环于 [-] kernel page retry N/12 mode=1,日志中没有任何内容说明原因。
source/ 中的两行代码修复了这个问题,二者都在 warhol-mte-fix.patch 中:
只有检查操作会去除标签。指针本身保持带标签,因为其标签正是它们所指向内存的正确标签 —— 这正是内核解引用它们时所需要的。
不要为此去读 arm64.memtag.bootctl:该属性是用户空间的 MTE 控制项,与内核是否标记自身分配毫无关系。
本仓库是一个移植。漏洞利用链的功劳归于上游。
source/ exploit core (upstream popsicle + the kernel-MTE fix; see warhol-mte-fix.patch)
src/ main.c slide.c fops.c pipe.c util.c preload.c su_daemon.c + kernelsnitch/
targets/ per-build generated target.h, one dir per fingerprint x preloader variant
warhol-OS3.0.304.0.WPSJPXM/ PRELOADER=retail
warhol-OS3.0.304.0.WPSJPXM-eng/ PRELOADER=eng
tools/
payload_dump.py extract partitions from an A/B payload.bin (verifies size+sha256)
kernel_banner.py read the Linux banner out of a boot.img
bootinfo.py dump boot / vendor_boot header fields
generate_target.py target.h generator; upstream's, MediaTek-only, plus kernel decompression
preloader_memlayout.py dump/diff a MediaTek preloader's static DRAM reservation table
lz4legacy.py LZ4 legacy-frame decompressor (kernel images)
out/ build artifacts (gitignored)
在 target.h 存在之前不会构建任何东西 —— Makefile 会明确报错失败,而不是针对错误的内核产出二进制文件。
# 1. extract boot.img straight out of the OTA zip (payload.bin is stored uncompressed,
# so --base seeks into the zip; no need to unpack 7.6 GB first)
python3 tools/payload_dump.py <ota.zip> --base 5081 -p boot,vendor_boot,dtbo -o <dir>
# 2. confirm the kernel banner — every offset downstream is pinned to it
python3 tools/kernel_banner.py <dir>/boot.img
python3 tools/bootinfo.py <dir>/boot.img
# 3. generate the target header. --preloader reads the kernel's physical load address
# out of the preloader's mb_kernel reservation; pass the preloader the phone runs.
python3 tools/generate_target.py \
--boot <dir>/boot.img --dtb <dir>/vendor_boot.img --preloader <preloader.bin> \
-o targets/warhol-OS3.0.304.0.WPSJPXM/target.h
# 4. build
make preload # DEVICE=warhol-OS3.0.304.0.WPSJPXM, PRELOADER=retail by default
手头没有 preloader 镜像时,--kernel-phys-delta 0 是较旧的用法,对本固件会生成相同的头文件 —— 但它是一种断言而非实测读取,因此优先使用 --preloader。两者都传时,生成器会对它们进行交叉校验,不一致则报错。
--base 5081 是此特定 OTA 的 payload.bin 本地头偏移量;对于其他任何软件包,请从 META-INF/com/android/metadata 中的 ota-property-files 读取该值。
需要 Android NDK(NDK_ROOT / ANDROID_NDK_HOME)和 llvm-objdump。
# <device> is the target name: <fingerprint> for PRELOADER=retail, <fingerprint>-eng for eng
adb push out/preload-<device>.so /data/local/tmp/preload.so
adb shell chmod 0644 /data/local/tmp/preload.so
adb shell "LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true"
adb shell "/data/local/tmp/su -c id"
cred 与 SELinux 状态是在内存中修补的,因此每次启动后都必须重复此操作。无需解锁 bootloader。
仅供在您自有的硬件上进行研究。运行此程序可能导致设备反复重启(bootloop)或变砖,并使保修失效。不提供任何形式的担保。
上游项目由其各自作者授予许可;本移植沿用其条款。
| 项目 | 值 | 来源 |
|---|
| 代号 | warhol | pre-device=warhol |
| 构建指纹 | Xiaomi/warhol_global/warhol:16/BP2A.250605.031.A3/OS3.0.304.0.WPSJPXM:user/release-keys | post-build |
| 增量版本 | OS3.0.304.0.WPSJPXM(JP 全球版) | post-build-incremental |
| Android | 16,SDK 36 | post-sdk-level=36 |
| 安全补丁 | 2026-05-01 | post-security-patch-level |
| OTA 类型 | A/B(payload.bin,CrAU,39 个分区) | ota-type=AB |
| SoC | 联发科 MT6993(天玑 9500) | vendor_boot DTB 中的 mediatek,mt6993-* 兼容项;cmdline bootopt=64S3,32N2,64N2 |
| 内核 | 6.12.38-android16-5-g1d46253471dd-ab15048002-4k,clang 19.0.1 | 从提取的 boot.img 中读取的 banner |
| 启动镜像 | header v4,内核 18,898,125 字节,LZ4-legacy 压缩,无 ramdisk | tools/bootinfo.py |
PRELOADER= | 手机上的 preloader | 目标目录 | 状态 |
|---|
retail(默认) | 随设备出货的 preloader_<device>.bin,与 fastboot ROM 中的 preloader_raw.img 逐字节相同 | targets/warhol-OS3.0.304.0.WPSJPXM/ | ✅ 2026-07-26 已在设备上验证 |
eng | 工程构建版 preloader_<device>_eng.bin | targets/warhol-OS3.0.304.0.WPSJPXM-eng/ | ✅ 2026-07-26 已在设备上验证 |
| 文件 | 更改 | 原因 |
|---|
src/util.c | kernelsnitch_setup(..., mte_enabled=1) | 设为 0 时,mm_struct 搜索只会尝试无标签的候选,永远无法触及带标签的指针 —— 这是结构性失配,而非运气不佳 |
src/util.c | is_kernel_ptr() / is_direct_ptr() 在范围检查前先去除标签 | 否则,从内核内存读出的带标签指针会因“不在线性映射中”而被拒绝(direct-entry-fatal reason=bad-task-or-cpu) |
src/kernelsnitch/kernelsnitch.h | 标签扫描 < 15 → < 16 | 标签 0xf 就是无标签/匹配所有指针的标签,因此该扫描现在也覆盖没有 MTE 的内核 —— mte_enabled=1 两种情况下都安全 |
| 仓库 | 在本项目中的角色 |
|---|
| https://github.com/MobiusM/CVE-2026-43499 | 原始 CVE-2026-43499 PoC / 崩溃触发程序 |
| https://github.com/x-spy/CVE-2026-43499-popsicle | 本移植的基础。 小米 17 Pro Max(popsicle),内核 6.12.23-android16-5;source/ 和 tools/generate_target.py 来自此仓库 |
| https://github.com/Kananosa/CVE-2026-43499-For-Xiaomi-17T-chagall | 最接近的姊妹设备(小米 17T,chagall,同为 MediaTek)。源自 popsicle,附带预构建的 preload.so —— 其编译期常量确认了物理加载偏移 |
| https://github.com/MiCode/Xiaomi_Kernel_OpenSource | 小米内核源码,用于交叉核对结构体布局 |