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的触发路径是这样的:

  1. 进程A的线程T1正在执行Binder事务,持有binder_thread的锁。
  2. 同时,进程A退出,触发binder_release,它开始遍历并释放该进程的所有Binder线程。
  3. 如果T1在释放过程中恰好被binder_release遍历到,而T1又持有锁,就会产生竞争。

我在项目中遇到过类似的情况,当时调试一个多进程通信的稳定性问题,发现偶尔会有进程莫名其妙地卡死。后来定位到就是这种“释放中还在用”的竞争条件。你想想看,内核里这种问题最难复现,因为它需要精确的时间窗口。

核心问题点:

  • binder_release在释放binder_thread时,没有等待该线程上的所有操作完成。
  • binder_thread_lock保护的是线程内的数据结构,但binder_release直接操作的是线程本身的生命周期。
  • 两者之间缺少一个全局的同步机制。

说白了,就是binder_release太“急”了。它不管线程还在不在忙,直接就把资源回收了。这就像你正在用电脑写代码,突然有人把电源线拔了——数据丢失是小事,系统崩溃才是大事。

13.2 漏洞利用:从竞争到提权

好,现在我们知道漏洞是怎么触发的了。但攻击者怎么利用它来提权呢?

这里的关键在于:竞争条件导致的内存释放后重用(Use-After-Free)。

攻击流程大致如下:

  1. 攻击者创建一个进程,并启动多个线程,其中一些线程持续进行Binder通信。
  2. 在某个精确的时间点,让进程退出,触发binder_release
  3. 如果竞争成功,binder_release会释放一个仍在被其他线程引用的binder_thread结构体。
  4. 攻击者通过堆喷等技术,用自己控制的数据重新占用了这块内存。
  5. 原本持有引用的线程继续操作这块内存,实际上是在操作攻击者伪造的数据。
  6. 通过精心构造的伪造数据,攻击者可以修改内核的关键指针,最终获得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图来总结这个漏洞的完整知识体系,从触发到利用再到修复:

CVE-2016-5340 漏洞知识体系 1. 漏洞触发 • binder_release 清理线程 • 线程T1仍在执行事务 • 两者竞争同一binder_thread • 缺少同步机制 2. 利用方式 • Use-After-Free 漏洞 • 堆喷占位伪造数据 • 修改内核关键指针 • 最终获得root权限 3. 修复方案 • 引入active_threads计数 • 先标记“正在释放” • 等待所有线程退出 • 再执行清理操作 核心教训 • 内核并发编程中,资源释放必须与使用者同步,不能“先斩后奏”。 • 引用计数+等待队列是解决此类问题的经典模式,值得在驱动开发中推广。 漏洞时间线 2016年8月:漏洞公开披露 2016年9月:Google发布安全补丁 影响范围:Android 4.x ~ 6.x

13.5 避坑指南

我曾经在开发一个自定义的IPC驱动时,也犯过类似的错误。当时我设计了一个“快速清理”机制,想着进程退出时越快越好,结果导致偶发的内核崩溃。后来花了整整一周才定位到是竞争条件。

所以,这里给大家几个实用的建议:

  • 不要假设“清理时没人用”——内核里没有“绝对安全”的时刻,除非你用锁或引用计数明确保护。
  • 使用引用计数时要小心死锁——补丁中的wait_event如果使用不当,可能导致线程永远等不到计数归零。
  • 测试竞争条件要用压力测试——单次运行很难复现,我建议写一个脚本,同时启动几十个线程反复创建和销毁Binder连接。
  • 关注内核的并发安全文档——Linux内核的Documentation目录下有很多关于并发编程的指南,值得一读。

总结一下:CVE-2016-5340是一个典型的Binder驱动竞争条件漏洞,它告诉我们一个朴素的道理——在并发环境下,资源释放必须与使用者同步。这个漏洞的修复方案也很有参考价值,引用计数+等待队列的模式可以推广到其他类似的场景。

好了,关于这个漏洞的分析就到这里。记住,内核安全没有银弹,每一个细节都可能成为攻击者的突破口。保持警惕,多思考“如果这里出现并发会怎样”,你就能写出更安全的代码。

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