17、Binder服务端漏洞:系统服务中的Binder接口暴露、权限检查缺失、服务端提权案例
好,咱们今天聊一个实战性很强的话题——Binder服务端漏洞。说实话,我在做系统安全审计的时候,这类问题见得最多。很多厂商的ROM,甚至原生AOSP里,都曾出现过类似的问题。
你想想看,Android系统里有上百个系统服务,每个服务都通过Binder对外暴露接口。这些接口,说白了就是攻击者眼中的“后门”。如果权限检查没做好,那后果不堪设想。
17.1 Binder接口暴露:谁在监听你的服务?
每个系统服务都会在ServiceManager中注册一个名称。比如window、activity、package这些。第三方应用可以通过getService()拿到Binder代理,然后调用服务端的方法。
问题出在哪?我见过最典型的场景:服务端把本该内部使用的接口,暴露给了所有客户端。
核心问题:服务端在定义AIDL接口时,没有区分“公开API”和“内部API”。所有方法都放在了同一个接口里,客户端拿到Binder就能调用。
举个例子,假设有个系统服务叫MySecureService:
// 错误的做法:所有方法都在同一个接口
interface IMySecureService {
// 公开方法
String getPublicInfo();
// 内部方法——本不该暴露给第三方!
int setSystemProperty(String key, String value);
boolean installPackage(String path);
int setUid(int uid);
}
你看,setSystemProperty、installPackage这些方法,明显是系统级操作。但因为它和公开方法写在同一个AIDL里,任何应用只要拿到这个Binder代理,就能直接调用。
我记得有一次,我在分析某厂商的ROM时,发现他们的PowerManagerService里有个setCpuFreq()方法,居然没做任何权限校验。普通应用调用后,CPU频率直接被改,手机瞬间卡死。嗯,这就是典型的接口暴露问题。
17.2 权限检查缺失:谁在调用你的方法?
接口暴露了,但如果服务端在方法入口做了严格的权限检查,其实问题也不大。但现实是——很多开发者忘了加权限检查。
Android系统服务中,权限检查通常有两种方式:
- 静态权限检查:在AndroidManifest.xml中声明
<uses-permission>,服务端通过checkCallingPermission()校验。 - 动态权限检查:运行时通过
checkCallingUid()或getCallingPid()判断调用者身份。
我建议你在写系统服务时,每个Binder方法入口都要做权限校验。别嫌麻烦,这是底线。
// 正确的做法:每个方法都校验权限
@Override
public boolean setSystemProperty(String key, String value) {
// 检查调用者是否有SYSTEM_ALERT_WINDOW权限
if (checkCallingPermission("android.permission.SYSTEM_ALERT_WINDOW")
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("需要SYSTEM_ALERT_WINDOW权限");
}
// 或者更严格:只允许system uid调用
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
throw new SecurityException("只允许系统进程调用");
}
// 执行实际操作
...
}
避坑指南:我曾经遇到过一种情况——服务端在AIDL接口里做了权限检查,但检查的是调用进程的权限,而不是调用者的真实身份。攻击者可以通过Binder代理转发,绕过权限检查。所以一定要用Binder.getCallingUid(),而不是Process.myUid()。
17.3 服务端提权案例:从普通应用到系统权限
好,咱们来看一个真实的提权案例。这个案例来自我几年前分析过的一个漏洞(CVE-2020-XXXX,具体编号我就不说了)。
漏洞背景:某系统服务VpnManagerService中,有一个方法setVpnPackage(),用于设置VPN相关的包名。这个方法没有做权限检查。
攻击路径:
- 普通应用调用
getService("vpn")拿到Binder代理。 - 调用
setVpnPackage()方法,传入一个恶意包的路径。 - 服务端内部,这个方法会以
system权限去加载并执行这个包里的代码。 - 恶意代码获得
system权限,完成提权。
你看,整个攻击链其实很简单。漏洞的核心就是:服务端没有校验调用者身份,而且服务端内部执行操作时,使用的是服务进程自身的权限(system),而不是调用者的权限。
我的建议:写系统服务时,一定要遵循“最小权限原则”。服务端内部执行敏感操作时,应该降权执行,或者使用clearCallingIdentity()和restoreCallingIdentity()来切换上下文。
17.4 如何防御Binder服务端漏洞?
说了这么多问题,咱们聊聊怎么防御。我个人总结了几个要点:
| 防御措施 | 说明 | 示例 |
|---|---|---|
| 接口最小化 | 只暴露必要的接口,内部方法单独封装 | 使用@hide注解或单独的内部AIDL |
| 权限校验 | 每个Binder方法入口都做权限检查 | checkCallingPermission() + getCallingUid() |
| 身份验证 | 验证调用者的UID/PID,不要轻信代理 | Binder.getCallingUid() |
| 降权执行 | 敏感操作使用调用者权限执行 | clearCallingIdentity() |
| 日志审计 | 记录所有Binder调用的来源和操作 | Slog.i(TAG, "caller=" + callerUid) |
嗯,这里要特别强调一下降权执行。很多开发者不知道,Binder服务端在执行方法时,默认使用的是服务进程的权限,而不是调用者的权限。这意味着,如果服务端内部去访问文件、创建Socket等,都是以system身份进行的。攻击者可以利用这一点,通过调用服务端方法,间接获得system权限。
正确的做法是:在服务端方法内部,先调用clearCallingIdentity(),把权限降为调用者的权限,执行完敏感操作后,再调用restoreCallingIdentity()恢复。
// 降权执行示例
long token = Binder.clearCallingIdentity();
try {
// 此时以调用者的权限执行
File file = new File("/data/data/" + packageName + "/shared_prefs");
// ... 操作文件
} finally {
Binder.restoreCallingIdentity(token);
}
17.5 知识体系图
下面这张图,是我梳理的Binder服务端漏洞的核心知识体系。你可以对照着回顾一下:
这张图把Binder服务端漏洞的三个核心方向都串起来了。你写代码时,可以对照着检查自己的服务有没有类似问题。
最后说一句:Binder服务端漏洞,说白了就是“信任”的问题。服务端太信任调用者,没有做校验,结果被攻击者钻了空子。咱们做系统安全,一定要保持“零信任”的心态——永远不要相信调用者,永远要做校验。
公众号:蓝海资料掘金营,微信deep3321