15、服务SELinux策略:service_contexts编写,服务启动与通信权限控制,Binder服务SELinux策略
好,咱们今天聊点硬核的。Binder 服务在 Android 系统里,就像人体的神经网络。它负责把各个模块串联起来,让 App 能调用系统服务,让系统服务之间也能互相通信。
但问题来了——如果这个神经网络被恶意攻击了怎么办?比如一个恶意 App 伪装成系统服务,窃取你的通讯录?
嗯,这就是 SELinux 要干的事。它给每个 Binder 服务都贴上了「身份证」和「通行证」。只有身份对、权限够的进程,才能访问对应的服务。
我个人习惯把这一章分成三块来讲:
- 第一块:service_contexts 文件怎么写,它决定了服务叫什么名字、属于哪个安全上下文。
- 第二块:服务启动时,SELinux 怎么检查权限,怎么控制通信。
- 第三块:Binder 通信的 SELinux 策略,这是最容易被忽略但最容易出坑的地方。
15.1 service_contexts 文件:服务的「身份证」
先问一个问题:当系统服务 manager(比如 servicemanager)收到一个请求,说「我要找 activity」——它怎么知道这个「activity」服务是谁?
答案就在 service_contexts 文件里。这个文件定义了一个映射表:服务名称 → SELinux 上下文。
举个例子,在 Android 10 以上的设备上,你可以在 /system/etc/selinux/ 目录下找到它。内容大概长这样:
# service_contexts 示例
activity u:object_r:activity_service:s0
package u:object_r:package_service:s0
window u:object_r:window_service:s0
batterystats u:object_r:batterystats_service:s0
每一行左边是服务注册时的名字,右边是它的 SELinux 安全上下文。注意看,这里的类型都是 _service 结尾。这是我个人习惯的命名方式,方便一眼看出这是 Binder 服务。
核心要点:service_contexts 文件是 servicemanager 进行访问控制的第一道关卡。当一个进程尝试获取某个服务时,servicemanager 会先查这个文件,拿到目标服务的上下文,然后检查发起者是否有权限。
我在项目中遇到过一个问题:有个第三方服务注册时名字写错了大小写,结果 service_contexts 里匹配不上,servicemanager 直接返回 null。排查了半天,最后发现是大小写敏感的问题。嗯,这种坑踩过一次就不会忘了。
15.2 服务启动与通信权限控制
服务启动时,SELinux 会做两件事:
- 检查启动者是否有权限向 servicemanager 注册该服务。
- 检查服务进程自身的域(domain)是否允许它运行。
说白了,就是「谁生的孩子谁负责」。系统服务通常由 init 进程启动,它的域是 init。如果 init 想启动一个 system_server 服务,SELinux 会检查 init 域是否有权限 transition 到 system_server 域。
对应的策略写法是这样的:
# 允许 init 域 transition 到 system_server 域
type_transition init system_server_exec:process system_server;
# 允许 system_server 域向 servicemanager 注册服务
allow system_server servicemanager:binder { transfer call };
allow system_server activity_service:service_manager { add };
你想想看,如果没有第二行 allow 规则,就算 system_server 进程跑起来了,它也没法把自己的服务注册到 servicemanager 里。系统会直接报 Permission denied,然后服务注册失败。
避坑指南:我曾经在调试一个系统服务时,发现服务进程已经起来了,但就是注册不到 servicemanager。查了半天的 log,发现 avc 拒绝信息里写着 denied { add } for service=my_service scontext=u:r:my_service:s0 tcontext=u:object_r:my_service_service:s0 tclass=service_manager。加上对应的 allow 规则后,问题解决。
15.3 Binder 服务 SELinux 策略
Binder 通信的 SELinux 策略,其实比 service_manager 的检查更细粒度。它分为三个层面:
| 层面 | 检查内容 | 对应策略类 |
|---|---|---|
| Binder 节点 | 是否允许打开/使用 binder 设备 | binder |
| Binder 调用 | 是否允许跨进程调用(call/transfer) | binder |
| 服务查找 | 是否允许查找/获取某个服务 | service_manager |
举个例子,假设 App 进程(域为 untrusted_app)想调用 activity_service:
# 第一步:允许 untrusted_app 使用 binder 设备
allow untrusted_app binder_device:chr_file { open read write ioctl };
# 第二步:允许 untrusted_app 向 servicemanager 查询 activity_service
allow untrusted_app servicemanager:binder { call };
allow untrusted_app activity_service:service_manager { find };
# 第三步:允许 untrusted_app 与 activity_service 进行 binder 通信
allow untrusted_app activity_service:binder { call transfer };
这三步缺一不可。我曾经见过有人只加了 service_manager find 权限,但忘了加 binder call 权限,结果 App 能查到服务,但一调用就崩溃。log 里报的是 Binder transaction failed,很容易让人误以为是 Binder 驱动的问题。
注意:Binder 通信的权限检查是双向的。不仅调用方需要权限,服务方也需要权限来响应调用。比如服务方需要 allow activity_service untrusted_app:binder { call transfer }; 才能接受来自 untrusted_app 的请求。
15.4 实战:编写一个自定义服务的 SELinux 策略
好,理论说完了,咱们来点实际的。假设我要添加一个名为 my_health_service 的系统服务,步骤如下:
- 定义服务类型:在
service_contexts中添加一行my_health u:object_r:my_health_service:s0 - 定义服务进程域:在
my_health.te中定义type my_health, domain;和type my_health_exec, exec_type, file_type; - 注册权限:
allow my_health servicemanager:binder { call transfer };和allow my_health my_health_service:service_manager { add }; - 调用权限:根据调用方的域,添加对应的
find和binder call权限。
这里我画了一张流程图,帮你理清整个权限检查的链路:
从这张图你可以看到,一次 Binder 服务调用,要经过至少 5 次 SELinux 权限检查。任何一个环节的权限缺失,都会导致调用失败。
我的调试习惯:遇到 Binder 通信权限问题时,先看 dmesg | grep avc 或者 logcat -b events | grep avc。avc 拒绝信息会明确告诉你:哪个进程(scontext)、想访问哪个服务(tcontext)、需要什么权限(tclass 和 requested permission)。照着这个信息去补策略,基本一次搞定。
15.5 总结
好了,这一章的内容就这些。核心就三句话:
- service_contexts 是服务的「身份证」,servicemanager 靠它做第一道检查。
- 服务启动和注册 需要
service_manager:add权限,别忘了。 - Binder 通信 需要双向权限:调用方要有
binder:call,服务方也要有对应的binder:call或transfer。
嗯,这些规则看起来繁琐,但只要你理解了 SELinux 的「最小权限」原则,写起来其实有章可循。下次遇到 Binder 通信失败,别急着怀疑代码逻辑,先查查 avc log——八成是策略没写全。
公众号:蓝海资料掘金营,微信deep3321