7、Binder安全模型基础:SELinux与Binder的结合、安全上下文检查、binder_selinux.h分析
好,咱们今天聊点硬核的。Binder 的安全模型,说白了就是 Android 系统里的一道「安检门」。你想想看,一个 App 想调用系统服务,比如读取联系人、发送短信,总得有个机制来管一管吧?这个机制,就是 SELinux 和 Binder 的深度结合。
我个人习惯把 Binder 通信比作「跨进程快递」。快递员(Binder 驱动)负责把包裹(数据)从 A 送到 B。但问题是,A 有没有权限寄这个包裹?B 有没有权限收?包裹里有没有违禁品?这些,就是 SELinux 要管的事。
7.1 SELinux 与 Binder 的结合:从「自由放任」到「强制管控」
在 SELinux 引入之前,Android 的权限模型主要靠 UID/GID 来区分。说白了,就是看进程的「身份证号」。但这种方式有个致命缺陷——一旦进程被提权,或者被注入恶意代码,它就能为所欲为。
我记得在 Android 4.x 时代,我帮客户分析过一个提权漏洞。攻击者利用一个系统服务的 UID 漏洞,直接拿到了 system 权限。那时候的防护,真的跟纸糊的一样。
SELinux 的加入,彻底改变了这个局面。它引入了「强制访问控制」(MAC)。什么意思呢?就是系统定义了一套规则,所有进程都必须遵守。哪怕你是 root 用户,也不能违反规则。
在 Binder 通信中,SELinux 主要做了三件事:
- 标记对象:每个 Binder 实体(服务端)和 Binder 代理(客户端)都被打上安全上下文标签。
- 检查权限:在 Binder 事务(transaction)发生时,检查发送方和接收方的安全上下文是否匹配。
- 审计日志:如果检查不通过,记录详细的 avc(Access Vector Cache)拒绝日志。
说白了,SELinux 就是给 Binder 通信装了一个「门禁系统」。每个进程进出,都要刷卡验证。
7.2 安全上下文检查:Binder 事务的「安检流程」
当一个进程通过 Binder 调用另一个进程的服务时,内核里的 Binder 驱动会触发一次安全上下文检查。这个检查发生在 binder_transaction 函数中。
我给大家画个流程图,方便理解:
这个检查的核心,就是比较两个安全上下文。安全上下文长什么样呢?举个例子:
u:r:untrusted_app:s0:c15,c256,c513,c768
这个字符串包含四个部分:
- u:用户(user),Android 里统一是 u。
- r:角色(role),一般也是固定的。
- untrusted_app:类型(type),这是最关键的部分,决定了进程的权限。
- s0:c15,c256,...:安全级别(MLS),用于多级安全策略。
检查时,Binder 驱动会调用 selinux_android_binder_* 系列函数。这些函数会查询 SELinux 策略数据库,判断「发送方类型」是否有权限向「接收方类型」发送 Binder 调用。
核心逻辑:检查的是「发送方」对「接收方」的 binder_call 权限。不是检查接收方能不能收,而是检查发送方有没有资格发。
7.3 binder_selinux.h 分析:安全模型的「源代码」
好,咱们来看看具体的代码。文件 binder_selinux.h 位于内核源码的 drivers/android/ 目录下。这个文件虽然不大,但它是 Binder 安全模型的「心脏」。
我打开这个文件,第一眼看到的就是几个关键函数声明:
// 检查 Binder 节点(服务端)的安全上下文
int selinux_binder_init_secctx(struct binder_node *node);
// 检查 Binder 事务的安全上下文
int selinux_binder_transaction(struct binder_proc *sender,
struct binder_proc *target,
struct binder_transaction *t);
// 检查 Binder 传输(transfer)的安全上下文
int selinux_binder_transfer(struct binder_proc *from,
struct binder_proc *to,
struct binder_node *node);
这三个函数,覆盖了 Binder 通信的三个关键环节:
| 函数名 | 检查时机 | 检查内容 |
|---|---|---|
selinux_binder_init_secctx |
服务端注册 Binder 服务时 | 服务端进程是否有权注册该服务 |
selinux_binder_transaction |
客户端发起 Binder 调用时 | 客户端是否有权调用该服务 |
selinux_binder_transfer |
Binder 对象跨进程传递时 | 源进程是否有权将对象传给目标进程 |
我曾经在分析一个系统服务漏洞时,就卡在了 selinux_binder_transfer 这个函数上。当时有个恶意 App 通过 Binder 把一个文件描述符传给了另一个高权限进程。按理说这是不允许的,但为什么没被拦截?
后来我发现,问题出在安全上下文的配置上。那个文件描述符的标签被错误地设置成了 u:r:system_file:s0,导致 SELinux 策略认为它是系统文件,允许了传输。嗯,这就是典型的「标签错误导致安全漏洞」。
7.4 安全上下文的传递与继承
在 Binder 通信中,安全上下文不是一成不变的。它会在进程间传递,甚至被继承。举个例子:
当一个 App 通过 Binder 调用 ActivityManagerService 时,AMS 会代表 App 执行一些操作。这时候,AMS 的安全上下文是 system_server,而不是 App 的 untrusted_app。
这就引出一个问题:AMS 会不会「滥用」自己的权限?
答案是:不会。因为 SELinux 策略中定义了「域转换」(domain transition)规则。AMS 在执行 App 请求的操作时,会临时切换到 App 的安全上下文,或者使用一个中间上下文。这就是所谓的「SELinux 上下文切换」。
避坑指南:我曾经在开发一个自定义系统服务时,忘记配置域转换规则。结果服务端进程一直用自己的高权限上下文执行操作,导致 SELinux 审计日志里全是拒绝记录。后来我加了一条 type_transition 规则,才解决问题。
7.5 实战:如何查看和调试安全上下文
在实际开发中,我们经常需要查看进程的安全上下文。这里有几个常用命令:
# 查看某个进程的安全上下文
ps -Z | grep com.example.app
# 查看 Binder 节点的安全上下文
ls -Z /dev/binder
# 查看 SELinux 策略中关于 Binder 的规则
sesearch -A -s untrusted_app -t system_server -c binder -p call
如果遇到 Binder 调用被拒绝的情况,第一件事就是看 dmesg 或 logcat 中的 avc 日志:
avc: denied { call } for pid=1234 comm="app_process"
scontext=u:r:untrusted_app:s0
tcontext=u:r:system_server:s0
tclass=binder
这条日志告诉我们:untrusted_app 想调用 system_server 的 Binder 服务,但被拒绝了。原因就是缺少 call 权限。
注意:avc 日志中的 scontext 是发送方,tcontext 是接收方。很多人会搞反,以为 scontext 是服务端。其实 s 代表 source(源),t 代表 target(目标)。
7.6 小结:Binder 安全模型的精髓
说了这么多,其实核心就一句话:SELinux 给 Binder 通信加了一把「锁」,这把锁的钥匙就是安全上下文。
我个人觉得,理解 Binder 安全模型的关键,不在于记住那些函数名,而在于理解「谁可以访问谁」这个逻辑。每次看到 avc 拒绝日志,我都会问自己三个问题:
- 发送方是谁?它的安全上下文是什么?
- 接收方是谁?它的安全上下文是什么?
- 策略中是否允许这种访问?
把这三个问题搞清楚了,Binder 安全模型也就掌握得差不多了。
嗯,最后提醒一句:别小看 binder_selinux.h 这个文件。虽然它只有几百行代码,但它是整个 Android 系统服务安全的基石。我建议你把它打印出来,贴在工位上,没事就翻翻。