2、Binder驱动层分析:Binder驱动的初始化、binder_open、binder_mmap、binder_ioctl核心函数解析

好,我们直接进入Binder驱动层。很多做应用层开发的兄弟,一听到「驱动」两个字就头大。其实没那么玄乎。Binder驱动,说白了就是内核里一个字符设备驱动。它负责把用户空间的IPC请求,转成内核能懂的数据结构,再送到目标进程。

我个人习惯把Binder驱动拆成四个阶段来看:初始化打开设备内存映射IO控制。这四个阶段,对应四个核心函数。搞懂了它们,Binder的底层逻辑你就拿下了七成。

核心要点:Binder驱动不是简单的数据搬运工。它管理着进程间的线程池、内存映射、引用计数。任何一个环节出问题,轻则卡顿,重则提权漏洞。

2.1 Binder驱动的初始化:binder_init

驱动加载时,内核会调用 binder_init。这个函数做什么?注册 misc 设备,创建/dev/binder节点,初始化各种链表和锁。

我记得第一次看这部分代码时,有个细节让我印象很深——Binder驱动是misc设备,不是普通的字符设备。为什么?因为misc设备共享主设备号10,节省资源。你想想看,Android系统里那么多驱动,每个都占一个主设备号,那不乱套了?

// 驱动入口
static int __init binder_init(void)
{
    int ret;
    // 创建名为"binder"的misc设备
    binder_deferred_workqueue = create_singlethread_workqueue("binder");
    if (!binder_deferred_workqueue)
        return -ENOMEM;

    ret = misc_register(&binder_miscdev);
    if (ret)
        destroy_workqueue(binder_deferred_workqueue);
    return ret;
}

这里有个坑。我曾经在分析某个内核版本时发现,binder_init 如果失败,系统不会直接崩溃,但所有跨进程通信都会挂掉。AMS、WMS这些系统服务全得瘫痪。所以,驱动初始化阶段的错误处理,是安全审计的重点。

安全提示:如果攻击者能通过某种方式阻止binder_init执行,或者篡改初始化过程中的链表指针,就能造成拒绝服务。虽然实际利用难度高,但概念验证是可行的。

2.2 打开设备:binder_open

当用户空间的进程调用 open("/dev/binder", ...) 时,内核会执行 binder_open。这个函数的核心任务就一个:为当前进程创建一个binder_proc结构体

binder_proc是什么?它是Binder驱动里最重要的数据结构之一。每个使用Binder的进程,都有一个对应的binder_proc。里面记录了进程的PID、线程列表、待处理事务、内存映射信息等等。

static int binder_open(struct inode *nodp, struct file *filp)
{
    struct binder_proc *proc;
    // 分配并初始化binder_proc
    proc = kzalloc(sizeof(*proc), GFP_KERNEL);
    if (proc == NULL)
        return -ENOMEM;
    get_task_struct(current);
    proc->tsk = current;
    // 初始化链表和锁
    INIT_LIST_HEAD(&proc->todo);
    init_waitqueue_head(&proc->wait);
    proc->pid = current->pid;
    // 关联到文件指针
    filp->private_data = proc;
    // 加入全局链表
    hlist_add_head(&proc->proc_node, &binder_procs);
    return 0;
}

嗯,这里要注意:binder_open 没有做权限检查。任何进程,只要有权访问/dev/binder,就能打开它。那权限控制在哪?在文件系统的权限位。默认情况下,/dev/binder的权限是0666,所以所有应用都能用。但如果你做定制系统,想限制某些进程使用Binder,改文件权限就行。

避坑指南:我曾经遇到一个案例,某个厂商在SELinux策略里误封了Binder设备的访问权限,结果所有第三方应用启动就崩溃。排查了两天才找到原因。所以,改Binder权限前,一定要确认系统服务不受影响。

2.3 内存映射:binder_mmap

这是Binder性能的关键。为什么Binder比传统Socket快那么多?答案就在 binder_mmap

传统IPC需要两次数据拷贝:发送方从用户空间拷贝到内核空间,接收方再从内核空间拷贝到用户空间。Binder只拷贝一次。怎么做到的?内核空间和用户空间共享同一块物理内存

具体来说,Binder驱动在内核分配一块物理页面,然后同时映射到内核虚拟地址空间和用户虚拟地址空间。发送方把数据写到自己的用户空间,内核直接就能看到。接收方也一样。

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct binder_proc *proc = filp->private_data;
    // 限制映射大小,防止耗尽内核内存
    if ((vma->vm_end - vma->vm_start) > SZ_4M)
        return -EINVAL;
    // 禁止可执行映射
    if (vma->vm_flags & VM_EXEC)
        return -EAGAIN;
    // 分配物理页面并建立映射
    // ... 省略具体分配逻辑
    proc->buffer = vma->vm_start;
    proc->user_buffer_offset = vma->vm_start - (unsigned long)proc->buffer;
    return 0;
}

