18. 系统服务安全:系统服务注册与访问控制、服务端权限校验
系统服务,说白了就是 Android 系统的“内脏”。你想想看,从你解锁手机到打开一个 App,背后有多少服务在跑?ActivityManagerService、PackageManagerService、WindowManagerService……这些服务一旦被攻破,整个系统就裸奔了。
我个人习惯把系统服务安全拆成两个层面来看:注册与访问控制(谁能找到服务、谁能连上服务)和服务端权限校验(连上之后,你能干什么)。今天我们就来聊聊这两个核心问题。
18.1 系统服务的注册机制
在 Android 里,系统服务不是你想注册就能注册的。它们通过 ServiceManager 统一管理。我刚开始研究这部分源码时,第一反应是:这不就是个全局注册表吗?嗯,确实有点像,但它的安全设计要精巧得多。
服务的注册流程大致如下:
- 系统服务进程(如 system_server)调用
ServiceManager.addService() - ServiceManager 检查调用者的 UID 和权限
- 只有系统级进程(UID 为 SYSTEM_UID 或具有系统签名)才能注册服务
- 注册成功后,服务名称和 Binder 引用被存入内部哈希表
关键点:ServiceManager 本身运行在一个独立的进程中(servicemanager),它只接受来自系统分区的 Binder 调用。普通 App 根本没法直接跟它通信——这是第一道防线。
我曾经在分析一个定制 ROM 时发现,厂商为了“方便”,把某个关键服务的注册权限放宽了。结果第三方 App 注册了一个同名的假服务,拦截了所有系统调用。嗯,那场面……相当刺激。
18.2 服务的访问控制
服务注册好了,谁来用?这就涉及到访问控制。Android 提供了两种机制:
18.2.1 基于 UID 的粗粒度控制
ServiceManager 在 getService() 时,默认不做权限检查。也就是说,任何进程都能查到某个服务是否存在,也能拿到它的 Binder 引用。但拿到引用不等于能调用——真正的校验在服务端。
不过,从 Android 8.0 开始,Google 引入了 服务白名单机制。只有白名单内的进程才能通过 ServiceManager 获取某些敏感服务的引用。我记得当时看 commit 记录时,注释写的是“Prevent untrusted apps from accessing system services”。
18.2.2 基于 SELinux 的强制访问控制
这才是真正的硬核手段。SELinux 策略会为每个服务定义明确的访问规则。举个例子:
# 允许 system_app 域访问 window 服务
allow system_app window_service:service_manager find;
# 允许 shell 域访问 activity 服务
allow shell activity_service:service_manager find;
如果某个进程的域(domain)没有对应的 service_manager:find 权限,它连服务的 Binder 引用都拿不到。这比在代码里做 if-else 检查要可靠得多——因为 SELinux 在内核层面就拦截了。
我的建议:在开发系统服务时,优先用 SELinux 策略做访问控制,代码里的权限校验作为第二道防线。两者配合,基本能挡住 99% 的攻击。
18.3 服务端的权限校验
好,现在客户端拿到了 Binder 引用,开始调用服务方法。服务端怎么知道这个调用是合法的?
Android 的 Binder 驱动会在每次跨进程调用时,自动传递调用者的 UID 和 PID。服务端可以通过 Binder.getCallingUid() 和 Binder.getCallingPid() 获取这些信息。然后呢?
18.3.1 权限检查的常见模式
我总结了三种最常见的校验模式:
| 模式 | 实现方式 | 适用场景 |
|---|---|---|
| UID 比对 | 直接检查 getCallingUid() 是否等于特定值 |
只允许系统进程调用的接口 |
| 权限声明 | 调用 checkCallingPermission() 检查 Manifest 权限 |
需要用户授权的敏感操作 |
| 签名验证 | 比对调用者 APK 的签名是否与系统签名一致 | 系统级 API,只对同签名应用开放 |
举个例子,ActivityManagerService 的 startActivity() 方法:
// 简化后的代码
public int startActivity(IApplicationThread caller, Intent intent, ...) {
// 1. 检查调用者是否有 START_ACTIVITIES_FROM_BACKGROUND 权限
if (intent.hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)) {
String permission = (callingUid == Process.SYSTEM_UID)
? null : android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND;
if (permission != null) {
enforceCallingPermission(permission, "startActivity()");
}
}
// 2. 检查 intent 的目标组件是否允许被外部调用
// 3. 检查调用者是否有启动该 Activity 的权限
// ...
}
你看,一个简单的启动 Activity 操作,背后有三四层权限校验。我当年第一次看这段代码时,心里想的是:这也太谨慎了吧?后来遇到一个漏洞——某个服务忘记在 onTransact() 里做权限检查,结果被恶意 App 利用来静默安装应用——我才明白,这种“过度谨慎”是有道理的。
18.3.2 常见的坑:忘记校验调用者身份
这里我要特别提醒一个坑:不要相信 Binder 传递过来的任何“自称”的身份信息。
什么意思?有些开发者会在 Binder 接口的参数里传递一个 UID 或包名,然后在服务端直接用这个值做权限判断。这是极其危险的!因为攻击者可以伪造这些参数。
我曾经在一个第三方 ROM 里看到这样的代码:服务端从 Bundle 参数里读取 callerUid,然后判断是否等于 1000(SYSTEM_UID)。攻击者只需要在调用时传一个假的 callerUid 就能绕过所有检查。正确的做法是始终使用 Binder.getCallingUid()——这个值由内核填充,无法伪造。
18.4 系统服务安全架构总览
说了这么多,我们来画一张图,把整个体系串起来:
18.5 实战:如何安全地开发一个系统服务
如果你正在开发一个系统服务,我建议你按这个 checklist 来:
- 注册时设置权限:在
addService()时指定允许访问的权限名称 - 编写 SELinux 策略:为你的服务定义明确的
service_manager规则 - 每个公开方法都做校验:不要假设调用者是可信的,哪怕它来自系统进程
- 使用
enforceCallingPermission():而不是手动解析权限字符串 - 记录审计日志:对敏感操作记录调用者的 UID、PID 和时间戳
一个小技巧:在开发阶段,可以在服务方法入口处加一行 Log.d(TAG, "caller=" + Binder.getCallingUid())。这样调试时能清楚看到谁在调用你的服务。我曾经靠这个日志抓到一个偷偷调用系统 API 的恶意 App。
好了,关于系统服务安全,核心就是这三件事:注册要控、访问要管、调用要验。把这三点做到位,你的系统服务基本就稳了。
公众号:蓝海资料掘金营,微信deep3321