16、Binder与SELinux策略绕过:SELinux策略编写缺陷、binder_call权限绕过、avc_audit日志分析
好,咱们今天聊点硬核的。Binder 和 SELinux 的组合,说白了就是 Android 安全体系里的「门锁」和「监控摄像头」。Binder 负责开门关门,SELinux 盯着谁该进谁不该进。但这两者一旦配合出问题,漏洞就来了。
我在做系统安全审计时,见过太多「策略写得太松」或者「Binder 调用没校验」导致的提权案例。今天我就把这块的坑,一个一个给你掰开讲。
16.1 SELinux 策略编写中的常见缺陷
SELinux 策略文件(.te 文件)是定义进程权限的核心。但很多开发者写策略时,容易犯几个低级错误。
16.1.1 过度授权的 allow 规则
最常见的问题就是「图省事,直接给了大权限」。比如:
allow system_app untrusted_app:file { read write open };
这条规则允许 system_app 对 untrusted_app 的文件进行读写。你想想看,这等于把自家保险柜的钥匙给了陌生人。我遇到过某厂商的预装应用,就因为这种规则被第三方应用提权,直接读取了通讯录数据库。
allow * system_server:binder { call };。这种通配符写法,等于把大门敞开了。
16.1.2 缺失的 neverallow 规则
neverallow 是 SELinux 的「底线规则」。但很多厂商为了兼容性,会故意删掉或放宽这些规则。比如:
neverallow { domain -system_app } system_server:binder { call };
这条规则禁止非 system_app 域调用 system_server 的 Binder。如果删掉它,任何第三方应用都能直接跟系统服务通信。嗯,这里要注意,这种漏洞在 Android 8 之前特别常见。
16.1.3 类型转换(type_transition)配置错误
类型转换用于控制进程创建子进程时的域切换。如果配置不当,可能导致子进程继承父进程的高权限域。
type_transition init_t shell_exec:process shell_t;
这条规则允许 init 域执行 shell 二进制时切换到 shell_t 域。但如果某个恶意进程伪造了 shell 二进制,就能从低权限域跳到高权限域。我见过一个案例,攻击者利用这个漏洞从普通应用直接拿到了 root 权限。
16.2 binder_call 权限绕过分析
Binder 是 Android 进程间通信的核心。SELinux 通过 binder_call 权限控制哪个进程能调用哪个服务的 Binder 接口。但绕过方式也不少。
16.2.1 利用服务端未校验调用方身份
有些系统服务在收到 Binder 请求后,不会二次校验调用方的 UID 或 PID。即使 SELinux 策略正确,服务端代码也可能存在漏洞。
// 错误示例:未校验调用方身份
public void onTransact(int code, Parcel data, Parcel reply, int flags) {
String action = data.readString();
// 直接执行高危操作
executeAction(action);
}
正确的做法是:
// 正确示例:校验调用方 UID
public void onTransact(int code, Parcel data, Parcel reply, int flags) {
int callingUid = Binder.getCallingUid();
if (callingUid != Process.SYSTEM_UID) {
throw new SecurityException("Unauthorized caller");
}
String action = data.readString();
executeAction(action);
}
16.2.2 利用 Binder 上下文对象(Binder Context Object)
Binder 允许服务端返回一个 Binder 对象给客户端。如果这个对象没有正确设置 SELinux 上下文,客户端就能绕过策略限制。
// 服务端返回一个未设置上下文的 Binder 对象
IBinder binder = new MyBinder();
reply.writeStrongBinder(binder);
正确的做法是:
// 设置 Binder 对象的 SELinux 上下文
IBinder binder = new MyBinder();
binder.setSELinuxContext("u:object_r:my_service:s0");
reply.writeStrongBinder(binder);
我见过一个案例,某个系统服务返回了一个 Binder 对象,但没设置上下文。攻击者拿到这个对象后,就能调用原本被 SELinux 禁止的方法。
16.3 avc_audit 日志分析实战
avc_audit 日志是 SELinux 的「黑匣子」。每次权限被拒绝,都会生成一条日志。学会看这些日志,是定位漏洞的关键。
16.3.1 日志格式解析
典型的 avc_audit 日志长这样:
avc: denied { call } for pid=1234 comm="app_process" scontext=u:r:untrusted_app:s0 tcontext=u:r:system_server:s0 tclass=binder
拆开来看:
| 字段 | 含义 | 示例值 |
|---|---|---|
| denied | 被拒绝的操作 | { call } |
| pid | 发起请求的进程 ID | 1234 |
| comm | 发起请求的进程名 | app_process |
| scontext | 源安全上下文(调用方) | u:r:untrusted_app:s0 |
| tcontext | 目标安全上下文(被调用方) | u:r:system_server:s0 |
| tclass | 对象类别 | binder |
16.3.2 如何从日志中定位策略缺陷
假设你看到这样一条日志:
avc: denied { read write } for pid=5678 comm="system_app" scontext=u:r:system_app:s0 tcontext=u:r:untrusted_app_file:s0 tclass=file
这条日志说明 system_app 试图读写 untrusted_app_file 类型的文件。如果这个操作是正常的业务需求,那就需要在策略中添加 allow 规则。但如果这个操作不应该发生,那就说明存在权限滥用。
audit2allow 工具自动生成需要的 allow 规则。但注意,生产环境一定要切回 enforcing 模式。
16.3.3 利用 avc_audit 发现绕过攻击
当攻击者尝试绕过 SELinux 策略时,avc_audit 日志会留下痕迹。比如:
avc: denied { call } for pid=9999 comm="malicious_app" scontext=u:r:untrusted_app:s0 tcontext=u:r:system_server:s0 tclass=binder
这条日志表明 untrusted_app 试图调用 system_server 的 Binder 接口。如果这个调用被 SELinux 拒绝,说明策略是有效的。但如果日志中出现了大量类似的 denied 记录,那就说明有人在尝试暴力破解。
我遇到过一种情况:攻击者通过修改 Binder 事务的上下文对象,让 SELinux 误判调用方的身份。这种情况下,avc_audit 日志中会出现 scontext 和实际进程不匹配的记录。比如:
avc: denied { call } for pid=9999 comm="malicious_app" scontext=u:r:system_app:s0 tcontext=u:r:system_server:s0 tclass=binder
注意看,scontext 是 system_app,但 comm 却是 malicious_app。这就是典型的上下文伪造攻击。
16.4 知识体系总览
下面这张图总结了 Binder 与 SELinux 策略绕过的核心逻辑:
这张图展示了从攻击入口到最终防御的完整链路。你可以看到,SELinux 策略缺陷、Binder 权限绕过和日志分析这三块是环环相扣的。任何一个环节出问题,都可能导致整个安全体系崩溃。
- SELinux 策略要遵循最小权限原则,别图省事写通配符
- Binder 服务端必须校验调用方身份,别依赖 SELinux 做唯一防线
- avc_audit 日志是发现攻击的重要线索,别忽视任何一条 denied 记录
好了,这一章的内容就到这儿。记住,安全是一个系统工程,Binder 和 SELinux 只是其中的两个环节。但把这两个环节守住了,大部分提权攻击就挡在门外了。