15、Binder 的权限校验:调用方 PID/UID 的获取与校验,以及 SELinux 在 Binder 中的应用

说到 Binder 通信,很多人只关注数据怎么传、怎么序列化。但有个问题我一直觉得特别关键——你怎么知道对面是谁?

你想想看,一个 App 调用了一个系统服务,系统服务怎么判断这个调用是合法的?总不能来者不拒吧。这就引出了我们今天要聊的核心:Binder 的权限校验机制

说白了,Binder 的权限校验分两层:一层是传统的 PID/UID 校验另一层是 SELinux 的 MAC 强制访问控制。这两者缺一不可。

15.1 调用方 PID/UID 的获取

在 Binder 驱动层面,每次通信都会携带调用方的身份信息。这些信息被记录在 binder_transaction 结构体中。我习惯把这种机制称为「通信的身份证」。

驱动层通过 binder_get_transaction 函数获取调用方的 PID 和 UID。具体来说:

  • PID:调用进程的进程 ID,由 task_tgid_nr(current) 获取
  • UID:调用进程的用户 ID,由 from_kuid(&init_user_ns, current_uid()) 获取

这里有个细节我提醒一下:PID 和 UID 是在驱动层获取的,用户空间无法伪造。为什么?因为驱动层拿的是当前正在运行的进程信息,你改不了。

核心要点:PID/UID 的获取发生在驱动层的 binder_thread_readbinder_transaction 函数中。这些值被写入 binder_transaction_data 结构体,随数据一起传递到接收端。

15.2 用户空间的校验方式

在用户空间,服务端收到 Binder 调用后,可以通过 Binder.getCallingPid()Binder.getCallingUid() 获取调用方的身份。

我举个例子,假设你写了一个系统服务,只允许系统应用调用:

public class MySystemService extends IMySystemService.Stub {
    @Override
    public void sensitiveOperation() {
        final int callingUid = Binder.getCallingUid();
        if (callingUid != Process.SYSTEM_UID) {
            throw new SecurityException("只有系统应用才能调用此方法");
        }
        // 执行敏感操作
    }
}

嗯,这里要注意:Binder.getCallingPid()Binder.getCallingUid() 只能在 Binder 线程中调用。如果你在非 Binder 线程中调用,返回的是当前进程自己的 PID/UID,那就没意义了。

我在项目中遇到过一个问题:有个同事在异步回调里调用了 Binder.getCallingUid(),结果拿到的永远是自己的 UID。排查了半天才发现,原来回调已经脱离了 Binder 线程上下文。

避坑指南:如果你需要在异步回调中校验调用方身份,记得在 Binder 方法入口处就把 PID/UID 保存下来,不要等到回调里再获取。

15.3 SELinux 在 Binder 中的应用

PID/UID 校验虽然有用,但它有个致命缺陷——它依赖调用方的身份,而不是行为。说白了,只要 UID 对了,什么都能干。这显然不够安全。

所以 Android 引入了 SELinux 的 MAC(强制访问控制)机制。SELinux 在 Binder 通信中扮演的角色,我总结为「最后的守门员」。

在 Binder 驱动层,每次 transaction 都会经过 selinux_android_binder_transaction 函数的检查。这个函数会检查:

  • 调用方的 SELinux context
  • 服务端的 SELinux context
  • 目标服务的名称(即 Binder 接口的标识)

只有通过了 SELinux 策略的允许规则,通信才能继续。否则驱动层直接返回错误。

我曾经调试过一个诡异的问题:某个系统服务明明 UID 校验通过了,但调用还是失败。最后发现是 SELinux 策略没配好,导致 Binder 通信被拦截了。

注意:SELinux 的检查是在驱动层完成的,用户空间无法绕过。即使你拿到了 root 权限,也无法绕过 SELinux 的 MAC 控制。这是 Android 安全模型的最后一道防线。

15.4 权限校验的完整流程

我把整个流程画了一张图,方便你理解:

Binder 权限校验完整流程 调用方进程 PID/UID 由驱动获取 Binder 调用 Binder 驱动层 获取 PID/UID SELinux 策略检查 通过 服务端进程 用户空间 UID 校验 拒绝 返回错误 -EPERM / SecurityException 拒绝 SecurityException 图例说明 调用方/服务方进程 Binder 驱动层(内核空间) 拒绝/错误路径 允许路径 拒绝路径

从图中你可以看到,权限校验分两个阶段:

  1. 驱动层校验:获取 PID/UID,同时进行 SELinux 策略检查。这一步是强制性的,无法跳过。
  2. 用户空间校验:服务端根据业务逻辑,对调用方的 UID 进行二次校验。这一步是可选的,但强烈建议加上。

15.5 实际项目中的经验

我在做系统服务开发时,总结了几条经验:

  • 不要只依赖 UID 校验:UID 可以被伪造(比如通过 root 权限切换),但 SELinux context 很难绕过。
  • SELinux 策略要细化:不要给所有系统服务开绿灯。每个服务应该有自己的 SELinux 规则。
  • 日志要打全:在权限校验失败时,记录下调用方的 PID、UID 和 SELinux context,方便排查问题。

小技巧:在调试 SELinux 权限问题时,可以用 adb shell dmesg | grep avc 查看 SELinux 的拒绝日志。这个命令我几乎每天都会用。

好了,关于 Binder 的权限校验,我们就聊到这里。记住一句话:权限校验不是可选项,而是必选项。无论是 PID/UID 还是 SELinux,都是保护系统安全的重要手段。