这里有个安全细节:映射大小限制在4MB。为什么是4MB?因为每个进程都要映射一块内存,如果没限制,恶意应用可以创建大量进程,每个映射几百MB,内核内存很快就耗尽了。这就是典型的资源耗尽攻击。

另外,VM_EXEC 标志被禁止。这意味着映射的内存不可执行。为什么?防止攻击者把shellcode写到共享内存里,然后跳转执行。我在分析某个内核漏洞时,就看到过类似的攻击手法——通过Binder映射可执行内存,然后提权。

个人经验:有一次我审计某个ROM的Binder驱动,发现他们把4MB的限制改成了64MB。问原因,说是为了传输大文件。我当时就指出:这等于给攻击者开了后门。后来他们改回去了。

2.4 IO控制:binder_ioctl

这是Binder驱动里最复杂的函数,没有之一。用户空间通过 ioctl 系统调用,向驱动发送各种命令。常见的命令有:

命令 功能 安全风险
BINDER_WRITE_READ 读写Binder数据(核心命令) 缓冲区溢出、类型混淆
BINDER_SET_CONTEXT_MGR 注册为ServiceManager 权限提升(只能注册一次)
BINDER_THREAD_EXIT 通知驱动线程退出 UAF(释放后使用)
BINDER_VERSION 获取Binder版本 信息泄露(低风险)

binder_ioctl 的代码量很大,几百行是常事。我挑最核心的 BINDER_WRITE_READ 来说。

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct binder_proc *proc = filp->private_data;
    switch (cmd) {
    case BINDER_WRITE_READ: {
        struct binder_write_read bwr;
        // 从用户空间拷贝命令参数
        if (copy_from_user(&bwr, (void __user *)arg, sizeof(bwr)))
            return -EFAULT;
        // 处理写请求
        if (bwr.write_size > 0)
            ret = binder_thread_write(proc, thread, bwr.write_buffer, bwr.write_size, &bwr.write_consumed);
        // 处理读请求
        if (bwr.read_size > 0)
            ret = binder_thread_read(proc, thread, bwr.read_buffer, bwr.read_size, &bwr.read_consumed, filp->f_flags & O_NONBLOCK);
        // 把结果写回用户空间
        if (copy_to_user((void __user *)arg, &bwr, sizeof(bwr)))
            return -EFAULT;
        break;
    }
    // ... 其他命令
    }
    return 0;
}

你看,这里用了 copy_from_usercopy_to_user。为什么不用简单的 memcpy?因为用户空间的指针可能是伪造的。直接 memcpy 会导致内核崩溃,或者更糟——被攻击者利用来读写任意内核内存。

我曾经在分析CVE-2019-2215时,就发现漏洞出在 binder_ioctl 的某个路径上。攻击者通过精心构造的ioctl命令,触发了内核中的UAF,最终实现提权。这个漏洞影响Android 4.4到10,范围极广。

安全警示:binder_ioctl 是Binder攻击面最大的入口。几乎所有已知的Binder漏洞,都出在这个函数或其子函数中。做安全审计时,binder_ioctl 应该是第一优先级。

2.5 核心逻辑流程图

下面这张图,是我自己总结的Binder驱动核心流程。从用户空间调用,到驱动处理,再到返回结果,一目了然。

Binder驱动核心流程 用户空间 进程A(客户端) 系统调用:open / ioctl / mmap 内核空间 binder_init 注册misc设备 binder_open 创建binder_proc binder_mmap 共享内存映射 binder_ioctl(核心入口,攻击面最大) BINDER_WRITE_READ / SET_CONTEXT_MGR / THREAD_EXIT 返回用户空间,继续执行 初始化/打开/映射 IO控制(高风险)

从图中可以看到,binder_ioctl 是连接用户空间和内核空间的核心枢纽。所有Binder通信,最终都要经过它。这也是为什么安全研究员最喜欢盯着这个函数看。

2.6 小结

Binder驱动层的四个核心函数,各有各的职责:

  • binder_init:搭好舞台,注册设备
  • binder_open:为每个进程分配专属数据结构
  • binder_mmap:建立共享内存,实现零拷贝
  • binder_ioctl:处理所有IPC请求,也是最容易出漏洞的地方

我个人觉得,理解Binder驱动的最好方式,就是跟着代码走一遍流程。从open开始,到mmap,再到ioctl。每一步都搞清楚内核做了什么,用户空间得到了什么。这样,你再看那些Binder漏洞的分析报告,就不会觉得是天书了。

学习建议:如果你手边有Android内核源码,建议直接打开 drivers/android/binder.c,对照着本文讲的四个函数看。代码是最好的老师。

好,Binder驱动层就讲到这里。下一层,我们会进入Binder协议层,看看数据在驱动里是怎么流转的。


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