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 框架的核心。从 ActivityManagerServiceWindowManagerService,它们都运行在 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 的控制非常精细。它主要检查两个东西:

  1. 服务查找(find):客户端能否找到这个服务
  2. 服务调用(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_managerbinder 是两个不同的 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_servicewindow_service 只允许 system_appshell 域调用。而 batterystats_service 对所有域开放。这就是“最小权限原则”的体现——每个服务只暴露必要的接口给必要的主体。

14.2.4 自定义系统服务的策略编写

如果你在开发一个自定义系统服务,比如 MySecureService,你需要做以下几件事:

  1. 定义服务类型:在 service_contexts 中添加 mysecure u:object_r:mysecure_service:s0
  2. 定义服务域:在 sepolicy 中定义 type mysecure_service, service_manager_type;
  3. 编写访问规则:允许特定客户端调用

一个完整的策略示例:

# 定义服务类型
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 的检查链条是:

  1. system_server 进程(域:system_server
  2. 调用 ActivityManagerService(类型:activity_service
  3. 服务内部读取属性 ro.build.version.sdk(类型:system_prop

每一步都需要对应的 SELinux 权限。如果某一步缺失,整个操作就会失败。这就是为什么我常说,SELinux 的策略是“链式防护”——任何一个环节断了,攻击者就无法得逞。

14.4 本章知识体系

为了让你更直观地理解本章的内容,我画了一张图:

Android SELinux 属性服务与系统服务控制 属性服务控制 property_contexts 定义 ro.* → system_prop 策略规则:allow domain type:property_service set 读取控制:默认开放,敏感属性受限 前缀匹配:ro.*, persist.*, ctl.* 系统服务控制 service_contexts 定义 activity → activity_service Binder 检查:find + call 服务级隔离:最小权限原则 自定义服务:type + allow 规则 交互 核心:谁(主体)能对什么(客体)做什么(权限)

这张图把属性服务和系统服务的控制逻辑分成了左右两个部分。左边是属性服务,核心是 property_contextsset 权限;右边是系统服务,核心是 service_contexts 和 Binder 的 find/call 检查。中间用箭头表示它们之间的交互——系统服务在运行时经常需要读写属性。

好了,这一章的内容就到这里。属性服务和系统服务的 SELinux 控制,说白了就是“动态门禁”。属性是动态的键值对,服务是动态的 IPC 接口。SELinux 通过类型和权限,把它们管得死死的。下一章我们会聊更底层的东西——内核层面的 SELinux 策略,那才是真正的大杀器。


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