Android GKI 6.12 内核漏洞利用程序,针对 CVE-2026-43499,将 rt_mutex 回滚漏洞与 pselect 栈覆盖串联,从而在三星和 Pixel 设备上获取 root 权限。
针对 Android GKI 6.12 系列(三星和 Pixel 设备)的 Linux 内核漏洞利用,目标是 CVE-2026-43499:rt_mutex_start_proxy_lock() 中 remove_waiter() 的回滚存在缺陷,它使用了 current 而不是 waiter::task,导致等待者的 pi_blocked_on 指向其(稍后被弹出的)内核栈上的 rt_mutex_waiter。结合 pselect() 的 fd_set 栈覆写以及由消费者 sched_setattr 驱动的伪造 rb_tree 遍历,可产生确定性的内核写入原语并获取完整 root 权限。
该漏洞利用以 LD_PRELOAD 共享库(preload.so)的形式运行,并在获取 root 后安装 root su 守护进程以及壁纸作为后置产物。
状态:活跃开发中。m1q(ZF1)目标是当前的重点攻关对象。 pipei tmp_page-uname 引导是当前活跃的路线:该遍历已在设备上验证干净(build #33:每个子进程独立播种的 rt_mutex 区域),完整的 168 候选扫描正在进行中。configfs CFI 路线在 m1q 上是死路(Rust ashmem —— 没有可注入的 fops 槽位),但仍保留为 C-ashmem 目标的路线。各目标的偏移量因设备而异 —— 在信任它们之前,请对照实际内核二进制进行验证。
当 FUTEX_CMP_REQUEUE_PI 重新排队一个等待者,且重新排队的死锁检测链遍历返回 -EDEADLK 时,__rt_mutex_start_proxy_lock() 会通过 remove_waiter()(rtmutex.c:1535)进行回滚。回滚正确地将等待者从等待树中出队,但清除的是重新排队调用者的 pi_blocked_on,而不是 waiter->task->pi_blocked_on。等待者的 pi_blocked_on 仍然悬空指向其栈上的 rt_mutex_waiter,该结构在 futex 超时后被弹出。
超时后,等待者的内核栈区域被复用:core_sys_select() 将三个 fd_set 复制到该栈缓冲区中(ZF1 上的 nfds < 344 栈路径)。精心构造的 fd_set 字数组会重新物化一个伪造的 rt_mutex_waiter / 伪造任务 / 伪造 rt_mutex(其 rb_tree 根指针受控)。随后一个消费者线程调用 sched_setattr_tid(waiter) → rt_mutex_adjust_pi() → rb_erase_cached,从而产生对受控值的任意地址写入。
整个原语需要 PI 链环路:持有者持有 f_pi_target,同时又阻塞在 f_pi_chain(由等待者持有)上,因此链遍历会命中 owner → chain → waiter → target → owner 并以 -EDEADLK 失败。移除该环路会使重新排队返回成功且对内核零影响。
sched_blocked_reason(m1q 主路线,设备上已验证):从环形缓冲区读取被阻塞 kworker 保存的返回 PC(stack_trace_save_tsk),并与编译内置的 worker_thread 偏移进行比较。在任何 boot_id 路线字被使用之前运行,以便数据别名写入目标(data_addr() = p0 别名 + slide_p0_offset)在非零 slide 下保持正确。SLIDE_LOGGERS_0_1 植入 boot_id sysctl 数据中;泄露的值可重建 stext。在 m1q 上已搁置:其 W1 目标(SLIDE_RANDOM_BOOT_ID_DATA_OFF,低物理 RAM)的可写性依赖于页 —— slide 每次启动都会将目标移动跨页边界,因此约 6/7 次运行会出错。仅通过 SLIDE_FORCE_BOOTID(机制测试)显式执行,并将 tree_pc/tree_left 重定向到喷射页。mm_struct 大小的对象,并利用 futex 哈希碰撞在堆上定位一个 mm_struct,泄露一个内核堆页地址,用作伪造对象喷射基址。清理时等待者被分离(而非 join),以避免在 #26 之前的构建中导致崩溃的内核栈 OOM。pselect fd_set 字在等待者的内核栈上重新物化一个伪造的 rt_mutex_waiter;消费者线程的 sched_setattr_tid(waiter) 通过 rt_mutex_adjust_pi() 遍历它。ZF1 字映射(反汇编验证):PSELECT_WAITER_WORD_SHIFT = 0,word 12 = task(@+0x50),word 13 = lock(@+0x58),word 14 = wake_state(@+0x60 = 3)。在播种了伪造 rt_mutex 的情况下,遍历干净地完成:[7] 的 rb_erase W1 *(tree_left)=tree_pc / W2 *(tree_pc&~3+8)=tree_left 是唯一的内核写入;[11](setprio / dequeue_pi)和 [9] 唤醒均被跳过(prio 100 的本地伪造等待者使栈节点不进入 top-waiter)。自 build #19 以来,每次进入遍历的完成都在设备上存活;此前的确定性崩溃是 8 字节载荷放置错误(SKB_DATA_DELTA),而非遍历体故障。
可选的 STAGE3=1 阶段,它 fork 一个桥子进程,将其 mm->pgd 交换为分阶段的伪造页表(3 级:PGD→L1→L2,RX 循环叶子 + RW 栈叶子),并让子进程运行一个仅使用寄存器的汇编 blob,通过 2MB 物理扫描窗口修补其自身的 cred —— 无需 configfs/pipe 原语即可实现全 RAM 物理读/写。目前仅接入 m1q 目标,且尚未在设备上运行。
src/targets/<codename>-<build>/ 中有 41 个目标,每个至少需要一个 target.h(内核符号偏移、KIMAGE_TEXT_BASE、直接映射基址、结构体偏移)。Pixel 目标(comet、tokay、tegu、caiman、komodo、frankel、mustang、rango、stallion、blazer)覆盖共享源;三星目标(m1q-*)添加设备特定逻辑。
make list-projects # full list
内核结构体布局因设备而异 —— 必须针对每个目标对照实际内核二进制 / kallsyms 验证偏移量。
输出:build/<PROJECT>/bin/preload.so(通过 LD_PRELOAD 加载的共享库)。
# Default project
CC=clang make
# Specific device target (default: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Without CC=clang: uses NDK if found ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), otherwise host clang + Android sysroot
make PROJECT=m1q-BP4A.251205.006
# Show build configuration
make info
# Clean
make clean
构建过程通过 src/su_blob.S / src/wallpaper_blob.S 将一个 PIE su_daemon 二进制(src/su_daemon.c,构建到 build/embed/su_daemon_aarch64_pie)和 assets/wallpaper.webp 嵌入到 preload.so 中。
将构建输出推送到 /data/local/tmp,然后在 LD_PRELOAD 下运行漏洞利用。在活跃攻关目标上运行时不要使用 tee —— tee 会缓冲自己的 stdio,并在内核 panic 时丢失日志尾部(漏洞利用的 stdout 是无缓冲的,因此直接重定向到文件可保留每一行):
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
成功后,守护进程监听 /data/local/tmp/temp_su.sock,且 su 被安装到 /apex/com.android.virt/bin 下。
tools/run_pipei.sh 推送二进制文件,在设备上对其进行 sha256 校验(过期的 preload.so 会产生静默不可读的判定 —— 该阶梯曾运行过 build < #22,其日志格式不同),然后以 3x56 分块运行完整的 168 候选扫描,这样一次 panic 最多丢失一个块(约 10 分钟)。漏洞利用的 stdout 直接流式传输通过 adb(无设备端重定向 —— 设备端 tee 在 panic 时会丢失尾部);每个块的主机端副本保存在 /tmp/m1q_pipei_chunk{1..3}_stream.log:
sh tools/run_pipei.sh
PASS = 任一块显示 tmp_page uname changed + after uname='CatOS。
PIPEI_CHILD_REGIONS 必须设置为至少块大小,否则每个子进程的 rt_mutex 区域将保持未播种,扫描会在候选 0 处 panic。完整覆盖还需要 TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES=1 —— 没有它,被释放的 8 页 mm 块中只有一页会被扫描。run_pipei_slim.sh 是单候选诊断变体;run_m1q_ladder.sh 是遍历/隔离测试组(B16-2 oracle、slide 机制、扫描步骤)。
exploit/
Makefile
assets/ # wallpaper.webp (embedded post-root artifact)
src/
main.c # run_exploit() entry point (shared)
preload.c # LD_PRELOAD constructor, su/wallpaper install
util.c # addr translation, page prep, configfs helpers
pipe.c # pipe-based physical read/write
root.c # cred patching / task walk
slide.c # KASLR slide (tracefs + boot_id routes)
fops.c # pselect corruption + fops manipulation
faketables.c # stage-3 fake page-table builders (3-level)
stage3.c # pgd-swap bridge child (no-op unless configured)
stage3_loop.S, stage3_poll.S # bridge blob + enter trampoline
su_daemon.c # root daemon (compiled as separate PIE)
su_blob.S, wallpaper_blob.S # embedded binaries
offset.h # pulls per-target target.h via -DTARGET_CONFIG_H
kernelsnitch/ # mm_struct futex-hash leak library
targets/<codename>-<build>/ # per-device target.h + optional overrides
poc/poc.c # standalone POC (separate from the exploit)
目标通过 Makefile 的 pick_src 机制覆盖共享源:如果 src/targets/<PROJECT>/<file>.c 存在,则使用它而不是共享的 src/<file>.c;否则使用共享版本。
-EDEADLK(errno 35)失败。缺少环路会表现为 success=1 且对内核零影响。PSELECT_WAITER_WORD_SHIFT 必须为 0(ZF1 栈深度从 ELF 验证)。其他目标仍默认为 1 —— 在信任它们之前请逐设备验证。p0_data_alias() 使用 DIRECT_MAP_BASE(线性映射),在 ZF1 上与 P0_PAGE_OFFSET 不同。被解引用的内核对象的数据别名必须使用线性映射。pr_error() 是致命的(exit(-1)),与警告/信息宏不同。SLIDE_LOGGERS_0_1,而非 SLIDE_NFULNL_LOGGER;该字是非随机化的线性映射别名,因此 boot_id 路线与 slide 无关(仅在 slide=0 时使用它 —— 先运行 tracefs 泄露)。SKB_DATA_DELTA 为 -0xe78(设备上实测,build #19)—— 真实的 skb frag 头比反汇编推导的 -0xe80 少 8 字节;该 8 字节错误导致了 build #6-18 中每次确定性 pselect 崩溃(进入遍历 → 垃圾解引用)。GKI 6.12 共享值;每个目标的 util.c 副本带有 环境 A/B 旋钮。不要将 叠加在已烘焙的常量之上。这是针对 Android 设备的漏洞利用代码,旨在用于安全研究、授权漏洞测试以及您拥有或获得明确测试许可的硬件上的防御工作。在您不拥有的设备上滥用是违法的。作者不对变砖的设备、变砖的内核或失效的保修负责。
write_iterASHMEM_SET_NAMEconfigfs_bin_write_iterpipe_inode_info*(tree_left) = tree_pctmp_pageinit_uts_nssysnameuname()preload.c 安装内嵌的 su 守护进程(tmpfs 挂载到 /apex/com.android.virt/bin,外加 adbd 命名空间和本地变体)并替换壁纸。| 变量 | 默认值 | 用途 |
|---|
PSELECT_ROUTE_SHIFT | 编译时 | 主路线(m1q)的 A/B fd_set 字偏移 |
SLIDE_SHIFT | 编译时 | slide 路线的 A/B fd_set 字偏移 |
PSELECT_ROUTE_DELAY_USEC | 50000 | sched_setattr 之前的消费者延迟(必须 > 0) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | slide 路线时序/绑核 |
SKIP_SLIDE | 0 | 使用直接映射回退(slide = 0) |
SLIDE_ONLY | 0 | 仅运行 KASLR slide 然后退出 |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | 覆盖 slide p0 别名 / boot_id 偏移 |
SLIDE_FORCE_BOOTID | 0 | 在 tracefs slide 之后强制使用 boot_id pselect 路线(机制测试) |
KSNITCH_COLLISIONS | 4 | KernelSnitch 碰撞次数 |
STAGE3 | 0 | 启用 pgd-swap 桥子进程阶段(m1q) |
STAGE3_DRYRUN | 0 | 分阶段/验证页表后在交换前中止 |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | 喷射/fd_set 布局调整 |
TMP_PAGE_UNAME | 0 | 运行 m1q pipei tmp_page-uname 引导 |
TMP_UNAME_PIPEI_SWEEP, TMP_UNAME_PIPEI_ALIGN_ORDER, TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES | — | 扫描模式;split_order 扫描被释放 mm 块的全部 8 页 |
TMP_UNAME_PIPEI_SLOT, TMP_UNAME_PIPEI_SLOT_CANDIDATES | — | 扫描范围(块起始 / 数量) |
PIPEI_CHILD_REGIONS | 0 | 每个子进程播种的 rt_mutex 区域;必须 >= 扫描块,否则未播种区域会在候选 0 处 panic |
PIPEI_RECLAIM_COUNT, PIPEI_DRAIN_COUNT | — | 回收/排空管道对;在回收未命中时提高 |
PIPEI_SPONGE_MB | 3072 | 在回收期间保持的 order-0 洪泛海绵(build #29) |
PSELECT_W0_PRIO_OVERRIDE | 130 | top-waiter 优先级;在播种区域下无效(遗留陈旧树防护,仅 A/B) |
PSELECT_SKB_DELTA_OVERRIDE | 0 | A/B 载荷页内偏移(仅当某设备与 -0xe78 不同时) |
TMP_UNAME_NAME, TMP_UNAME_HOLD_SEC | "CatOS", 5 | 成功后标记名称 / 保持时间 |
SKB_DATA_DELTAPSELECT_SKB_DELTA_OVERRIDEASHMEM_SET_NAME KVec 封装与 configfs_bin_write_iter 不匹配。使用 pipei tmp_page 引导。m1q target.h 中的 PSELECT_WRITE_RIGHT_SPRAYED / PSELECT_PI_TOP_TASK_SELF 防护将遍历的 [11] 写入重定向离开 RO 静态 fops。payload+0x6000 + idx*0x40)。共享 fake_lock 扫描在候选约 9-12 处 panic(陈旧内核栈节点被回收),而 build #32 的空区域在候选 0 处 panic([9] 在 0x41 填充的 fake_task 上 owner==NULL 唤醒)。必须设置 PIPEI_CHILD_REGIONS,否则区域保持未播种。