17、Binder服务端漏洞:系统服务中的Binder接口暴露、权限检查缺失、服务端提权案例

好,咱们今天聊一个实战性很强的话题——Binder服务端漏洞。说实话,我在做系统安全审计的时候,这类问题见得最多。很多厂商的ROM,甚至原生AOSP里,都曾出现过类似的问题。

你想想看,Android系统里有上百个系统服务,每个服务都通过Binder对外暴露接口。这些接口,说白了就是攻击者眼中的“后门”。如果权限检查没做好,那后果不堪设想。

17.1 Binder接口暴露:谁在监听你的服务?

每个系统服务都会在ServiceManager中注册一个名称。比如windowactivitypackage这些。第三方应用可以通过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);
}

你看,setSystemPropertyinstallPackage这些方法,明显是系统级操作。但因为它和公开方法写在同一个AIDL里,任何应用只要拿到这个Binder代理,就能直接调用。

我记得有一次,我在分析某厂商的ROM时,发现他们的PowerManagerService里有个setCpuFreq()方法,居然没做任何权限校验。普通应用调用后,CPU频率直接被改,手机瞬间卡死。嗯,这就是典型的接口暴露问题。

17.2 权限检查缺失:谁在调用你的方法?

接口暴露了,但如果服务端在方法入口做了严格的权限检查,其实问题也不大。但现实是——很多开发者忘了加权限检查

Android系统服务中,权限检查通常有两种方式:

  1. 静态权限检查:在AndroidManifest.xml中声明<uses-permission>,服务端通过checkCallingPermission()校验。
  2. 动态权限检查:运行时通过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相关的包名。这个方法没有做权限检查。

攻击路径:

  1. 普通应用调用getService("vpn")拿到Binder代理。
  2. 调用setVpnPackage()方法,传入一个恶意包的路径。
  3. 服务端内部,这个方法会以system权限去加载并执行这个包里的代码。
  4. 恶意代码获得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服务端漏洞 接口暴露 权限检查缺失 服务端提权案例 AIDL未区分公开/内部 ServiceManager注册过多 Binder代理可被获取 未调用checkCallingPermission 未校验getCallingUid 未降权执行 VpnManagerService漏洞 setVpnPackage未校验 以system权限执行 防御:接口最小化 + 权限校验 + 降权执行 + 日志审计

这张图把Binder服务端漏洞的三个核心方向都串起来了。你写代码时,可以对照着检查自己的服务有没有类似问题。

最后说一句:Binder服务端漏洞,说白了就是“信任”的问题。服务端太信任调用者,没有做校验,结果被攻击者钻了空子。咱们做系统安全,一定要保持“零信任”的心态——永远不要相信调用者,永远要做校验


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