7. 应用沙箱(二):文件系统沙箱、数据目录隔离、外部存储访问控制
好,我们接着聊沙箱。上一章我们把沙箱的进程隔离讲透了,这一章咱们聚焦在文件系统层面。说白了,就是App在手机上能读写哪些文件,不能碰哪些文件。这个事儿,我当年刚接触Android时也踩过坑,以为权限搞定了就万事大吉,后来才发现——文件系统沙箱才是真正把App关进笼子的那道铁门。
7.1 文件系统沙箱:每个App的独立小世界
Android系统为每个应用分配一个独立的Linux用户ID(UID),并在/data/data/(Android 10以下)或/data/user/0/(Android 10+)下创建专属目录。这个目录,只有该UID的进程能访问。
你想想看,这意味着什么?App A想读App B的数据库?门儿都没有。哪怕App B自己不小心把数据库权限设成0666,Linux内核也会直接拒绝——因为UID不对。
我记得有一次做安全审计,发现某个预装应用把日志文件写到了/sdcard/下,还抱怨说“我自己的日志为什么别的App也能读”。这就是典型的没搞懂沙箱边界——内部存储是私有的,外部存储才是共享的。
7.2 数据目录隔离:内部存储的权限模型
每个应用安装时,系统会执行以下操作:
- 分配一个唯一的UID
- 在
/data/data/<package_name>创建目录 - 设置目录所有者为此UID,权限为
rwx------(即0700) - 挂载一个专用的命名空间(namespace),隔离
/proc、/sys等敏感路径
来看一个实际的目录结构:
/data/data/com.example.app/
├── databases/ # 数据库文件
├── files/ # 普通文件存储
├── shared_prefs/ # SharedPreferences
├── cache/ # 缓存文件
├── code_cache/ # 代码缓存
└── lib/ # native库(只读)
每个子目录的权限也很有讲究:
| 目录 | 默认权限 | 说明 |
|---|---|---|
| databases/ | rwx------ | 只有本应用可读写 |
| files/ | rwx------ | 同上,但可通过MODE_WORLD_READABLE放开(不推荐) |
| cache/ | rwx------ | 系统可随时清理 |
| lib/ | r-x------ | 只读,防止篡改native库 |
MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE已经被废弃。如果你还在用这两个flag,赶紧改掉——它们在新版本上会直接抛异常。
我曾经在一个项目中看到,某团队为了“方便”把数据库文件设成了MODE_WORLD_READABLE,结果被另一个恶意App把用户聊天记录全拖走了。嗯,这种坑我建议你千万别踩。
7.3 外部存储访问控制:从混乱到规范
外部存储(也就是/sdcard或/storage/emulated/0)一直是Android安全的老大难问题。早期版本基本是“谁都能读”,后来才一步步收紧。
7.3.1 传统模式(Android 10之前)
应用只要声明READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE权限,就能读写整个外部存储。说白了,就是一把钥匙开所有门。
这带来的问题很明显:
- App可以扫描你的照片、下载文件、音乐库
- 恶意App可以偷偷读取其他App的缓存数据
- 用户隐私几乎不设防
7.3.2 分区存储(Android 10+)
Google终于出手了。分区存储(Scoped Storage)把外部存储分成了几个区域:
/storage/emulated/0/
├── Android/
│ ├── data/ # 每个App的私有外部目录
│ │ └── com.example.app/
│ └── obb/ # 扩展包文件
├── DCIM/ # 照片(需特殊权限)
├── Download/ # 下载(需特殊权限)
├── Pictures/ # 图片(需特殊权限)
└── ... # 其他公共目录
关键变化有三点:
- 私有目录:
Android/data/<package>只有本应用能访问,其他App不能读 - 公共目录:访问
DCIM、Download等需要用户明确授权,且只能通过MediaStore API - 直接路径访问:不再允许通过
/sdcard/DCIM/xxx.jpg直接读写,必须用Content URI
7.3.3 Android 11的进一步收紧
Android 11又加了一道锁:即使你声明了权限,也不能直接访问其他应用的Android/data/目录。系统会直接返回空结果。
来看一个代码对比:
// ❌ 旧方式(Android 10以下可用)
File file = new File("/sdcard/DCIM/photo.jpg");
FileInputStream fis = new FileInputStream(file);
// ✅ 新方式(Android 10+推荐)
ContentResolver resolver = getContentResolver();
Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = resolver.query(uri, null, null, null, null);
// 通过cursor获取具体文件的Uri
说实话,这个改动让很多开发者骂娘。但从安全角度看,这是正确的——用户的数据,凭什么让App随便翻?
7.4 沙箱逃逸的常见手法与防御
讲完了规则,咱们聊聊怎么绕过规则。我见过几种典型的沙箱逃逸方式:
| 逃逸方式 | 原理 | 防御措施 |
|---|---|---|
| 符号链接攻击 | 在公共目录创建指向私有数据的符号链接 | 系统在Android 10+已禁止跨UID符号链接 |
| ptrace注入 | 通过调试接口读写其他进程内存 | Android 10+默认禁用ptrace系统调用 |
| 文件描述符传递 | 通过Binder传递FD绕过权限检查 | SELinux策略限制FD传递范围 |
| Zygote注入 | 在应用启动前修改其文件访问权限 | SELinux + 文件系统挂载选项双重保护 |
/proc/<pid>/fd/读取其他App打开的文件描述符。这个路径在Android 11上已经被SELinux严格限制了,但如果你还在用Android 9或更低版本,这个漏洞依然存在。升级系统版本是最直接的防御手段。
7.5 文件系统沙箱的完整架构
来,我画了一张图,把整个文件系统沙箱的架构串起来:
从这张图可以看得很清楚:内部存储是每个App的独立小房间,外部存储是公共客厅。小房间只有主人能进,客厅虽然大家都能进,但每个柜子(目录)也有自己的锁。
7.6 总结几个关键点
- UID是沙箱的根基——没有UID隔离,文件权限就是空谈
- 内部存储默认完全隔离——除非你主动用
MODE_WORLD_READABLE作死 - 外部存储正在走向“分区化”——Android 10是分水岭,Android 11是进一步收紧
- SELinux是最后一道防线——即使UID和权限都对了,SELinux还能再拦一道
嗯,文件系统沙箱这块儿,说白了就是“你的数据放你家,我的数据放我家,公共区域大家守规矩”。理解了这一点,后面讲SELinux策略时你会觉得更顺。
公众号:蓝海资料掘金营,微信deep3321