13、Binder竞争条件漏洞:CVE-2016-5340漏洞分析、binder_release与binder_thread_lock竞争、提权路径
各位同学,今天我们来聊一个很有意思的漏洞——CVE-2016-5340。说实话,这个漏洞在我刚接触Binder驱动时,一度让我觉得“这也能出问题?”但仔细分析后,你会发现它背后反映的,是内核并发编程中一个非常经典的陷阱:竞争条件。
这个漏洞出在Binder驱动的binder_release函数里。嗯,咱们先别急着看代码,先想想看:当一个进程退出时,内核会调用binder_release来清理它占用的Binder资源。但问题是,清理过程中如果另一个线程正在操作同一个Binder节点,会发生什么?
13.1 漏洞根源:binder_release与binder_thread_lock的竞争
我个人习惯把这类问题叫做“清理时的幽灵之手”。你这边在释放资源,那边还有线程在偷偷使用,不出事才怪。
具体来说,CVE-2016-5340的触发路径是这样的:
- 进程A的线程T1正在执行Binder事务,持有
binder_thread的锁。 - 同时,进程A退出,触发
binder_release,它开始遍历并释放该进程的所有Binder线程。 - 如果T1在释放过程中恰好被
binder_release遍历到,而T1又持有锁,就会产生竞争。
我在项目中遇到过类似的情况,当时调试一个多进程通信的稳定性问题,发现偶尔会有进程莫名其妙地卡死。后来定位到就是这种“释放中还在用”的竞争条件。你想想看,内核里这种问题最难复现,因为它需要精确的时间窗口。
核心问题点:
binder_release在释放binder_thread时,没有等待该线程上的所有操作完成。binder_thread_lock保护的是线程内的数据结构,但binder_release直接操作的是线程本身的生命周期。- 两者之间缺少一个全局的同步机制。
说白了,就是binder_release太“急”了。它不管线程还在不在忙,直接就把资源回收了。这就像你正在用电脑写代码,突然有人把电源线拔了——数据丢失是小事,系统崩溃才是大事。
13.2 漏洞利用:从竞争到提权
好,现在我们知道漏洞是怎么触发的了。但攻击者怎么利用它来提权呢?
这里的关键在于:竞争条件导致的内存释放后重用(Use-After-Free)。
攻击流程大致如下:
- 攻击者创建一个进程,并启动多个线程,其中一些线程持续进行Binder通信。
- 在某个精确的时间点,让进程退出,触发
binder_release。 - 如果竞争成功,
binder_release会释放一个仍在被其他线程引用的binder_thread结构体。 - 攻击者通过堆喷等技术,用自己控制的数据重新占用了这块内存。
- 原本持有引用的线程继续操作这块内存,实际上是在操作攻击者伪造的数据。
- 通过精心构造的伪造数据,攻击者可以修改内核的关键指针,最终获得root权限。
注意:这个漏洞的利用难度较高,因为需要精确控制竞争窗口。但在Android 4.x到6.x的某些内核版本上,确实存在稳定的利用方法。我记得当时有安全团队演示过,在Nexus 6P上成功实现了提权。
13.3 补丁分析:如何修复这个竞争条件
Google的修复方案其实很直接——在binder_release中增加了一个等待机制。
我们来看一下关键的补丁代码:
// 修复前的binder_release(简化版)
static int binder_release(struct inode *nodp, struct file *filp) {
struct binder_proc *proc = filp->private_data;
// 直接释放所有线程
binder_defer_work(proc, BINDER_DEFERRED_RELEASE);
return 0;
}
// 修复后的binder_release(简化版)
static int binder_release(struct inode *nodp, struct file *filp) {
struct binder_proc *proc = filp->private_data;
// 先标记进程为“正在释放”
proc->is_releasing = 1;
// 等待所有线程完成当前操作
wait_event(proc->wait, atomic_read(&proc->active_threads) == 0);
// 然后再进行清理
binder_defer_work(proc, BINDER_DEFERRED_RELEASE);
return 0;
}
嗯,这里要注意:补丁的核心思想是“先等后清”。它引入了一个引用计数active_threads,每个线程在开始Binder操作时递增,结束时递减。binder_release会等待这个计数归零,确保没有线程还在活跃。
我的建议:如果你在内核开发中遇到类似的“清理时竞争”问题,可以借鉴这个思路——不要直接释放,先标记“即将释放”,然后等待所有使用者退出。这是一种很经典的“引用计数+等待队列”模式。
13.4 知识体系图
下面我用一张SVG图来总结这个漏洞的完整知识体系,从触发到利用再到修复:
13.5 避坑指南
我曾经在开发一个自定义的IPC驱动时,也犯过类似的错误。当时我设计了一个“快速清理”机制,想着进程退出时越快越好,结果导致偶发的内核崩溃。后来花了整整一周才定位到是竞争条件。
所以,这里给大家几个实用的建议:
- 不要假设“清理时没人用”——内核里没有“绝对安全”的时刻,除非你用锁或引用计数明确保护。
- 使用引用计数时要小心死锁——补丁中的
wait_event如果使用不当,可能导致线程永远等不到计数归零。 - 测试竞争条件要用压力测试——单次运行很难复现,我建议写一个脚本,同时启动几十个线程反复创建和销毁Binder连接。
- 关注内核的并发安全文档——Linux内核的Documentation目录下有很多关于并发编程的指南,值得一读。
总结一下:CVE-2016-5340是一个典型的Binder驱动竞争条件漏洞,它告诉我们一个朴素的道理——在并发环境下,资源释放必须与使用者同步。这个漏洞的修复方案也很有参考价值,引用计数+等待队列的模式可以推广到其他类似的场景。
好了,关于这个漏洞的分析就到这里。记住,内核安全没有银弹,每一个细节都可能成为攻击者的突破口。保持警惕,多思考“如果这里出现并发会怎样”,你就能写出更安全的代码。