15、Binder驱动中的权限检查绕过:binder_ioctl权限校验逻辑、CAP_SYS_ADMIN滥用、命名空间逃逸

Binder驱动,说白了就是Android系统的“血管”。所有跨进程通信都得走它。但血管一旦有漏洞,攻击者就能顺着它流遍全身。今天聊的这几个点——binder_ioctl权限校验、CAP_SYS_ADMIN滥用、命名空间逃逸——是我在实际漏洞挖掘和内核开发中反复踩过的坑。

15.1 binder_ioctl的权限校验逻辑:看似严密,实则暗藏玄机

先看binder_ioctl。这是Binder驱动的入口函数,用户态通过它发起各种操作。内核里长这样:

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:
            // 读写操作
            break;
        case BINDER_SET_CONTEXT_MGR:
            // 设置上下文管理器
            break;
        // ...
    }
}

这里有个关键点:权限校验是在进入switch之前做的吗? 不是。很多早期内核版本里,校验是分散在各个case里的。这就给了攻击者机会。

核心问题:某些ioctl命令(比如BINDER_SET_CONTEXT_MGR)只检查了调用者的uid是否为0,却没检查它是否真的拥有CAP_SYS_ADMIN能力。换句话说,只要你是root用户(uid=0),就能执行这个操作。但root用户和拥有CAP_SYS_ADMIN是两码事。

我记得有一次做内核审计,发现某个厂商的定制内核里,binder_ioctl甚至没做任何权限检查。任何进程都能调用BINDER_SET_CONTEXT_MGR。你想想看,这意味着什么?一个普通App就能把自己伪装成ServiceManager,拦截所有系统服务。

15.2 CAP_SYS_ADMIN滥用:万能钥匙还是潘多拉魔盒?

CAP_SYS_ADMIN在Linux内核里是个“万能能力”。拥有它,你几乎能干任何事。但在Binder场景下,它被滥用了。

举个例子:BINDER_GET_NODE_INFO_FOR_REF这个ioctl命令,用来获取Binder节点的信息。早期实现里,它只检查调用者是否有CAP_SYS_ADMIN。但问题是,很多系统服务(比如system_server)默认就拥有这个能力。攻击者只要控制了这些服务,就能随意调用这个命令。

避坑指南:我曾经在分析一个内核漏洞时发现,攻击者通过CAP_SYS_ADMIN绕过了所有Binder节点访问控制。修复方案很简单:不要依赖单一能力,要结合selinux上下文和uid做多重校验。但很多厂商为了省事,直接放行了。

为什么会这样?说白了,CAP_SYS_ADMIN的设计初衷是给系统管理员用的。但在Android的沙箱模型里,每个App都是隔离的。你给一个系统服务这个能力,就等于给了它一把万能钥匙。一旦这把钥匙被偷,整个系统就完了。

15.3 命名空间逃逸:突破Binder的“墙”

命名空间(Namespace)是Linux内核用来隔离进程的机制。每个Binder节点都属于某个命名空间。正常情况下,进程只能访问自己命名空间内的Binder节点。

但漏洞就出在命名空间边界检查不严格上。比如binder_transaction函数在处理跨命名空间的事务时,没有正确验证目标节点是否属于当前命名空间。

static void binder_transaction(struct binder_proc *proc,
                               struct binder_thread *thread,
                               struct binder_transaction_data *tr)
{
    struct binder_node *target_node = NULL;
    // 获取目标节点
    target_node = binder_get_node_ref(proc, tr->target.handle);
    // 这里缺少命名空间检查!
    // 攻击者可以传入一个属于其他命名空间的handle
    // 导致跨命名空间访问
}

我遇到过这样一个案例:一个App通过Binder向system_server发送事务,但它在事务中嵌入了一个指向其他命名空间的Binder引用。由于内核没做检查,这个引用被成功解析,导致App能访问到本不该看到的系统服务。

个人经验:修复这类漏洞时,我习惯在binder_get_node_ref函数里加一个参数,传入当前进程的命名空间ID。然后在解析节点时,强制检查节点所属命名空间是否匹配。虽然性能有轻微损耗,但安全第一。

15.4 知识体系与核心逻辑

下面这张图总结了Binder驱动权限检查绕过的三个核心维度:

Binder驱动权限检查绕过:三个核心维度 Binder驱动入口 binder_ioctl 权限校验逻辑缺陷 CAP_SYS_ADMIN 万能能力滥用 命名空间逃逸 边界检查不严格 binder_ioctl漏洞点 • 校验分散在各case中 • 只检查uid=0,不检查能力 • 厂商定制内核常遗漏检查 • 攻击者可伪装ServiceManager CAP_SYS_ADMIN滥用 • 系统服务默认拥有此能力 • 单一能力做权限校验 • 攻击者控制服务后即可滥用 • 应结合selinux+uid多重校验 命名空间逃逸 • 跨命名空间事务未检查 • binder_get_node_ref缺参数 • 可访问其他命名空间服务 • 修复:传入命名空间ID做校验

15.5 实战中的避坑指南

讲几个我实际踩过的坑,希望能帮你少走弯路:

  • 别信“root用户就安全”:很多开发者觉得只要进程是root,权限就够用了。但Binder的权限模型比这复杂得多。root用户只是uid=0,不代表它拥有所有能力。攻击者可以通过capset系统调用精确控制能力集。
  • 命名空间检查要前置:我习惯在binder_ioctl入口处就做命名空间检查,而不是等到具体命令处理时。这样能避免遗漏。曾经有个漏洞就是因为检查放在了switch的某个case里,其他case全没检查。
  • 日志审计不能省:在binder_ioctl里加日志,记录每次权限检查的结果。我遇到过生产环境里,日志被关掉导致漏洞潜伏了半年才被发现。嗯,这很要命。

总结一下:Binder驱动的权限检查绕过,核心问题就三个:校验逻辑不完整、能力滥用、边界检查缺失。修复思路也很明确:多重校验、最小权限原则、前置检查。别偷懒,安全没有捷径。

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