26、Binder与多用户隔离:多用户场景下的Binder安全、用户ID隔离、跨用户Binder调用限制

多用户隔离这个话题,说实话,在Android早期版本里并没有被足够重视。我记得刚接触Android 4.x时代的多用户功能时,第一反应是——这玩意儿真的安全吗?后来深入看了Binder层的实现,才明白Google在这块下了不少功夫。

今天我们就聊聊多用户场景下Binder是怎么做隔离的。说白了,就是不同用户之间不能随便互相调用对方的服务,不能互相看对方的文件,更不能互相提权。

用户ID隔离:Binder通信的第一道防线

Android里每个用户都有一个独立的User ID(UID)。你想想看,系统怎么知道一个Binder调用是来自哪个用户的?答案就在Binder驱动层。

每个进程在创建Binder节点时,内核会记录它的UID。当另一个进程发起Binder调用时,驱动会检查调用方的UID和目标服务的UID是否匹配。如果不匹配,嗯,直接返回权限错误。

核心机制:Binder驱动在底层维护了每个进程的UID信息,跨用户调用时,驱动会进行UID校验。这是最底层的隔离手段,应用层无法绕过。

我在项目中遇到过一个问题:某个系统服务需要被所有用户访问,但默认的UID检查会阻止跨用户调用。当时我查了半天,发现解决方案是使用android:singleUser属性,或者通过Context.bindServiceAsUser()显式指定目标用户。

跨用户Binder调用的限制策略

Android对跨用户Binder调用做了三层限制,我习惯称之为“三明治隔离模型”:

  1. 驱动层限制:Binder驱动直接拒绝不同UID的节点访问,除非目标服务显式声明了跨用户访问权限。
  2. 服务层限制:系统服务(如ActivityManagerService)会检查调用方的UserHandle,只有特定服务才允许跨用户操作。
  3. 应用层限制:应用通过Binder调用系统服务时,系统服务会二次校验调用方的用户身份。

避坑指南:我曾经在开发一个多用户管理应用时,发现通过Binder调用IUserManager接口时,明明代码逻辑没问题,但就是返回权限错误。后来才发现,原来系统服务内部会检查调用进程的UID是否属于系统用户(User 0)。非系统用户的应用,即使有权限声明,也无法直接调用某些敏感接口。

Binder令牌机制:用户身份的传递

跨用户调用时,光靠UID检查还不够。为什么?因为一个服务可能被多个用户调用,服务端需要知道当前请求来自哪个用户。这时候就需要Binder令牌(Token)机制了。

具体来说,系统服务在收到Binder调用时,会通过Binder.getCallingUid()获取调用方的UID,然后通过UserHandle.getUserId(uid)解析出用户ID。但这里有个坑——如果调用方是通过代理方式发起的请求,UID可能已经被篡改?

嗯,不用担心。Binder驱动在传递调用方身份时,使用的是内核级别的信任链。调用方无法伪造自己的UID,因为UID是在驱动层由内核设置的,应用层无法修改。

// 系统服务中常见的用户身份校验代码
int callingUid = Binder.getCallingUid();
int userId = UserHandle.getUserId(callingUid);

// 检查当前用户是否允许执行该操作
if (userId != UserHandle.myUserId()) {
    throw new SecurityException("Cross-user operation not allowed");
}

多用户场景下的Binder服务注册

每个用户都有自己的服务实例吗?不一定。这取决于服务的配置方式。

服务类型 行为 跨用户访问
单例服务(默认) 所有用户共享同一个服务实例 需要显式权限
按用户实例化服务 每个用户有独立的服务实例 默认不允许
系统级服务 运行在系统用户(User 0) 通过代理访问

我记得有一次调试一个奇怪的问题:用户A启动了一个后台服务,切换到用户B后,服务还在运行,但用户B无法通过Binder连接到这个服务。原因就是服务注册时绑定了用户A的上下文,Binder驱动在连接时会校验用户身份。

跨用户Binder调用的安全风险

虽然Android做了多层隔离,但跨用户Binder调用仍然存在一些风险点:

  • 信息泄露:如果某个系统服务没有正确校验调用方用户,可能导致用户A的数据被用户B读取。
  • 权限提升:恶意应用可能通过跨用户Binder调用,利用系统服务的漏洞获取更高权限。
  • 服务劫持:如果某个跨用户服务没有绑定签名,可能被其他用户的应用劫持。

警告:千万不要在自定义Binder服务中直接信任getCallingUid()返回的值就完事了。我见过一个案例,某个应用通过bindServiceAsUser()将Binder连接绑定到其他用户的服务,然后利用服务端的校验漏洞,绕过了用户隔离。正确的做法是:在服务端显式检查UserHandle,并且对敏感操作增加二次确认。

多用户Binder隔离的架构图

下面这张图展示了多用户场景下Binder隔离的整体架构。你可以看到,从应用层到驱动层,每一层都在做用户身份的校验和隔离。

多用户Binder隔离架构 应用层 用户A应用 (UID=10010) | 用户B应用 (UID=10011) | 系统应用 (UID=1000) 系统服务层 ActivityManagerService | WindowManagerService | PackageManagerService Binder驱动层 UID校验 | 用户身份传递 | 跨用户访问控制 Linux内核层 进程隔离 | 文件系统权限 | SELinux策略 隔离机制 UID隔离 用户令牌 服务校验 SELinux 进程隔离

实际案例分析:跨用户Binder调用漏洞

讲个真实案例。几年前我分析过一个漏洞,某个系统服务在处理Binder请求时,没有正确校验调用方的用户身份。攻击者可以在用户B中,通过Binder调用这个服务,然后利用服务端的逻辑缺陷,读取用户A的私有数据。

具体来说,这个服务提供了一个“获取用户配置”的接口,内部实现是:

// 有漏洞的代码
public Bundle getUserConfig(int userId) {
    // 没有校验调用方用户身份!
    return readConfigFromFile(userId);
}

修复方式很简单,加上用户身份校验:

// 修复后的代码
public Bundle getUserConfig(int userId) {
    int callingUserId = UserHandle.getUserId(Binder.getCallingUid());
    if (callingUserId != userId) {
        throw new SecurityException("Cannot access other user's config");
    }
    return readConfigFromFile(userId);
}

你看,就这么一行校验,就能堵住一个严重的信息泄露漏洞。所以我在做安全审计时,一定会检查每个Binder接口是否做了用户身份校验。

总结

多用户场景下的Binder安全,核心就是一句话:永远不要信任调用方的身份声明,要在服务端做二次校验。Binder驱动虽然提供了底层的UID隔离,但应用层和服务层的校验同样重要。

我个人习惯是在每个Binder接口的入口处,都加上用户身份校验逻辑。虽然看起来有点冗余,但安全这东西,多一道防线总比少一道好。你想想看,如果因为少写了一行校验代码,导致用户数据泄露,那代价可就大了。

小技巧:如果你在开发多用户环境下的系统服务,建议使用Context.bindServiceAsUser()来显式指定目标用户,而不是依赖默认的跨用户行为。这样代码意图更清晰,也更容易做安全审计。

好了,关于多用户场景下的Binder隔离,今天就聊到这里。记住,安全不是靠单一机制实现的,而是多层防御的结果。从驱动层到应用层,每一层都要做好自己的那份工作。

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