14、属性系统SELinux策略:property_contexts编写,属性读写权限控制,系统属性安全策略定制

属性系统,说白了就是Android的“全局配置中心”。你平时用getpropsetprop看到的那些键值对,背后都有一套严格的SELinux管控机制。我刚开始接触这块时,觉得不就是读写个属性嘛,能有多复杂?直到有一次,某个系统服务因为写了一个不该写的属性,直接导致整个开机流程卡死……嗯,从那以后,我再也不敢小看property_contexts了。

14.1 property_contexts 文件结构

这个文件位于/system/etc/selinux/目录下,它的作用很简单——给每个属性打上SELinux标签。你想想看,系统里成百上千个属性,如果没有分类管理,那权限控制就是一团乱麻。

文件格式长这样:

# 系统核心属性
net.                    u:object_r:net_radio_prop:s0
sys.                    u:object_r:system_prop:s0
persist.                u:object_r:persist_prop:s0
ctl.                    u:object_r:ctl_prop:s0

# 厂商定制属性
vendor.                 u:object_r:vendor_prop:s0
ro.vendor.              u:object_r:vendor_prop:s0

# 特定属性精确匹配
ro.build.fingerprint    u:object_r:build_prop:s0
persist.sys.timezone    u:object_r:system_prop:s0

每一行由两部分组成:

  • 属性匹配模式:支持通配符,.作为分隔符
  • SELinux上下文:格式为u:object_r:类型:s0
我的习惯:匹配顺序是从精确到模糊。如果ro.build.fingerprintro.build.同时存在,精确匹配优先。我曾经踩过这个坑——写了个模糊匹配把不该覆盖的属性给覆盖了,调试了一整天。

14.2 属性读写权限控制机制

属性权限控制,本质上就是问三个问题:

  1. 谁想读?——发起操作的进程是什么域(domain)
  2. 读什么?——目标属性是什么类型(type)
  3. 允许吗?——TE规则里有没有放行

对应的TE规则长这样:

# 允许system_app域读取system_prop类型的属性
allow system_app system_prop:property_service { read };

# 允许init域设置ctl_prop类型的属性(控制服务启停)
allow init ctl_prop:property_service { set };

# 允许shell域读取和转储所有属性
allow shell property_type:property_service { read dump };

核心要点:属性的读写权限是分开控制的。你可以让某个进程只能读不能写,或者只能写特定的属性。我在项目中遇到过,某个三方应用疯狂尝试写入persist.开头的属性,被SELinux直接拦截,日志里全是avc denial。这就是权限控制的价值。

14.3 系统属性安全策略定制实战

假设我们要定制一个厂商专属属性vendor.custom.feature,只有特定的系统服务能写,其他进程只能读。怎么做?

第一步:定义属性类型

property_contexts中添加:

vendor.custom.feature    u:object_r:vendor_custom_prop:s0

第二步:编写TE规则

# 定义类型
type vendor_custom_prop, property_type;

# 允许my_service域写入
allow my_service vendor_custom_prop:property_service { set };

# 允许所有域读取(通过property_type的通用规则)
# 注意:这里不需要额外写allow,因为property_type已经包含了read权限

第三步:验证策略

编译后刷机,用avcstatdmesg | grep avc检查是否有权限拒绝。我建议你写一个测试脚本:

#!/system/bin/sh
# 测试写入
setprop vendor.custom.feature "test_value"
echo "写入结果: $?"

# 测试读取
getprop vendor.custom.feature
echo "读取结果: $?"
注意:如果写入失败,先别急着改策略。检查一下property_contexts的匹配顺序,看看是不是被其他模糊匹配给截胡了。我曾经遇到过,vendor.前缀的模糊匹配把精确匹配给覆盖了,折腾了两天才发现。

14.4 属性系统的SELinux策略架构图

下面这张图展示了属性从请求到响应的完整流程,以及SELinux在其中的拦截点:

属性系统SELinux策略架构图 进程域 (Domain) 例如: system_app, init setprop/getprop SELinux 检查点 avc_has_perm() 拦截 通过/拒绝 property_contexts 属性 → SELinux类型映射 TE 规则 allow domain type:property_service { read set } 属性存储区 property_service 服务 拒绝 操作结果 成功 / avc: denied 日志审计 logcat / dmesg 流程:进程发起请求 → SELinux检查 → 匹配property_contexts → 校验TE规则 → 执行/拒绝 进程域 检查点 上下文映射 TE规则

14.5 常见问题与避坑指南

问题1:属性写不进去,但日志里没有avc denial

这种情况我遇到过好几次。原因往往是property_contexts里没有匹配到该属性,导致它被分配了默认的default_prop类型。而default_prop的权限通常很严格。解决办法:检查property_contexts的匹配规则,确保你的属性被正确分类。

问题2:persist属性重启后丢失

嗯,这里要注意。persist.开头的属性会写入/data/property/目录。如果SELinux策略阻止了property_service写入该目录,属性就存不下来。我曾经排查过一个bug,就是persist.vendor.xxx属性每次重启都重置,最后发现是file_contexts/data/property的标签配错了。

问题3:ctl属性控制服务失败

ctl.开头的属性用于控制服务的启动和停止。只有init域才有权限设置这些属性。如果你想让某个系统服务也能控制服务启停,需要在TE规则里显式放行:

allow my_service ctl_prop:property_service { set };
调试小技巧:adb shell dmesg | grep "property"可以快速过滤出属性相关的SELinux拒绝日志。我习惯把这个命令做成一个alias,调试时直接敲propavc就能看到所有属性权限问题。

14.6 属性安全策略的最佳实践

场景 推荐做法 不推荐做法
厂商定制属性 使用vendor.前缀,单独定义类型 复用系统属性类型,容易权限泄露
持久化属性 使用persist.前缀,严格控制写入权限 允许所有域写入persist属性
控制类属性 只放行给init和特定系统服务 放行给shell或普通应用
只读属性 使用ro.前缀,只赋予read权限 同时赋予read和set权限

最后说一句,属性系统的SELinux策略,说白了就是“谁能在什么条件下读写什么属性”。把这个逻辑理清楚了,策略文件写起来就顺了。我在项目里见过最离谱的情况,是一个同事把allow * property_type:property_service { read };写进了策略里——这等于把所有属性的读权限都放开了,还好在code review阶段被拦下来了。

嗯,关于property_contexts和属性权限控制,今天就聊到这儿。记住一点:权限最小化,类型精确化,这是定制属性安全策略的核心原则。


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