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_read 和 binder_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 权限校验的完整流程
我把整个流程画了一张图,方便你理解:
从图中你可以看到,权限校验分两个阶段:
- 驱动层校验:获取 PID/UID,同时进行 SELinux 策略检查。这一步是强制性的,无法跳过。
- 用户空间校验:服务端根据业务逻辑,对调用方的 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,都是保护系统安全的重要手段。