14. Android SELinux(三):属性服务SELinux控制、系统服务SELinux策略
好,我们继续往下挖。前两章我们把 SELinux 的基础概念和文件系统的策略讲透了。这一章,咱们聊聊两个更贴近“运行时”的东西——属性服务和系统服务。说白了,就是 Android 系统跑起来之后,那些动态的、需要跨进程通信的东西,怎么用 SELinux 管住它们。
我个人习惯把 SELinux 的策略理解成“门禁系统”。文件系统是静态的,门禁卡是固定的。但属性服务和系统服务,它们是动态的——属性值可以改,服务可以启动和绑定。嗯,这里要特别注意,动态的东西最容易出安全漏洞。
14.1 属性服务的 SELinux 控制
属性服务(Property Service)是 Android 里一个很特殊的存在。它其实就是一个全局的键值对存储,任何进程都能读,但只有特定进程能写。你想想看,如果随便一个 App 都能改 ro.build.version.sdk 或者 persist.sys.timezone,那系统不就乱套了?
所以,SELinux 对属性服务的控制,核心就是一句话:谁可以设置哪个属性。
14.1.1 属性服务的 SELinux 上下文
属性本身也有安全上下文。在 SELinux 的世界里,每个属性都被打上了一个标签。比如:
# 系统属性,只读
ro.* u:object_r:system_prop:s0
# 持久化属性,可写但受限
persist.* u:object_r:system_prop:s0
# 安全相关属性
selinux.* u:object_r:security_prop:s0
# 控制属性
ctl.* u:object_r:ctl_prop:s0
这些上下文定义在 property_contexts 文件里。我刚开始看这个文件的时候,觉得它跟文件系统的 file_contexts 很像,但后来发现一个关键区别:属性的上下文匹配是前缀匹配,不是精确匹配。比如 ro.* 匹配所有以 ro. 开头的属性。
核心要点:属性服务的 SELinux 控制,本质上是“主体(进程)→ 客体(属性)”的访问控制。进程必须有 set_prop 权限,并且属性的类型必须被允许。
14.1.2 属性设置的策略规则
我们来看一个实际的策略规则。假设我们想让 system_server 进程能够设置 persist.sys. 开头的属性:
# 允许 system_server 设置 persist.sys.* 属性
allow system_server system_prop:property_service set;
# 更精细的控制:只允许设置特定前缀
allow system_server persist_sys_prop:property_service set;
这里有个细节:property_service 是一个特殊的 class,专门用于属性服务操作。而 set 是权限。嗯,我记得有一次在项目中,客户反馈说某个系统服务无法写入属性,查了半天发现是策略里写的是 system_prop 而不是 persist_sys_prop。类型没对上,权限自然就拒绝了。
避坑指南:我曾经在调试一个 OTA 升级问题时,发现 update_engine 无法设置 persist.sys.ota_status。最后定位到是 property_contexts 里把 persist.sys.ota 单独定义了一个类型 ota_prop,但策略里没给 update_engine 授权。所以,自定义属性一定要检查上下文和策略是否匹配。
14.1.3 属性读取的控制
你可能觉得属性读取是开放的,其实不然。虽然大部分属性是全局可读,但有些敏感属性(比如 selinux.*、ro.build.fingerprint)也需要控制。不过,SELinux 对属性读取的控制相对宽松,默认允许所有进程读取 system_prop 类型的属性。但如果你定义了新的属性类型,记得要显式允许读取:
# 允许所有域读取 system_prop
allow { domain -untrusted_app } system_prop:property_service read;
# 只允许特定域读取 security_prop
allow system_server security_prop:property_service read;
14.2 系统服务的 SELinux 策略
系统服务(System Service)是 Android 框架的核心。从 ActivityManagerService 到 WindowManagerService,它们都运行在 system_server 进程中。但每个服务其实都有自己独立的 SELinux 上下文,这就是所谓的“服务级隔离”。
为什么需要服务级隔离?说白了,就是防止一个服务被攻破后,整个系统沦陷。你想想看,如果 ActivityManagerService 被恶意代码控制了,它能做的事情太多了——启动任意 Activity、获取应用列表、甚至模拟用户操作。但有了 SELinux,即使它被攻破,也只能做策略允许的事情。
14.2.1 服务声明的 SELinux 上下文
每个系统服务在注册时,都会指定一个 SELinux 上下文。这个上下文定义在 service_contexts 文件中:
# service_contexts 文件示例
activity u:object_r:activity_service:s0
window u:object_r:window_service:s0
package u:object_r:package_service:s0
batterystats u:object_r:batterystats_service:s0
当客户端通过 Binder 调用服务时,SELinux 会检查:客户端的域是否有权限访问服务端的类型。这个检查发生在 Binder 驱动层,对上层完全透明。
14.2.2 Binder 调用的 SELinux 检查
Binder 是 Android 的 IPC 机制,SELinux 对 Binder 的控制非常精细。它主要检查两个东西:
- 服务查找(find):客户端能否找到这个服务
- 服务调用(call):客户端能否调用这个服务的方法
看一个实际的策略例子:
# 允许 system_app 查找并调用 activity_service
allow system_app activity_service:service_manager find;
allow system_app activity_service:binder call;
# 允许 untrusted_app 查找并调用 activity_service
allow untrusted_app activity_service:service_manager find;
allow untrusted_app activity_service:binder call;
这里有个有意思的点:service_manager 和 binder 是两个不同的 class。前者控制的是“能否找到服务”,后者控制的是“能否调用服务”。我遇到过一种情况:某个 App 能通过 ServiceManager.getService() 拿到服务引用,但一调用就崩溃。原因就是策略只给了 find 权限,没给 call 权限。
注意:不要以为给了 find 权限就万事大吉。没有 call 权限,客户端拿到服务引用也是废的。而且这种错误很难调试,因为 ServiceManager.getService() 不会报错,只有调用时才会抛出 SecurityException。
14.2.3 系统服务的权限分离
在 Android 中,不同的系统服务有不同的敏感度。比如:
| 服务名称 | SELinux 类型 | 敏感度 | 典型调用者 |
|---|---|---|---|
| activity | activity_service | 高 | system_app, shell |
| window | window_service | 高 | system_app, shell |
| package | package_service | 中 | system_app, shell, untrusted_app |
| batterystats | batterystats_service | 低 | 所有域 |
| alarm | alarm_service | 低 | 所有域 |
从这张表可以看出,activity_service 和 window_service 只允许 system_app 和 shell 域调用。而 batterystats_service 对所有域开放。这就是“最小权限原则”的体现——每个服务只暴露必要的接口给必要的主体。
14.2.4 自定义系统服务的策略编写
如果你在开发一个自定义系统服务,比如 MySecureService,你需要做以下几件事:
- 定义服务类型:在
service_contexts中添加mysecure u:object_r:mysecure_service:s0 - 定义服务域:在
sepolicy中定义type mysecure_service, service_manager_type; - 编写访问规则:允许特定客户端调用
一个完整的策略示例:
# 定义服务类型
type mysecure_service, service_manager_type;
# 允许 system_app 查找和调用
allow system_app mysecure_service:service_manager find;
allow system_app mysecure_service:binder call;
# 允许 mysecure_service 自身访问系统资源
allow mysecure_service system_prop:property_service set;
allow mysecure_service app_data_file:dir read;
个人经验:我建议在编写自定义服务策略时,先用 permissive 模式跑一遍,收集所有 avc 拒绝日志。然后根据日志逐条添加规则。这样比凭空想象要准确得多。我曾经因为少写了一条 binder call 规则,导致服务在正式环境上一直无法正常工作,排查了整整两天。
14.3 属性服务与系统服务的交互
属性服务和系统服务不是孤立的。很多系统服务在启动时会读取属性,在运行时也会修改属性。比如 ActivityManagerService 在启动时会读取 ro.build.version.sdk 来判断 API 级别。这时候,SELinux 的检查链条是:
system_server进程(域:system_server)- 调用
ActivityManagerService(类型:activity_service) - 服务内部读取属性
ro.build.version.sdk(类型:system_prop)
每一步都需要对应的 SELinux 权限。如果某一步缺失,整个操作就会失败。这就是为什么我常说,SELinux 的策略是“链式防护”——任何一个环节断了,攻击者就无法得逞。
14.4 本章知识体系
为了让你更直观地理解本章的内容,我画了一张图:
这张图把属性服务和系统服务的控制逻辑分成了左右两个部分。左边是属性服务,核心是 property_contexts 和 set 权限;右边是系统服务,核心是 service_contexts 和 Binder 的 find/call 检查。中间用箭头表示它们之间的交互——系统服务在运行时经常需要读写属性。
好了,这一章的内容就到这里。属性服务和系统服务的 SELinux 控制,说白了就是“动态门禁”。属性是动态的键值对,服务是动态的 IPC 接口。SELinux 通过类型和权限,把它们管得死死的。下一章我们会聊更底层的东西——内核层面的 SELinux 策略,那才是真正的大杀器。
公众号:蓝海资料掘金营,微信deep3321