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。这种低级错误,真的让人抓狂。

小技巧: 调试SELinux时,可以先把策略设为permissive模式(只记录不阻止),等所有avc日志都处理完了,再切回enforcing模式。命令是 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>
核心要点: 应用沙箱 + SELinux + 权限模型,三者共同构成了Android的安全铁三角。沙箱隔离进程,SELinux控制操作,权限模型管理用户交互。缺一个,安全体系就不完整。

10.4 知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以看到,从底层Linux内核到上层应用,安全机制是一层叠一层的。

Android 安全机制三层结构 第一层:Linux 内核 SELinux 强制访问控制 · 进程隔离 · 文件系统权限 第二层:Android 框架 权限模型(普通/危险/签名) · 运行时授权 · 权限降级 第三层:应用层 应用沙箱 · UID隔离 · 共享UID · 数据保护 每一层都依赖下一层提供的安全基础,层层递进,缺一不可

10.5 避坑指南

做嵌入式Android开发,安全相关的坑特别多。我挑几个常见的说说:

  • SELinux策略遗漏:新加一个设备节点,忘了写file_contexts和te规则。结果服务启动后一直报avc denied。解决办法:先setenforce 0,抓avc日志,再补策略。
  • 权限降级过度:为了省事,把一堆dangerous权限改成normal。结果第三方应用也能轻松获取敏感权限。我建议只对系统预置应用做降级。
  • 共享UID滥用:两个应用用sharedUserId,结果其中一个被卸载,另一个也跟着崩溃。因为共享UID的应用必须同时安装、同时卸载。这个坑我踩过两次。
我的习惯: 每次修改SELinux策略后,我都会用 checkpolicy 工具做一次静态检查。命令是 checkpolicy -M -c 30 -o /dev/null policy.conf。能提前发现大部分语法错误。

好了,关于权限与安全机制,今天就聊这么多。记住一句话:安全不是功能,而是设计。在嵌入式Android系统里,每一行代码、每一条策略,都可能成为攻击者的突破口。多花点时间在安全上,绝对值得。


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