8. 应用沙箱(三):SELinux在沙箱中的应用、沙箱逃逸与防护

好,我们继续聊沙箱。前两讲我们把Android沙箱的“地基”和“围墙”讲清楚了——UID隔离和文件权限控制。但说实话,光靠这两样,在真实攻防中根本不够看。

为什么?因为UID隔离只是“逻辑隔离”,它挡不住内核漏洞。一旦攻击者拿到了一个内核漏洞,什么UID、什么文件权限,统统形同虚设。这时候,就需要SELinux出场了。

8.1 SELinux:沙箱的“第二道锁”

SELinux,全称是Security-Enhanced Linux。它不是Android独有的,但在Android上,它被用到了极致。

我个人的理解是:UID隔离是“你是谁”,SELinux是“你能做什么”。前者管身份,后者管行为。

举个例子。一个普通App,UID是100xx,它想访问/sdcard下的文件。UID隔离说:“可以,因为你有文件权限。”但SELinux会跳出来说:“等等,你的domain是untrusted_app,你没有权限访问sdcard的context。”——这就是SELinux的威力。

核心概念:SELinux通过“主体(Subject)”、“客体(Object)”和“策略(Policy)”三个要素,定义了“谁可以做什么”。在Android沙箱中,主体是进程(App),客体是文件、socket、Binder等资源,策略就是SELinux policy。

8.2 SELinux在沙箱中的具体应用

在Android中,每个App进程都被分配了一个SELinux domain。比如:

  • untrusted_app:第三方普通App
  • platform_app:系统签名App
  • system_app:系统核心App
  • isolated_app:隔离进程(比如WebView沙箱)

每个domain有一套严格的allow规则。说白了,就是一张“白名单”。不在白名单里的操作,一律拒绝。

我记得在Android 5.0刚强制启用SELinux enforcing模式时,很多厂商的预装App直接崩溃了。为什么?因为它们在manifest里声明了权限,但SELinux policy里没写allow规则。系统直接kill掉——这就是“强制访问控制”的威力。

8.2.1 一个典型的SELinux规则示例

我们来看一段真实的SELinux policy(te文件):

# 允许untrusted_app读取自己的数据目录
allow untrusted_app app_data_file:dir r_dir_perms;
allow untrusted_app app_data_file:file rw_file_perms;

# 禁止untrusted_app访问系统数据
neverallow untrusted_app system_data_file:dir r_dir_perms;
neverallow untrusted_app system_data_file:file rw_file_perms;

看到那个neverallow了吗?这是SELinux的“硬边界”。即使你通过漏洞绕过了UID检查,SELinux也会在最后关头拦住你。

避坑指南:我曾经在调试一个系统App时,发现它总是无法写入某个目录。查了半天,发现是SELinux policy里漏写了一条allow规则。记住:在enforcing模式下,SELinux的拒绝日志会出现在dmesg或logcat中,关键词是“avc: denied”。这是你排查问题的第一线索。

8.3 沙箱逃逸:攻击者是怎么绕过去的?

讲完了防护,我们聊聊攻击。沙箱逃逸,说白了就是“从笼子里跑出来”。在Android上,常见的逃逸手法有几种:

  1. 内核漏洞利用:比如CVE-2015-6639(Stagefright),通过解析恶意媒体文件触发内核提权,直接拿到root权限。这时候UID隔离和SELinux都失效了——因为攻击者已经在内核态了。
  2. SELinux策略漏洞:有些厂商为了兼容性,会放宽SELinux规则。比如允许untrusted_app访问某些系统服务,结果被攻击者利用。
  3. 文件描述符泄露:父进程打开了一个高权限文件,子进程通过继承fd来访问。这在早期Android版本中很常见。
  4. Binder漏洞:通过Binder调用系统服务,利用服务端的逻辑漏洞来执行未授权操作。

你想想看,这些逃逸手法,本质上都是在“绕过”或“打破”沙箱的边界。而SELinux的作用,就是给这个边界加上一层“保险丝”。

8.4 防护策略:如何让沙箱更坚固?

基于我多年的实战经验,我总结了几个关键防护点:

8.4.1 最小权限原则

每个App的SELinux domain,只给最少的权限。不要因为方便就放开规则。我曾经见过一个厂商,为了省事,直接把所有第三方App都放到了platform_app domain里——结果就是,一个普通App可以访问系统级资源。

8.4.2 启用SELinux enforcing模式

permissive模式只记录日志,不阻止操作。这在调试时有用,但生产环境必须用enforcing。我建议在开发阶段就用enforcing,早发现问题早解决。

8.4.3 定期审计SELinux策略

使用checkpolicysesearch工具,检查是否有不必要的allow规则。特别是neverallow规则,一定要写全。

8.4.4 内核加固

SELinux依赖内核来执行策略。如果内核本身有漏洞,SELinux也救不了。所以,及时打内核补丁、启用KASLR、CFI等内核防护机制,是必不可少的。

警告:不要以为有了SELinux就万事大吉。它只是“深度防御”的一环。真正的安全,需要UID隔离、文件权限、SELinux、内核加固、应用层安全等多层协同。任何一层出问题,都可能被攻破。

8.5 知识体系图:SELinux在沙箱中的角色

下面这张图,是我自己梳理的。它展示了Android沙箱中,SELinux与其他安全机制的关系:

Android沙箱安全架构:SELinux的角色 Linux内核层 SELinux强制访问控制 App 1 UID: 10001 Domain: untrusted_app 允许:读自己的数据 允许:写自己的数据 禁止:访问系统数据 禁止:访问其他App数据 App 2 UID: 10002 Domain: untrusted_app 允许:读自己的数据 允许:写自己的数据 禁止:访问系统数据 禁止:访问其他App数据 系统服务 UID: 1000 Domain: system_app 允许:访问系统数据 允许:管理其他App 禁止:被untrusted_app访问 SELinux在UID隔离之上,提供第二层强制访问控制 即使UID相同,不同domain的进程也无法越权访问

8.6 实战:如何检测和防御沙箱逃逸

在实际项目中,我建议你从以下几个维度入手:

维度 检测方法 防御措施
内核漏洞 使用uname -a检查内核版本,对比CVE列表 及时打补丁,启用KASLR、CFI
SELinux策略 使用sesearch --allow列出所有allow规则 遵循最小权限原则,写全neverallow
文件描述符 检查/proc/[pid]/fd目录 在fork前关闭不必要的fd
Binder调用 使用dumpsys查看服务调用日志 对敏感服务增加SELinux权限检查

嗯,这里要注意:检测只是第一步,真正的防护在于“预防”。我个人的习惯是,在项目初期就把SELinux策略写死,而不是等到上线后再补。因为后期补策略,很容易漏掉某些边界情况。

一个小技巧:在开发阶段,可以用setenforce 0切换到permissive模式,收集所有avc: denied日志。然后根据这些日志,生成完整的SELinux策略。这样既不会漏掉规则,又能保证安全性。

好了,关于SELinux在沙箱中的应用,我们就聊到这里。记住一句话:沙箱不是万能的,但没有SELinux的沙箱是万万不能的


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