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 发送请求时,驱动会做这么几件事:
- 从当前进程的 task_struct 里读取 PID
- 从当前进程的 cred 结构体里读取 UID
- 把这些值写入 Binder 事务的
sender_pid和sender_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 校验流程
实际项目中的坑
说几个我踩过的坑:
- 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