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上,常见的逃逸手法有几种:
- 内核漏洞利用:比如CVE-2015-6639(Stagefright),通过解析恶意媒体文件触发内核提权,直接拿到root权限。这时候UID隔离和SELinux都失效了——因为攻击者已经在内核态了。
- SELinux策略漏洞:有些厂商为了兼容性,会放宽SELinux规则。比如允许untrusted_app访问某些系统服务,结果被攻击者利用。
- 文件描述符泄露:父进程打开了一个高权限文件,子进程通过继承fd来访问。这在早期Android版本中很常见。
- 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策略
使用checkpolicy和sesearch工具,检查是否有不必要的allow规则。特别是neverallow规则,一定要写全。
8.4.4 内核加固
SELinux依赖内核来执行策略。如果内核本身有漏洞,SELinux也救不了。所以,及时打内核补丁、启用KASLR、CFI等内核防护机制,是必不可少的。
警告:不要以为有了SELinux就万事大吉。它只是“深度防御”的一环。真正的安全,需要UID隔离、文件权限、SELinux、内核加固、应用层安全等多层协同。任何一层出问题,都可能被攻破。
8.5 知识体系图:SELinux在沙箱中的角色
下面这张图,是我自己梳理的。它展示了Android沙箱中,SELinux与其他安全机制的关系:
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