针对 CVE-2023-20938 的本地权限提升概念验证,这是一个 Android binder 驱动程序中的释放后使用漏洞。从无权限的 shell 中获得 uid 0 并将 SELinux 降级为 permissive,然后生成一个 root shell。
目标构建环境:Android Cuttlefish / Goldfish AVD, x86_64, 内核 5.10.107。
⚠️ 仅限授权研究/教育用途。 仅在您自己的测试设备和模拟器上运行。该错误已在当前 Android 中修复;此 PoC 针对的是一个旧版、故意留有漏洞的测试版本。不提供任何保证。

使用 asciinema 录制,并使用 agg 转换为 GIF:
drivers/android/binder.c,binder_transaction_buffer_release()binder_transaction_buffer_release() 信任攻击者提供的 offsets_size。发送一个 offsets_size 比完整条目少一个字节(sizeof(offset) - 1 == 7)的事务,会使释放路径错误解析尾部的 flat_binder_object,并过度减少一个 binder_node 的引用计数,从而在悬垂引用仍存在时将其释放。
该漏洞利用将跨缓存信息泄露串联为任意读取和受控写入,然后重写凭据。各阶段如下(参见 poc_cf.c 中的 main()):
B. 跨缓存信息泄露
释放一批 binder_node (kmalloc-128) -> 页面返回给伙伴分配器
-> 用 eventpoll epitems (eventpoll_epi 缓存,0x80 字节对象) 回收该页面。
一个悬垂的 binder 事务现在从 epitem 中读取内核指针,
泄露一个 struct file 及其 epitem 地址。
C. 任意读取
重新触发 UAF,并使用喷洒的伪造 binder_node 将 binder 的
hlist_del 解除链接转换为受控的 *(where) = what 写入。
两次写入将受害者文件的 f_inode 重定向到覆盖在 epitem 上的伪造 inode,
并将其标记为 S_PRIVATE。随后对文件执行 FIGETBSZ 会返回
*(epitem.event.data + 24):一个可操纵的 4 字节读取。
D/E/F. 权限提升
击败 KASLR(通过 f_op 泄露 eventfd_fops),遍历 init_task 的任务列表
到启动器的 cred,将其 uid/gid 集合和 seccomp.mode 清零,
然后将 selinux_state 清零以将 SELinux 降级为 permissive。
G. 一个预先派生、不含 eventfd 的启动器执行 root shell。
需要两个无权限进程共享一个 binder 节点来驱动 UAF;
通过 hwservicemanager 的 ITokenManager(createToken / get-by-token)。
clang -static -Wall -Wextra -o poc_cf poc_cf.c -lpthread
binder.h 必须位于 poc_cf.c 旁边。有意采用静态构建,以便单个二进制文件能在没有工具链的目标上运行。
从你的 Cuttlefish 主机包目录,使用匹配的内核 + initramfs 启动 AVD:
HOME=$PWD ./bin/launch_cvd \
--kernel_path=bzImage \
--initramfs_path=initramfs.img
adb push poc_cf /data/local/tmp/
adb shell /data/local/tmp/poc_cf
成功运行会依次经过各个阶段,并最终进入 root shell:
该漏洞利用是概率性的。每个阶段都会在内部重试。
因此,无论你看到的是干净退出还是客户机崩溃,只需重新运行(AVD 的 /data 会在重启时清除,所以崩溃后需要重新推送二进制文件)。
如果在较慢或更繁忙的目标上回收命中率较低,可以增加等待时间或提高喷洒数量(poc_cf.c 顶部附近的 #define 宏):
结构体偏移量和未随机化的符号基线(*_NOKASLR)是特定于此 5.10.107 构建版本的,在其他地方无法使用。要移植到不同的内核,请使用匹配的 vmlinux 中的值更新 poc_cf.c 顶部的偏移量块和符号块。我在提取偏移量后在 Pixel 7 上尝试过。
| 宏 | 默认值 | 含义 |
|---|
SETTLE_US | 5000 | 节点释放后、喷洒前暂停的时间 |
FAKE_SPRAY_COUNT | 8 | 阻塞的 sendmsg() 竞争回收槽位的数量 |
FAKE_WAIT_US | 10000 | 释放喷洒后、解除链接前暂停的时间 |
PRESKIP_US | 1000 | 第一次牺牲性 sendmsg 后暂停的时间 |
KW_RETRY_COUNT | 8 | 每次解除链接写入的尝试次数 |
STAGE_C_TRIES | 8 | 启动任意读取的尝试次数 |