Appearance
权限与安全模型:从 UID 沙箱到 SELinux 域
本页导读:用户点下的「允许/拒绝」只是这座安全大厦的门铃。本页自上而下拆四层防线——UID 进程沙箱、seccomp 与命名空间、SELinux 域与 MCS、权限框架与 AppOps,并给出 Android 11~16 的收紧时间线。前置:架构篇 init/Zygote 与 Binder 页;进程模型。
一、多层防御总览
| 防线 | 拦什么 | 代表机制/文件 |
|---|---|---|
| UID 沙箱 | 应用互相读写、冒充身份 | 一 App 一 UID,zygote 降权 fork |
| seccomp | 危险/过时系统调用 | 按 ABI 的内核过滤(bionic 装载) |
| 命名空间 | 文件系统视图(isolated 进程尤其严) | mount ns / default ns(以 AOSP 为准) |
| SELinux | 即便拿到 UID 也碰不到的对象 | untrusted_app 域 + MCS 分级 |
| 权限框架 | 能力授予的业务语义 | PermissionManagerService + PermissionController |
| AppOps | 「有权限也限制使用频次/状态」 | AppOpsManager op 表 |
四层不是冗余,是纵深:越界攻击要同时突破内核过滤、MAC 策略与权限记账,任何一层都是独立审计对象。
顺带澄清 targetSdk 的角色:它不解锁安全策略,只解锁行为兼容(比如旧弹窗样式);安全层的安全默认值几乎都按设备版本一刀切。
二、UID 沙箱:一切隔离的锚点
- 安装时分配 UID(多用户下
userId*100000 + appId编码),所有进程由 Zygote fork 后降权到该 UID 再执行应用代码;adb shell id -u进应用进程可见u0_a123风格身份。 android:isolatedProcess进一步进 9999x 保留段,无数据目录、无网络默认,只留给渲染/媒体解码这类「拿代码执行权但不给数据权」的场景。- WebView 的渲染进程走 App Zygote:应用自己
ZygoteProcessfork 的子沙箱(共享预加载),这是「一个 App 里还有自己的 Zygote」的正解。 - 同签名 + 历史
sharedUserId的共 UID 玩法已停止新增(Android 10+ 安装器逐步拒绝),跨 App 共享请走 FileProvider/绑定服务。
沙箱的直接后果:应用代码碰不到别的 UID 的 /data、socket、进程信号——「互相拉起保活」在 UID 层面本就该难,能保活的年代是策略放水,不是模型允许。
bash
adb shell ps -A -o PID,USER,NAME | grep com.example
# u0_a123 = userId 0 + appId 123;换个工作资料用户则是 u10_a123
# 同 App 的所有进程(:remote 等)共享同一 UID——「拆进程提优先级」不存在的根据三、seccomp 与命名空间:收窄内核表面积
- seccomp-bpf:进程启动时装载按 ABI 的 allowlist 过滤器,不在名单的 syscall 直接
SIGSYS。NDK 可调用面(restricted NDK API)就是它的用户态投影——新内核 API 逐个解禁、危险老调用永久拉黑。 - 命名空间:isolated 进程拥有严格裁剪的 mount ns;普通应用进程运行在共享的 default mount ns(Android 12~16 的 ns 口径有演进,进程 mount ns 化以 AOSP release notes 为准)。
- 装载点可以在 Zygote 的 fork-specialize 路径读到(native):
验证进程当前 seccomp 状态(比读策略文件更直接):
bash
adb shell cat /proc/$(pidof com.example | cut -d' ' -f1)/status | grep -i seccomp
# Seccomp: 2 ← 2 = filter mode(已装载 bpf 过滤器)cpp
// frameworks/base/core/jni/com_android_internal_os_Zygote.cpp( specialize 阶段骨架大意)
static void specializeAppProcess(...) {
// 1. uid/gid/caps 降级、补充组
// 2. seccomp filter 装载(按 targetSdk/ABI 选择策略版本)
// 3. rlimits / scheduler policy
// 4. 命名空间与 SELinux 上下文切换
// SELinux 按 seapp_contexts 计算目标域 + MCS 级别
}版本注记:基于 Android 16 / API 36;Android 5 起 SELinux enforcing 全量、6 起 NDK seccomp 收紧、11 起 seccomp 策略与 targetSdk 绑定(旧目标应用更宽);Android 12~15 差异:进程 mount ns 实验推进(以 AOSP 为准)。
四、SELinux:域 + 级别,越权也撞墙
- 域转换:Zygote fork 出应用进程时,按
system/sepolicy的seapp_contexts规则从zygote域切到untrusted_app(系统应用按签名/分区进platform_app/priv_app/app_zygote等)。 - MLS/MCS 级别:
s0:c512:c1024...的类别由userId与appId哈希生成——同一 App 同用户同级别,跨用户互不可见;这就是「工作资料里的进程摸不到主资料文件」的底层实现。 neverallow规则让「加白名单」在代码评审层面就不可能合入 AOSP(如 untrusted_app 禁访别的 App 数据目录)。- 调试视角:
adb shell dmesg | grep avc(userdebug)看 denial;应用市场审核与厂商 ROM 的「额外权限」最终都要落在 sepolicy 上,拿不到域内白名单就是物理不可能。
seapp_contexts 的规则行长这样(理解格式即可读全局域分配):
text
# system/sepolicy/public/seapp_contexts(示意行)
user=_app seinfo=... domain=untrusted_app levelFrom=all
# 关键词:user/isolate 匹配进程身份 → domain 决定 SELinux 域 →
# levelFrom 决定 MCS 类别如何从 appId/userId 推导- 全局只有 enforcing 一种真相:user 版 SELinux 不可关闭(早期
setenforce 0后门早已封死),厂商想加能力必须写 sepolicy 并过 CTS。
bash
adb shell ps -Z | grep com.example # 每行的第一个字段就是进程 SELinux 上下文
# u:r:untrusted_app:s0:c192,c256,c257,c258,c259,c260,c261 ← 域 + MCS 级别五、权限框架:安装时、运行时、一次性
protection level 决定「谁来批」:
| 级别 | 授予方式 | 例 |
|---|---|---|
| normal | 安装即得(manifest 声明) | INTERNET、VIBRATE |
| dangerous | 运行时弹窗(用户可撤) | CAMERA、LOCATION、READ_MEDIA_* |
| signature/privileged | 同签名/特权分区 | SIGNATURE 类系统能力 |
| appop/特殊 | 设置页/对话框单独开通 | SYSTEM_ALERT_WINDOW、MANAGE_EXTERNAL_STORAGE |
运行时权限的链路要点:
requestPermissions发起 → 系统权限 UI(AOSP 为 PermissionController,Android 13+ 授权流程下沉为系统侧服务逻辑)→ 结果写PermissionManagerService台账 → 回调onRequestPermissionsResult/ActivityResultContracts。- 可撤回是常态:用户随时在设置页撤销;代码必须按「权限可能从有到无」运行(监听
onCallbackRequired/AppOps 变更,相机/定位服务要能中途降级)。
1.5 权限组的「一次授权全组」已死:Android 11 起同组权限逐项询问、逐项可撤,RECORD_AUDIO 不再因 CAMERA 顺手给——任何「组授权」假设的代码都要按单项重写。 3. 一次性权限(Android 11):相机/麦克风出现「仅这一次」选项——授予随会话存在,进程结束即撤回,无需应用配合。 4. 自动重置(Android 11):长期未使用的 App,其运行时权限被系统静默撤销并通知——「装了就永远有」不存在。
bash
adb shell pm grant com.example android.permission.CAMERA # 测试三连
adb shell pm revoke com.example android.permission.CAMERA
adb shell pm reset-permissions com.example # 恢复初始(需 root)
请求-回调的完整时序(以 Activity Result API 口径):
```text
requestPermissions(permArray, n)
└ PermissionManagerService 记录「等待结果」
└ 拉起 PermissionController UI(可能连弹多个)
└ 用户选择 → 台账更新(grant/revoke + flags)
└ onRequestPermissionsResult / ActivityResult 回调
└ 进程若在弹窗期间被回收:结果丢,重开后重新发起——别把 UI 状态押在弹窗上
## 六、AppOps 与特殊权限:有资格 ≠ 随便用
AppOps 给「运行时行为」记账:同一个 CAMERA 权限,op 状态可以是 `MODE_ALLOWED / MODE_IGNORED / MODE_ERRORED / MODE_DEFAULT`——IGNORED 表现为 API 静默失败(相机黑帧),ERRORED 直接抛 SecurityException,这是「权限给了却像没给」类工单的底层原因。
- 查看:`adb shell appops query`(或 `dumpsys appops`);改:`adb shell appops set <pkg> SYSTEM_ALERT_WINDOW allow` 之类——特殊权限(悬浮窗、安装未知应用、所有文件访问、精确闹钟)在实现上都是 appop + 专属设置页。
- 敏感 op 还有**使用计数与审计**(`AppOpsManager.startOperation/getHistoricalOps`),隐私仪表盘的数据源就是它——滥用某能力会被平台看到,这是 Play 政策执法的技术依据。
```bash
adb shell dumpsys appops | sed -n '1,40p' # 全部 op 的当前模式(输出较长)
adb shell appops get --user 0 com.example COARSE_LOCATION
# 常见审计字段:时间窗、持续时间、拒绝次数(版本字段名以 AOSP 为准)- 工程注意:定位的「精确/大致」(Android 12)、「仅使用期间」(Android 10 起 while-in-use 语义)都同时横跨权限与 appop 两层,排查要两头看。
kotlin
// 入口三连查:权限台账 / AppOps 实际状态 / 硬件在位
fun cameraUsable(ctx: Context): Boolean =
ctx.checkSelfPermission(Manifest.permission.CAMERA) == PERMISSION_GRANTED
&& (ctx.getSystemService(AppOpsManager::class.java)
.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_CAMERA,
Process.myUid(), ctx.packageName)
== AppOpsManager.MODE_ALLOWED)
&& ctx.packageManager.hasSystemFeature(FEATURE_CAMERA_ANY)七、Android 11 → 16 收紧年表
text
API 30 一次性权限;权限自动重置;后台位置弹窗独立化;软件键盘类收紧
API 31 精确/大致定位拆分;BLUETOOTH_SCAN/CONNECT/ADVERTISE 新运行时权限
API 32 (小幅,无用户面重点)
API 33 POST_NOTIFICATIONS 成为运行时权限;NEARBY_WIFI_DEVICES;READ_MEDIA_* 取代存储三件套
API 34 部分照片/视频访问(READ_MEDIA_VISUAL_USER_PROPERTIES + 用户选择);FGS 类型与权限强绑定;Health Connect 数据类走向标准化
API 35 Health Connect 以 android.permission.health.* 体系接管健康数据
API 34+ 后台位置继续加压:while-in-use 未授予则 FGS 定位类直接崩
API 36 延续「使用期间」与可见性绑定(如 while-in-use 类 FGS 的退后台受限),新能力以行为变更文档为准一句话史观:Android 安全模型的十年是把「授予」从一次性事件改造成持续谈判——每一次使用都在弹窗、设置页、审计表与系统限流里重新确认。
八、常见误区问答
Q1:targetSdk 不升是不是还能躲过新权限? 躲得过弹窗,躲不过平台底线:部分安全策略(seccomp 版本、FGS 限制、敏感路径拦截)按设备版本生效,与 target 无关;且商店上架门槛绑定 target 晋级。
Q2:弹窗拒绝了两次会不会永久拉黑? 系统会在连续拒绝后把选项变灰/改为「去设置」,逻辑由 PermissionController 裁决(各厂商 UI 有差异);应用侧正确姿势:shouldShowRequestPermissionRationale 判时机、永久拒绝引导设置页,别循环弹窗骚扰。
Q3:查 checkSelfPermission 返回 GRANTED 就万事大吉? 它只看权限台账;AppOps 可能 IGNORED、FGS 可能缺类型、用户可能刚撤销。关键功能入口要做「权限 + op + 硬件可用性」三连查。
Q4:厂商 ROM 的「自启动/后台弹出」开关算哪层? 都不算——它是厂商在权限框架之外私加的策略层,没有公开 API 与稳定语义;测试矩阵要单独覆盖,但架构决策不能依赖它。
本页小结:安全是四层纵深——UID 沙箱定进程边界,seccomp/命名空间收内核面,SELinux 域+MCS 封 MAC 越界,权限框架与 AppOps 管「能力语义」;运行时时代的核心心智是授予可撤回、使用可审计、后台必受限,Android 11~16 的每一项新弹窗都站在这条主线上。
参考链接
- https://developer.android.com/guide/how/permissions/rp-request-permissions
- https://developer.android.com/guide/how/user-permissions
- https://source.android.com/docs/security/features/security-enhancements
- https://cs.android.com/android/platform/system/sepolicy/+/main:public/app.te
- https://cs.android.com/android/platform/frameworks/base/+/main:core/java/android/app/AppOpsManager.java
- 站内:进程模型、Service、Intent 与解析、架构篇 init 与 Zygote