本文记录 Nebula Security 公开的 IonStack 利用链中核心漏洞 CVE-2026-43499(GhostLock)的利用原理,以及在 OnePlus 13T(PLC110,kernel 6.6.89 GKI)上对其进行开发适配的完整过程。所有偏移数据均来自真机实测(root kallsyms + BTF + IDA),非照抄原 Pixel 10 target。
IonStack 是 Nebula Security 公布的一条 Android 完整利用链,从 Firefox 渲染器(CVE-2026-10702,SpiderMonkey JIT function.name 类型混淆)切入,衔接到 Android 内核提权(CVE-2026-43499,GhostLock),最终拿到 root 并植入 su daemon。
CVE-2026-43499 被 README 称为 "GhostLock —— 一个在所有 Linux 发行版中存在了 15 年的栈 UAF"。它位于内核 futex PI(优先级继承)requeue 路径,是核心 futex 代码,因此几乎所有 Linux 系统都受影响。
原利用仅针对 Pixel 10 系列(Android 17)提供了 39 个固件的 target 配置。本文的目标是把它适配到一台 OnePlus 13T(PLC110) 上:
适配的核心工作是:重新提取全部内核符号/结构体偏移,并验证各子技术在本机内核上的兼容性。
Linux futex 的 PI requeue 用于把一个等待在 futex_A 上的 waiter 原子地转移到 futex_B 的 PI 等待链上,典型场景是 pthread_cond_broadcast 底层。涉及两个操作:
由于 B 是 PI futex,waiter 会被挂入 B 的 rt_mutex PI 等待树,参与优先级继承。
PoC(IonStack/CVE-2026-43499/poc/poc.c)用三个 futex 字和三个线程构造一个 PI 死锁:
requeue 后,waiter 被挂入 f_pi_target 的 PI 等待树。但 owner 持有 f_pi_target 且在等 f_pi_chain,而 waiter 持有 f_pi_chain 且在等 f_pi_target —— 死锁。
waiter 的 WAIT_REQUEUE_PI 带了 5 秒超时。超时到期时,内核必须把 waiter 从 f_pi_target 的 PI 树上摘除并回滚 PI 链。这段超时清理路径引用了已经无效的内核栈内存:waiter 在等待期间其 task 栈可能被释放/复用,而清理代码仍按栈上保存的指针去解引用、去操作 PI 树,形成 内核栈 UAF。
关键性质:
PoC 的 stamp_* 系列函数演示了多种栈控制途径:
完整利用实际走的是 pselect6 路由(do_pselect_fake_lock_route):pselect 的 fd_set 参数直接在内核栈上,攻击者把伪造的指针铺进 fd_set,超时清理路径读到这些指针就完成了"栈 UAF 可控化"。
Pixel 10 内核开启了 CONFIG_CFI_CLANG,间接调用会校验目标函数签名,不能跳任意 gadget。作者用"伪造结构体里每个函数指针都指向同签名合法内核函数"绕过:
有了 configfs 任意读写后,通过 kmalloc_caches 定位 pipe 对象所在的 kmalloc-cg-2k slab,做 slab 风水:释放 pipe_buffer、用受控页回收、伪造 pipe_buffer->ops,把 page 字段指向任意物理页的 struct page,从而得到任意物理地址读写(pipe_phys_read_data / pipe_phys_write_data)。后续的 cred/seccomp/selinux patch 全部基于这个物理读写原语。
这一点在适配时非常关键。最初我以为 slide 是暴力枚举 KASLR 范围(那对高位 0xffffffe7 镜像区会是麻烦),但读 slide.c 后发现它是直接泄漏:
校验仅要求 (leaked >> 48) == 0xffff(是内核指针),与 KASLR 滑到多高无关。这决定了适配时高位镜像区不是障碍。
IonStack 的偏移是编译期注入,通过 per-固件头 src/targets/<project>/target.h 参数化:
offset.h 只是个壳(#include TARGET_CONFIG_H)。make PROJECT=<project> 选定 target。futex GhostLock 触发本身不需要偏移;从 KASLR 泄漏开始的每一步都依赖 target.h。
适配就是为本机生成一份正确的 target.h。偏移分三类:
设备已 Magisk root,这极大简化了侦察。一条条来:
kallsyms(需 root,否则空):
关键认知:_OFF 偏移 = kallsyms 运行时地址 - _text 运行时地址,KASLR slide 会抵消。所以从 kallsyms 直接就能算出 target.h 里的 A 类偏移,不需要链接时基址。
config.gz: 确认 CONFIG_ARM64_VA_BITS=39、CONFIG_CFI_CLANG=y、CONFIG_RANDOMIZE_BASE=y、CONFIG_BPF_JIT_ALWAYS_ON=y、CONFIG_USERFAULTFD=y、4K 页。这些与 Pixel 10 的 target 假设基本一致 —— 利好。
BTF: /sys/kernel/btf/vmlinux 可读(6MB),拉到本地用 bpftool btf dump file vmlinux.btf 解析,得到所有结构体字段偏移(B 类)。
iomem: Kernel code: 80010000-... → 内核物理加载在 0x80010000(Pixel 是 0x80000000)。
用一个 python 脚本批量计算(相对 _text = 0xffffffe71b800000):
ashmem 完整存在是重要利好 —— 现代 Android 在弃用 ashmem,但本机仍保留 fops + 全部成员函数。两个"地址是字段而非符号"的特例:
bpftool btf dump file vmlinux.btf + python 解析,得到:
task_struct(只列关键字段):
cred: uid +0x8 securebits +0x28 caps +0x30 security +0x80
pipe_inode_info / pipe_buffer / file_operations / seccomp / struct page / miscdevice / selinux_state: 全部按本机 6.6 布局确认。
对比 comet target.h,发现两处必须换本机值的差异:
file_operations 那 5 个偏移若用 Pixel 的值,leak_kernel_base 会读错函数指针、伪造 fops 表也会填错槽位 —— 直接崩或过不了 CFI。这是适配中最危险的一处,也证明了"照搬 target.h 不可行"。
39-bit VA 的线性映射基址由公式确定:PAGE_OFFSET = -(1UL << 39) = 0xffffff8000000000(与 Pixel 一致),VMEMMAP_START = 0xfffffffe00000000。结合 iomem:
SLIDE_RANDOM_BOOT_ID_DATA(slide KASLR 泄漏的着陆点)是 random.c 里的 static 变量,不在 kallsyms(flat Image 也没有独立符号表)。这是适配中唯一的真缺口。
解法:用 IDA 读 random_table。 IDA 加载了 flat Image(/home/zzz/AI/CyberMeowfia/split_img/kernel.i64,base 0,_text@0x0,_stext@0x10000)。flat Image 由 boot.img 解包还原,解包与内核符号/地址还原方法来自笔者博客《解包小米11的bootimg还原内核符号以及地址》。关键映射:IDA 地址 = kallsyms 偏移(相对 _text)。
random_table(off 0x2229400)是 struct ctl_table 数组(每项 0x40 字节,data 字段在 +0x8)。读 512 字节解析 8 个表项,找到 proc_handler = proc_do_uuid(0xffffffc0808b83b0)的项:
data 字段 = 0xffffffc082236a0d8,减去链接基址 0xffffffc080000000 = 0x236a0d8 —— 这正是 kallsyms 里 sysctl_bootid 符号的偏移!即 boot_id 数据缓冲区就是 sysctl_bootid 变量,SLIDE_RANDOM_BOOT_ID_DATA_OFF = 0x236a0d8。读 procname 字符串(在 IDA 0x15bcffc)确认为 "boot_id",闭环。
顺带推出链接时 _text = 0xffffffc080000000(由 proc_handler 指针 0xffffffc0808b83b0 - proc_do_uuid 偏移 0x8b83b0 得到,非标准 0xffffffc008000000,OnePlus 改过)。
关于 KIMAGE_TEXT_BASE 的抵消:这个值在 exploit 的 image/slide 数学中其实会抵消 —— 因为所有"image 地址"都定义为 KIMAGE_TEXT_BASE + OFF,而运行时通过 text_addr(x) = x + slide、slide = kaslr_base - KIMAGE_TEXT_BASE 转换,KIMAGE_TEXT_BASE 在加减中消去,p0_data_alias 里 (image_addr - KIMAGE_TEXT_BASE) 也恒等于 OFF。所以理论上任何一致值都可用,但用真值最干净。
这是适配中最关键的正确性判断。读 slide.c + BTF 对比:
rt_mutex_waiter 本机布局(BTF):
slide 的 pselect 伪造按 "word i @ 偏移 i*8" 铺设:
与本机 rt_mutex_waiter 完全吻合 ✓。主路由用到的 FAKE_WAITER_PI_TREE_*(common.h:0x28/0x40/0x50/0x58/0x60/0x68)也匹配;而非 FAKE 的 WAITER_* 在代码里根本未被使用(dead code)。结论:slide KASLR 泄漏和主路由 fops 劫持的 waiter 伪造在本机 6.6 内核上布局兼容,无需改 common.h。
所有缺口解决,生成 src/targets/PLC110-BP2A.250605.015/target.h,无 TODO(仅 PSELECT_WAITER_WORD_SHIFT=1 标注运行时可调)。
target.h 偏移已全部验证正确,完整利用运行受阻于 OnePlus 13T hardened 内核配置(非偏移问题)。
运行现象:完整利用以 shell 用户(uid 2000)LD_PRELOAD 注入运行,~36–45s 后内核 UBSAN panic 重启。崩在 slide 阶段(slide_leak_kernel_base fork 的子进程 consumer 的 sched_setattr 触发 PI 链),没进 main route,slide 没成功泄露 KASLR。
崩溃链:wake_up_state(init_task) → try_to_wake_up 锁 init_task->pi_lock → queued_spin_lock_slowpath UBSAN array bounds(lock 值 0x5512xxxx 非法)→ BRK #0x5512 → OnePlus mrdump ipanic_die 强制 panic → 重启。
根因(OnePlus 13T vs Pixel 内核配置差异):
Pixel 未开 UBSAN_TRAP,slide 的 PI 链即便触发 UBSAN 也只 warn 继续,故 slide 可用;OnePlus 直接触发 BRK → 必崩。echo 0 > /proc/sys/kernel/panic_on_oops 验证无效(mrdump 绕过)。
未解之谜:静态分析 init_task.pi_lock 静态值 = 0(IDA 确认,pid@0x618=0 验证是 init_task),地址推导全对(39-bit VA、无 KASAN、标准线性映射、phys 落在 Kernel data 段),但运行时 slowpath 读到非法 lock 值。无法静态解释,疑动态因素(线性映射运行时差异 / 竞争 / TLB),需 kgdb 确认 0xffffff800211e780 运行时实际映射的值。
可能的推进方向(待选):
本次适配沉淀出一套可复用的内核利用跨设备移植流程:
最值得记取的教训:结构体字段偏移在不同内核版本间会静默变化(file_operations 在 6.6 与 Pixel 的内核间 ioctl 偏移差 8 字节,task_struct.pi_lock 差 0x18),这类"隐性偏移"不会在 target.h 里显式标注(它们在 common.h 或代码逻辑里),却会直接导致崩溃或 CFI 失败。适配时必须用 BTF 逐一核对,而非只关注 target.h 里的符号偏移。
另一个同等重要的教训:hardened 内核配置会静默阻断利用链。CONFIG_UBSAN_TRAP 把原本只 printk warn 的 UBSAN 变成 BRK 指令,配合 CONFIG_PANIC_ON_OOPS 与厂商 panic 模块(OnePlus 的 mrdump 经 ipanic_die 强制 panic),会让某条子技术链路(本次是 slide 的 PI 链 wake_up_state -> try_to_wake_up -> slowpath)在 Pixel 上能跑、在本机一触即崩,且 echo 0 > /proc/sys/kernel/panic_on_oops 也绕不过。跨设备移植时,除偏移外必须核对 config.gz 里的 sanitizing / hardening 选项(UBSAN_TRAP、BUG_ON_DATA_CORRUPTION、PANIC_ON_OOPS、厂商 mrdump/kip 等),它们决定哪些子技术可用,可能迫使改换泄露原语(如 slide -> KernelSnitch)。
⚠️ 本文所述技术仅用于授权安全研究与自有设备测试。GhostLock 是影响所有 Linux 发行版的内核栈 UAF,正确修复应从内核 futex PI requeue 超时清理路径入手,确保不引用已释放/复用的 task 栈内存。
| 项 |
原目标 (Pixel 10 / comet) |
适配目标 (OnePlus 13T) |
| 机型 |
Pixel 10 |
OnePlus 13T (PLC110) |
| Android |
17 |
16 |
| 内核 |
android16-6.x |
6.6.89 GKI (android15-6.6) |
| VA_BITS |
39 |
39 |
| 页大小 |
4K |
4K |
| CFI |
开 |
开 |
| 镜像区 VA |
0xffffffc0... |
0xffffffe7...(KASLR 滑到高位) |
| root |
无需 |
Magisk root |
| 途径 |
原理 |
stamp_prctl |
PR_SET_MM_MAP 把 auxv 指向受控共享内存(memfd+fallocate punch hole 制造抖动) |
stamp_socket |
MCAST_JOIN_SOURCE_GROUP |
stamp_pselect |
pselect6 的 fd_set 直接铺在栈上 |
stamp_process_vm |
process_vm_readv/writev 的 iovec 数组 |
stamp_keyctl |
KEYCTL_DH_COMPUTE / KEYCTL_INSTANTIATE_IOV |
stamp_tcp |
TCP_ZEROCOPY_RECEIVE |
stamp_futex |
futex PI 本身的栈使用 |
| 类别 |
内容 |
获取方式 |
| A |
内核符号偏移(相对 _text) |
root /proc/kallsyms |
| B |
结构体字段偏移 |
BTF(/sys/kernel/btf/vmlinux)+ bpftool |
| C |
架构/内存布局常数 |
/proc/config.gz + /proc/iomem + 推理 |
| 缺口 |
boot_id 静态变量(不在 kallsyms) |
IDA 读 random_table |
| 字段 |
Pixel (comet) |
本机 6.6 |
影响 |
file_operations.unlocked_ioctl |
0x50 |
0x48 |
leak_kernel_base 读错函数指针 |
.open |
0x70 |
0x68 |
伪造 fops 表填错槽 |
.release |
0x80 |
0x78 |
同上 |
.splice_read |
0xc0 |
0xb8 |
同上 |
.show_fdinfo |
0xe0 |
0xd8 |
同上 |
task_struct.pi_lock |
0x924 |
0x90c |
伪造 task PI 字段错位 |
pi_waiters/top_task/blocked_on |
0x938/0x948/0x950 |
0x920/0x930/0x938 |
同上 |
P0_KERNEL_PHYS_LOAD |
0x80000000 |
0x80010000 |
物理别名算错 |
f_wait, f_pi_target, f_pi_chain
owner 线程: lock(f_pi_target); lock(f_pi_chain); // 持有 target,等 chain -> 阻塞
waiter 线程: lock(f_pi_chain); // 持有 chain
WAIT_REQUEUE_PI(f_wait -> f_pi_target, timeout=5s) // 等 requeue 到 target
主线程: CMP_REQUEUE_PI(f_wait -> f_pi_target) // 把 waiter requeue 到 target 的 PI 链
slide_leak_kernel_base()
└─ GhostLock 栈 UAF + pselect 伪造 rt_mutex_waiter
└─ 内核把 nfulnl_logger 运行时地址写进 boot_id 缓冲区
└─ 读 /proc/sys/kernel/random/boot_id 解析出地址 -> stext
prepare_good_kernel_page(PAGE_PAYLOAD_FOPS)
run_main_route_threads()
└─ try_cfi_stage()
└─ 改 ashmem_misc.fops 指向伪造 fops 表(CFI 友好)
└─ 得到 configfs 一次性任意内核读写
└─ leak_kernel_base:读 ashmem_fops 函数指针算内核基址
└─ install_pipe_physrw:伪造 pipe_buffer->ops -> 任意物理地址 r/w
install_android_root()
└─ 遍历 init_task.tasks 找 root 子进程
└─ patch cred: uid/gid->0, caps->full, securebits->0
└─ patch SELinux SID -> kernel_sid
└─ patch seccomp:清 TIF_SECCOMP / mode / filter(绕过 Android 应用 seccomp)
└─ 写 selinux_enforcing = 0
└─ root child: setuid(0) -> 装 su daemon -> 换壁纸
TARGET_CFLAGS := -DTARGET_CONFIG_H=\"targets/$(PROJECT)/target.h\" # Makefile:84
ffffffe71b800000 T _text
ffffffe71b810000 T _stext
noop_llseek 0x3d19f4
copy_splice_read 0x41ec54
configfs_read_iter 0x49a434
configfs_bin_write_iter 0x49a960
ashmem_ioctl 0xc8de34
compat_ashmem_ioctl 0xc8e4f0
ashmem_mmap 0xc8e544
ashmem_open 0xc8e764
ashmem_release 0xc8e7ec
ashmem_show_fdinfo 0xc8e878
ashmem_fops 0x12dc4d8
anon_pipe_buf_ops 0x115c0c8
kmalloc_caches 0x1664e10
security_hook_heads 0x16652d0
selinux_blob_sizes 0x1665a08
nfulnl_logger 0x2102748
loggers (nf_loggers[]) 0x2102690
init_task 0x210e780
init_uts_ns 0x2292ed0
empty_zero_page 0x22fe000
root_task_group 0x2306580
selinux_state 0x23490e0
ashmem_misc 0x226c118
sysctl_bootid 0x236a0d8
random_table 0x2229400
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!