30. 安全机制:Binder 如何通过 PID/UID 校验实现安全访问控制?

说到 Binder 的安全机制,我脑子里第一个蹦出来的就是 PID 和 UID 校验。这玩意儿说白了,就是 Android 系统给每个进程发的一张「身份证」。你想想看,一个系统里这么多进程跑来跑去,要是谁都能随便调用别人的服务,那不乱套了吗?

我在做系统稳定性优化的时候,就遇到过好几次因为安全校验没做好导致的诡异问题。有一次某个第三方应用居然能直接调用系统级的 Location 服务,查了半天才发现是 Binder 的 UID 校验被绕过了。嗯,从那以后我对这套机制就格外上心。

PID 和 UID 到底是什么?

先简单说下概念:

  • PID(Process ID):进程的唯一标识。每次启动一个进程,系统就给它分配一个 PID。这个值会变,进程重启后 PID 就换了。
  • UID(User ID):用户标识。在 Android 里,每个应用安装时都会被分配一个 UID。系统应用的 UID 通常是固定的(比如 system 是 1000,root 是 0)。

Binder 驱动在传输数据时,会自动把调用方的 PID 和 UID 塞进传输结构体里。服务端拿到这些信息,就能判断「谁在叫我」。

关键点:PID 和 UID 是由内核 Binder 驱动填充的,应用层无法伪造。这是整个安全机制的基石。

Binder 驱动如何获取 PID/UID?

我们来看看 Binder 驱动里是怎么干的。当进程 A 通过 Binder 向进程 B 发送请求时,驱动会做这么几件事:

  1. 从当前进程的 task_struct 里读取 PID
  2. 从当前进程的 cred 结构体里读取 UID
  3. 把这些值写入 Binder 事务的 sender_pidsender_euid 字段

代码层面,在 drivers/android/binder.c 里可以看到:

// Binder 驱动中填充 PID/UID 的核心逻辑
static void binder_transaction(struct binder_proc *proc,
                               struct binder_thread *thread,
                               struct binder_transaction_data *tr)
{
    struct binder_transaction *t;
    struct binder_work *w;

    // 从当前进程获取 PID
    t->from = proc;
    t->from_pid = proc->pid;
    
    // 从当前进程的 cred 获取 UID
    t->from_uid = current_euid().val;
    
    // 后续处理...
}

这里有个细节:current_euid() 获取的是有效用户 ID,不是真实用户 ID。为什么用 euid?因为有些进程通过 setuid 切换了身份,这时候应该用切换后的身份做校验。

服务端如何校验?

服务端拿到 PID 和 UID 后,一般有两种校验方式:

1. 显式校验(手动检查)

在 Binder 服务端代码里,通过 Binder.getCallingPid()Binder.getCallingUid() 获取调用方信息,然后自己判断:

// 服务端显式校验示例
public class MyService extends IMyService.Stub {
    
    private static final int ALLOWED_UID = 1000; // 只允许 system 进程调用
    
    @Override
    public void sensitiveOperation() {
        int callingUid = Binder.getCallingUid();
        int callingPid = Binder.getCallingPid();
        
        // 校验 UID
        if (callingUid != ALLOWED_UID) {
            throw new SecurityException("UID " + callingUid + " 没有权限");
        }
        
        // 记录日志,方便排查问题
        android.util.Log.i("MyService", 
            "sensitiveOperation called by PID=" + callingPid + ", UID=" + callingUid);
        
        // 执行实际逻辑
        doSensitiveWork();
    }
}

我个人习惯在敏感接口里都加上这种校验。别看就几行代码,关键时刻能挡住不少恶意调用。

2. 隐式校验(权限声明)

Android 框架层提供了一套基于权限的校验机制。服务端声明需要某个权限,系统在 Binder 调用链路中自动检查:

// AndroidManifest.xml 中声明权限
<permission android:name="com.example.ACCESS_SENSITIVE" 
            android:protectionLevel="signature" />

// 服务端代码中检查权限
public class MyService extends IMyService.Stub {
    
    @Override
    public void sensitiveOperation() {
        // 系统自动检查调用方是否拥有该权限
        int result = checkCallingPermission("com.example.ACCESS_SENSITIVE");
        if (result != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("缺少必要权限");
        }
        
        doSensitiveWork();
    }
}

这种方式的好处是:权限声明在 Manifest 里,一目了然。但要注意,checkCallingPermission 内部其实也是通过 PID/UID 去查权限数据库的。

PID/UID 校验的典型场景

我整理了几个实际项目中常见的校验场景:

场景 校验方式 说明
系统服务只允许 system 进程调用 UID == 1000 比如 WindowManagerService 的某些接口
只允许特定应用调用 UID 匹配指定包名 通过 PackageManager 查 UID
只允许前台进程调用 PID 对应的进程处于前台 需要结合 ActivityManager 判断
只允许同签名应用调用 UID 对应的签名一致 通过 PackageManager 比对签名

避坑指南:我曾经在做一个系统级服务时,只校验了 UID 没校验 PID。结果有个恶意进程 fork 了一个子进程,子进程继承了父进程的 UID,绕过了校验。后来我加上了 PID 校验,并且定期检查 PID 对应的进程是否还活着。

SVG 流程图:Binder PID/UID 校验流程

Binder PID/UID 校验流程 调用方进程 Binder 驱动 服务端进程 发送请求 转发请求+PID/UID 驱动内部处理 1. 读取 task_struct 获取 PID 2. 读取 cred 获取 UID 3. 写入 binder_transaction 服务端校验 1. getCallingPid() 获取 PID 2. getCallingUid() 获取 UID 3. 与允许列表比对 4. 通过则执行,否则抛异常 校验失败:抛出 SecurityException 校验通过:执行实际逻辑 PID/UID 由内核填充,不可伪造

实际项目中的坑

说几个我踩过的坑:

  • PID 复用问题:进程退出后,PID 可能被新进程复用。如果你缓存了 PID,一定要确认对应的进程还是原来那个。我一般会同时校验 PID 和进程启动时间。
  • UID 伪装:虽然应用层不能伪造 UID,但 root 进程可以切换 UID。所以对于特别敏感的操作,建议加上 SELinux 策略做双重保障。
  • 跨进程调用链:A 调用 B,B 再调用 C。这时候 C 拿到的 PID/UID 是 B 的,不是 A 的。如果需要原始调用方信息,得手动传递。

特别注意:不要完全依赖 PID 做权限控制。PID 是动态的,进程重启就变了。UID 才是相对稳定的标识。我见过有人用 PID 做白名单,结果系统一重启,所有 PID 都变了,服务直接挂掉。

总结

Binder 的 PID/UID 校验机制,说白了就是一套「你是谁,你从哪里来」的身份验证系统。内核负责提供真实身份,服务端负责判断是否放行。这套机制虽然简单,但非常有效——前提是你得正确使用它。

我个人建议:所有对外暴露的 Binder 接口,至少加上 UID 校验。如果涉及敏感操作,再加上 PID 校验和权限声明。别嫌麻烦,安全这东西,多一道防线总比少一道好。


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