10、权限与安全机制:SELinux 策略、Android 权限模型、应用沙箱
说到嵌入式Android系统的安全,很多人第一反应是“权限弹窗”。其实,那只是冰山一角。真正撑起Android安全体系的,是三层结构:Linux内核的强制访问控制(SELinux)、Android框架层的权限模型、以及底层的应用沙箱机制。这三者缺一不可。
我个人习惯把安全比作一栋楼:SELinux是楼里的保安,权限模型是门禁卡,应用沙箱则是每个房间的独立隔断。今天我们就一层层拆开来看。
10.1 SELinux:强制访问控制
SELinux,全称Security-Enhanced Linux。说白了,它是一套“即使你是root,也不能为所欲为”的机制。传统Linux的权限模型是“用户-组-其他”,一旦拿到root权限,基本就畅通无阻了。但SELinux引入了“主体-客体-策略”的三元组,所有操作都得过策略这一关。
我在项目中遇到过一个问题:某个系统服务明明有root权限,却总是无法访问一个设备节点。查了半天,发现是SELinux策略里没写allow规则。嗯,这就是SELinux的典型场景——它不认你是谁,只认策略。
10.1.1 策略文件与标签
在AOSP中,SELinux策略文件通常放在 system/sepolicy 目录下。每个文件、进程、socket都被打上了安全上下文标签。比如:
# 一个典型的文件上下文规则
/dev/ttyGS0 u:object_r:serial_device:s0
这个标签告诉系统:/dev/ttyGS0 属于 serial_device 类型。只有被允许访问该类型的进程才能操作它。
你想想看,如果没有SELinux,一个恶意应用只要提权到root,就能直接读写串口设备。有了SELinux,即使它提权成功,策略不允许,照样没辙。
10.1.2 策略编写与调试
写SELinux策略其实不复杂,但调试起来很痛苦。我常用的调试命令是:
# 查看当前进程的安全上下文
ps -Z
# 查看文件的安全上下文
ls -Z /dev/ttyGS0
# 查看avc审计日志(拒绝操作会记录在这里)
logcat -b events | grep avc
我曾经花了一整天,就为了加一条allow规则。最后发现是类型写错了——把 serial_device 写成了 serial_dev。这种低级错误,真的让人抓狂。
setenforce 0(permissive)和 setenforce 1(enforcing)。
10.2 Android 权限模型
Android的权限模型,从API 23(Android 6.0)开始有了大变化。之前是安装时授权,之后变成了运行时授权。这个改动,说白了就是让用户更清楚应用在干什么。
但权限模型远不止“弹窗-同意”这么简单。它分为三个层级:
- 普通权限(Normal Permissions):比如访问网络、设置闹钟。系统自动授予,用户无感知。
- 危险权限(Dangerous Permissions):比如读取联系人、获取位置。需要用户明确同意。
- 签名权限(Signature Permissions):只有相同签名的应用才能获取。常用于系统级API。
我在定制ROM时,经常需要调整权限策略。比如某个系统应用需要访问位置,但不想弹窗打扰用户。这时候我会把该权限从dangerous降级为normal。具体做法是在 frameworks/base/core/res/AndroidManifest.xml 中修改权限的protectionLevel。
<!-- 修改前 -->
<permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:protectionLevel="dangerous" />
<!-- 修改后 -->
<permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:protectionLevel="normal" />
10.3 应用沙箱
应用沙箱,是Android安全的基础。每个应用运行在独立的进程中,拥有独立的UID和GID。应用之间默认不能互相访问数据,也不能直接操作硬件。
沙箱的实现,依赖Linux的用户隔离机制。每个应用安装时,系统会分配一个唯一的UID。比如:
# 查看应用的UID
ps -A | grep com.example.app
u0_a123 12345 ... com.example.app
这里的 u0_a123 就是该应用的UID。系统通过这个UID,限制应用只能访问自己的文件(/data/data/com.example.app/)。
我记得有一次,客户要求让两个应用共享数据。常规做法是用ContentProvider,但客户嫌麻烦。我建议他们使用共享UID(sharedUserId)。只要两个应用签名相同,并且在AndroidManifest中声明相同的sharedUserId,它们就能运行在同一个进程中,共享所有数据。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app1"
android:sharedUserId="com.example.shared">
...
</manifest>
10.4 知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以看到,从底层Linux内核到上层应用,安全机制是一层叠一层的。
10.5 避坑指南
做嵌入式Android开发,安全相关的坑特别多。我挑几个常见的说说:
- SELinux策略遗漏:新加一个设备节点,忘了写file_contexts和te规则。结果服务启动后一直报avc denied。解决办法:先setenforce 0,抓avc日志,再补策略。
- 权限降级过度:为了省事,把一堆dangerous权限改成normal。结果第三方应用也能轻松获取敏感权限。我建议只对系统预置应用做降级。
- 共享UID滥用:两个应用用sharedUserId,结果其中一个被卸载,另一个也跟着崩溃。因为共享UID的应用必须同时安装、同时卸载。这个坑我踩过两次。
checkpolicy 工具做一次静态检查。命令是 checkpolicy -M -c 30 -o /dev/null policy.conf。能提前发现大部分语法错误。
好了,关于权限与安全机制,今天就聊这么多。记住一句话:安全不是功能,而是设计。在嵌入式Android系统里,每一行代码、每一条策略,都可能成为攻击者的突破口。多花点时间在安全上,绝对值得。