2、Android安全模型基础:Linux内核安全机制与Android沙箱原理
各位同学,今天我们聊点硬核的——Android安全模型的根基。说白了,Android的安全体系不是凭空造出来的,它站在Linux巨人的肩膀上。我个人习惯把这一层叫做“地基”,地基不稳,上层建筑再花哨也没用。
2.1 Linux内核安全机制:UID/GID、DAC、Capabilities
先说说Linux内核那套老本行。Android虽然是个移动操作系统,但它的内核还是Linux那一套。嗯,这里要注意,Android用的Linux内核是经过定制的,但安全机制的核心没变。
2.1.1 UID/GID:每个应用的“身份证”
在Linux世界里,每个用户都有一个唯一的UID(用户ID)和GID(组ID)。Android把这个概念玩出了新花样——每个应用安装时,系统都会给它分配一个独立的UID。你想想看,这意味着什么?
意味着每个应用在系统看来,就是一个独立的“用户”。我在项目中遇到过一个问题:两个应用想共享数据,结果发现UID不同,直接没法访问对方的文件。这就是UID隔离的效果。
关键点:Android中,普通应用的UID范围通常是10000到19999。系统应用的UID则从1000开始。这个分配机制是Zygote进程在fork新应用时完成的。
GID的作用呢?它主要用于权限组管理。比如,某个应用属于“sdcard_r”组,那它就能读SD卡。说白了,GID就是一把把“钥匙”,不同的组有不同的权限。
2.1.2 DAC:自主访问控制
DAC(Discretionary Access Control),中文叫自主访问控制。这个名字有点绕,我换个说法——“谁创建的文件,谁说了算”。
每个文件都有三组权限:所有者、所属组、其他人。每组权限又分读(r)、写(w)、执行(x)。举个例子:
# 查看文件权限
$ ls -l /data/data/com.example.app/
drwxrwx--x 2 u0_a123 u0_a123 4096 2024-01-15 10:30 files
看到没?这个目录的所有者是u0_a123(就是那个应用的UID对应的用户名),所属组也是u0_a123。其他用户连进入这个目录的权限都没有。这就是DAC在起作用。
我的经验:曾经有个同事问我,为什么他的应用读不了另一个应用的文件?我让他用ls -l一看,权限位是rwx------,只有所有者能访问。这就是DAC的典型表现——文件所有者拥有绝对控制权。
2.1.3 Capabilities:细粒度的“超级权限”
传统的Linux中,root用户拥有所有权限,普通用户几乎没有。这太极端了。Capabilities机制就是为了解决这个问题——把root权限拆成一个个小单元。
比如,你想让一个程序能绑定到1024以下的端口,但又不想给它全部root权限。怎么办?给它CAP_NET_BIND_SERVICE这个capability就行了。
# 给程序添加网络绑定能力
$ setcap cap_net_bind_service=+ep /path/to/your/app
Android中,系统服务经常使用Capabilities。我记得有一次调试一个网络问题,发现某个系统服务明明有root权限,但就是绑定不了端口。后来一查,原来是Capabilities被限制了。嗯,这个坑我踩过。
| Capability名称 | 作用 | Android中的使用场景 |
|---|---|---|
| CAP_NET_BIND_SERVICE | 允许绑定到1024以下端口 | DHCP客户端、VPN服务 |
| CAP_SYS_ADMIN | 系统管理操作 | 挂载文件系统、设置主机名 |
| CAP_DAC_OVERRIDE | 绕过DAC权限检查 | 备份恢复服务、包管理器 |
| CAP_CHOWN | 改变文件所有者 | 安装应用时修改文件归属 |
2.2 Android沙箱原理
沙箱,说白了就是“隔离区”。Android的沙箱机制,我愿称之为“应用界的单身公寓”——每个应用都有自己的独立空间,互不干扰。
2.2.1 沙箱的核心:UID隔离
前面说了,每个应用有独立的UID。这个UID就是沙箱的“门牌号”。系统通过UID来决定:
- 这个应用能访问哪些文件
- 这个应用能调用哪些系统服务
- 这个应用能和其他哪些应用通信
你想想看,如果恶意应用想偷你的银行App数据,它得先知道银行App的UID,然后还得有权限访问那个UID下的文件。但DAC机制已经把它挡在门外了。
注意:沙箱不是万能的。如果系统本身被root了,或者有内核漏洞,沙箱就可能被绕过。我曾经在某个项目中遇到过利用内核漏洞提权,然后直接读取其他应用数据的情况。所以,及时打安全补丁非常重要。
2.2.2 沙箱的文件系统隔离
每个应用安装后,系统会在/data/data/下创建一个以包名命名的目录。这个目录的权限设置非常严格:
/data/data/com.example.app/
├── files/ # 应用私有文件
├── databases/ # 数据库文件
├── shared_prefs/ # SharedPreferences文件
└── cache/ # 缓存文件
这些目录的权限通常是drwxrwx--x或drwx------。其他应用连ls都执行不了。这就是为什么你没法直接通过文件管理器看到其他应用的数据。
2.3 进程隔离与Binder通信安全
进程隔离是沙箱的延伸。每个应用运行在独立的进程中,有自己的地址空间。一个进程崩溃了,不会影响其他进程。但问题来了——进程之间怎么通信?
2.3.1 Binder:Android的进程通信利器
Binder是Android特有的IPC(进程间通信)机制。它比Linux传统的Socket、管道等方式更高效、更安全。我个人觉得,Binder是Android架构中最精妙的设计之一。
Binder的工作原理,我简单说一下:
- 客户端进程发起一个Binder调用
- Binder驱动在内核空间处理这个调用
- 服务端进程收到调用请求并处理
- 结果通过Binder驱动返回给客户端
整个过程,数据只在内核空间拷贝一次。这就是它高效的原因。
2.3.2 Binder的安全机制
Binder的安全主要体现在两个方面:
- UID/PID验证:Binder驱动会自动记录调用方的UID和PID。服务端可以检查调用方是否有权限。
- 权限检查:系统服务在实现Binder接口时,会检查调用方是否拥有所需的权限。
// Binder服务端检查调用方UID
public class MyService extends IMyService.Stub {
@Override
public void doSensitiveOperation() {
// 获取调用方的UID
int callingUid = Binder.getCallingUid();
// 检查是否是系统应用
if (callingUid != Process.SYSTEM_UID) {
throw new SecurityException("只有系统应用才能调用此方法");
}
// 执行敏感操作
performSensitiveOperation();
}
}
避坑指南:我曾经在开发一个系统服务时,忘记检查调用方的UID。结果一个第三方应用直接调用了我的接口,修改了系统设置。从那以后,我养成了一个习惯——所有Binder接口都要做UID/PID校验,哪怕你觉得这个接口很安全。
2.3.3 Binder通信的权限管控
Android的权限系统与Binder紧密结合。当应用调用某个系统服务时,系统会检查该应用是否声明了相应的权限。比如:
<!-- 应用需要声明权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
// 系统服务检查权限
public class LocationManagerService extends ILocationManager.Stub {
@Override
public Location getLastKnownLocation() {
// 检查调用方是否有定位权限
if (checkCallingPermission(
android.Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("需要ACCESS_FINE_LOCATION权限");
}
return getLastLocation();
}
}
这个检查过程是自动的,但开发者也可以手动添加更细粒度的控制。我建议你在自定义系统服务时,一定要加上权限检查,不要依赖默认行为。
小结
今天的内容,说白了就是Android安全的“地基”。UID/GID解决了“你是谁”的问题,DAC解决了“你能做什么”的问题,Capabilities解决了“你能做到什么程度”的问题。沙箱和Binder则把这些机制串联起来,形成了一个完整的安全体系。
嗯,下一章我们会聊Android的权限模型——从安装时授权到运行时授权的演变。到时候我会分享一些我在适配Android 6.0运行时权限时踩过的坑,敬请期待。