17. Binder安全机制:Binder通信模型、Binder权限校验、SELinux对Binder的控制

Binder,可以说是Android系统的“任督二脉”。

你想想看,应用要读通讯录、要发通知、要启动一个Activity,哪个不是通过Binder?如果Binder不安全,整个系统就像个筛子。今天我们就来聊聊,Binder到底是怎么保证安全的。

17.1 Binder通信模型:谁在跟谁说话?

先搞清楚Binder的基本模型。说白了,就是三个角色:

  • Client:请求方,比如你的App想读取联系人
  • Server:服务提供方,比如联系人ContentProvider
  • ServiceManager:黄页,帮你找到Server在哪

我刚开始接触Binder时,总觉得它跟Linux的Socket很像。但有个关键区别——Binder只传输一次数据,不需要两次拷贝。这在性能上是个巨大优势。

核心要点:Binder通信中,Client和Server运行在不同进程。数据通过内核态的Binder驱动完成传递,用户态只操作一次。

举个例子。你的App要调用getContentResolver().query(),背后发生了什么?

  1. App(Client)向ServiceManager查询“联系人服务”的句柄
  2. ServiceManager返回一个Binder引用
  3. App通过这个引用,向联系人进程发送请求
  4. 联系人进程处理完,把结果通过Binder驱动返回

嗯,这里要注意:Binder通信是同步的。Client发完请求会阻塞,直到Server返回结果。如果你的Server处理太慢,Client就会ANR。我在项目中就踩过这个坑——一个后台服务在Binder回调里做了网络请求,结果前台App直接卡死。

17.2 Binder权限校验:谁有资格调用?

Binder通信本身是畅通的,但谁都能调用系统服务吗?当然不是。Android在Binder层做了两层校验。

17.2.1 调用方UID校验

每个Binder事务都携带调用方的UID。Server端可以检查这个UID,判断调用方是不是系统进程、是不是某个特定App。

// 在Binder服务端检查调用方UID
public int onTransact(int code, Parcel data, Parcel reply, int flags) {
    int callingUid = Binder.getCallingUid();
    if (callingUid != Process.SYSTEM_UID) {
        throw new SecurityException("只有系统进程才能调用");
    }
    // 继续处理...
}

我记得有一次,一个第三方App试图直接调用系统级的IActivityManager接口。结果呢?UID校验直接把它拦下来了。这就是第一道防线。

17.2.2 权限检查

光有UID还不够。很多服务需要检查调用方是否声明了特定权限。比如,要读取联系人,你的App必须在AndroidManifest里声明READ_CONTACTS权限。

系统服务通常这样检查:

// 在系统服务中检查权限
int pid = Binder.getCallingPid();
int uid = Binder.getCallingUid();
if (checkCallingPermission("android.permission.READ_CONTACTS") 
        != PackageManager.PERMISSION_GRANTED) {
    throw new SecurityException("缺少READ_CONTACTS权限");
}

避坑指南:我曾经遇到一个Case——App明明声明了权限,但调用系统服务时还是报权限拒绝。后来发现,是App的targetSdkVersion太低,系统做了兼容性处理,把权限归类到了“危险权限”组,需要动态申请。所以,不要以为声明了就一定能用

17.3 SELinux对Binder的控制:终极防线

UID和权限检查都是在用户态做的。如果内核态被攻破了呢?或者,如果恶意App直接通过Binder驱动发送伪造的UID呢?

这时候,SELinux就登场了。

17.3.1 SELinux如何拦截Binder通信

SELinux在Binder驱动层加了一道钩子。每次Binder事务经过内核时,SELinux都会检查:

  • 发送方(Source)的SELinux上下文是什么?
  • 接收方(Target)的SELinux上下文是什么?
  • 这次操作(binder_call)是否被允许?

说白了,SELinux给Binder通信加了一个“白名单”。不在白名单里的通信,直接在内核态就被拒绝了,用户态连错误都收不到。

17.3.2 典型的SELinux策略规则

external/sepolicy目录下,你会看到类似这样的规则:

# 允许system_app调用system_server的binder接口
allow system_app system_server:binder_call;

# 允许system_server向untrusted_app发送binder通知
allow system_server untrusted_app:binder_call;

# 禁止untrusted_app调用system_server的敏感接口
neverallow untrusted_app system_server:binder_call;

嗯,这里有个关键点:neverallow规则是强制性的。即使你在用户态把UID校验绕过了,SELinux的neverallow也会在编译期就报错,根本部署不上去。

重要提醒:千万不要随意修改SELinux策略来“解决”权限问题。我见过有人为了省事,直接写了一个allow * *:binder_call;,结果整个系统的隔离性全没了。这等于把防盗门拆了。

17.4 三层防护体系:从用户态到内核态

把上面讲的串起来,Binder安全其实是一个三层模型:

层级 机制 作用 绕过难度
应用层 Android权限模型 检查App是否声明了权限 低(声明即可)
框架层 UID/PID校验 检查调用方身份 中(需提权)
内核层 SELinux策略 强制访问控制 高(需内核漏洞)

我个人习惯把这三层比作“小区门禁”:

  • 第一层:保安问你要门禁卡(权限声明)
  • 第二层:刷卡验证你是谁(UID校验)
  • 第三层:只有白名单上的人才能进电梯(SELinux)

三层都过了,你才能调用到目标服务。少一层都不行。

17.5 实战:如何排查Binder权限问题?

如果你遇到“Permission Denial”或者“SELinux avc: denied”的日志,怎么排查?

  1. 先看logcat:搜索“Permission Denial”或“SecurityException”,确认是哪个服务拒绝了你
  2. 再看dmesg:搜索“avc: denied”,看SELinux有没有拦截。如果有,会告诉你source context和target context
  3. 用audit2allow生成策略:把avc日志喂给audit2allow,它会告诉你需要加什么规则

我的经验:有一次,一个系统服务总是报“Binder transaction failed”。查了半天,发现是SELinux策略里漏了一条binder_call规则。加上之后,问题立刻解决。所以,遇到Binder问题,先查SELinux,别在用户态瞎折腾。

17.6 本章小结

Binder安全不是单一机制,而是一套组合拳:

  • 通信模型决定了谁跟谁说话
  • UID校验和权限检查在用户态把关
  • SELinux在内核态做最后一道防线

你想想看,如果没有SELinux,一个恶意App只要拿到系统签名,就能随意调用任何Binder服务。但有了SELinux,即使它拿到了签名,内核态的策略依然能拦住它。这就是纵深防御的价值。

好了,Binder安全就聊到这里。下一章我们聊聊更底层的安全机制——如果你感兴趣的话。


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