2. Linux内核安全基础:UID/GID机制、进程隔离、文件权限、Capabilities机制

好,我们开始第二讲。上一章我们聊了Android安全架构的全景,这一章我们要深入到底层——Linux内核。说白了,Android的安全基石,就是站在Linux这个巨人的肩膀上。你想想看,没有内核的进程隔离,没有文件权限控制,上层搞再多花样也是空中楼阁。

我个人习惯,讲安全一定要从最基础的“身份”讲起。你是谁?你能干什么?你不能干什么?这就是UID/GID、文件权限和Capabilities要回答的问题。

2.1 UID/GID机制:你是谁?

Linux是一个多用户系统。每个用户都有一个唯一的数字ID,叫UID(User ID)。用户还属于一个或多个组,每个组也有一个GID(Group ID)。

嗯,这里要注意:在Android上,UID的含义有点不一样。Android不是传统意义上的多用户系统(虽然现在有“多用户模式”),它把每个应用当作一个“用户”。每个安装的应用,系统都会给它分配一个独立的UID。这就是应用隔离的起点。

核心概念:在Android中,UID = 应用的身份标识。不同UID的应用,默认情况下谁也碰不了谁的数据。

举个例子,你安装了一个微信,系统给它分配UID=10080。你又装了一个支付宝,UID=10081。这两个进程跑起来,内核一看UID不同,直接就把它们隔离开了。微信想读支付宝的内存?门都没有。

我在项目中遇到过一个问题:有个第三方ROM把系统UID分配搞乱了,结果两个系统应用互相读不了对方的文件,导致蓝牙和WiFi功能同时崩溃。查了两天才定位到是UID冲突。所以,UID分配这件事,看似简单,但一旦出错就是灾难。

2.1.1 特殊UID

有几个特殊的UID你需要记住:

UID 名称 说明
0 root 超级用户,拥有最高权限。Android上一般被限制使用。
1000 system 系统服务专用UID,权限仅次于root。
2000 shell adb shell使用的UID,有一定特权但有限。
10000+ 应用 普通应用的UID范围,每个应用一个。

避坑指南:我曾经见过一个开发者,在应用里直接写死了UID=1000,想冒充系统服务。结果呢?应用根本装不上,因为Android在安装时就会检查UID冲突。这条路走不通。

2.2 进程隔离:内核的“铁壁”

进程隔离,说白了就是让每个进程活在自己的小世界里。Linux内核通过虚拟内存和UID/GID机制,实现了进程间的天然隔离。

为什么会这样?因为每个进程都有自己的虚拟地址空间。进程A的地址0x1234,和进程B的地址0x1234,物理上完全是两个地方。内核的MMU(内存管理单元)负责做这个映射。进程A想直接访问进程B的内存?MMU直接给你抛一个段错误。

再加上UID的隔离:两个不同UID的进程,连发信号(比如kill命令)都要有权限。更别提读写/proc/pid/下的敏感信息了。

我记得有一次调试一个ANR问题,需要看另一个进程的堆栈信息。我直接用adb shell进去,发现根本读不了/proc/其他进程的maps文件。这就是进程隔离在起作用——即使你有shell权限,也不能随便窥探别的进程。

注意:进程隔离不是绝对的。如果你有root权限(UID=0),那所有隔离对你来说都是透明的。所以Android系统极力避免让应用获得root权限。这也是为什么“越狱”或“root”对Android安全是致命打击。

2.3 文件权限:rwx的学问

Linux文件权限,经典的三元组:读(r)、写(w)、执行(x)。每个文件有三组权限:所有者(owner)、所属组(group)、其他人(others)。

ls -l看一下:

-rw-r--r-- 1 u0_a100 u0_a100 1024 Jan 1 12:00 myfile.txt

这个文件的所有者是u0_a100(UID=10100),所属组也是u0_a100。权限是rw-r--r--,意思是:

  • 所有者可以读写(rw-)
  • 同组人可以读(r--)
  • 其他人也可以读(r--)

在Android上,文件权限是应用数据保护的第一道防线。每个应用的数据目录(/data/data/包名/)默认只有该应用自己可以访问。其他应用连目录列表都看不到。

我见过一个常见的坑:有些开发者为了方便,把SharedPreferences或者数据库文件设成了777(全局可读写)。结果呢?恶意应用只要知道路径,就能直接读取你的登录态数据。嗯,这就是典型的“方便一时,后悔一世”。

最佳实践:Android上,永远不要手动修改应用私有目录的文件权限。系统默认的rwx------(只有自己可读写执行)就是最安全的。如果你需要跨进程共享数据,请使用ContentProvider或者FileProvider,而不是直接改文件权限。

2.4 Capabilities机制:细粒度的“特权”

好,接下来是本章的重头戏——Capabilities。你想想看,传统的Linux权限模型太粗糙了:要么是root(无所不能),要么是普通用户(啥特权没有)。但很多时候,一个进程只需要一点点特权,比如绑定一个低端口(1024以下),或者修改系统时间。

Capabilities机制就是来解决这个问题的。它把root的超级权限拆成了一个个小单元,每个单元叫一个capability。比如:

  • CAP_NET_BIND_SERVICE:允许绑定到1024以下的端口
  • CAP_SYS_TIME:允许修改系统时钟
  • CAP_DAC_OVERRIDE:允许绕过文件权限检查
  • CAP_KILL:允许发送信号给其他进程

一个进程可以拥有多个capability,但不需要拥有全部。这样,即使某个服务被攻破,攻击者也只能利用它拥有的那几个capability,而不是整个root权限。

我记得在Android 8.0之前,很多系统服务都是以root身份运行的。后来Google逐步推进“最小权限原则”,把这些服务改成用普通UID运行,只赋予必要的capability。比如mediaserver进程,以前是root,现在只给了CAP_SYS_NICECAP_DAC_OVERRIDE等几个必要的capability。

个人经验:我曾经分析过一个漏洞,某个系统服务因为拥有CAP_DAC_OVERRIDE,导致攻击者可以通过这个服务绕过文件权限检查,读取其他应用的数据。修复方案就是去掉这个不必要的capability。所以,给capability的时候一定要“吝啬”,给多了就是给自己挖坑。

2.4.1 Capabilities的三种集合

每个进程的capability分为三个集合:

集合 含义 说明
Permitted 允许的 进程可以使用的capability上限。即使当前没启用,也可以随时启用。
Effective 生效的 内核实际检查的capability。只有在这个集合里的,才算真正拥有。
Inheritable 可继承的 子进程可以继承的capability。用于execve()时传递。

举个例子:一个进程的Permitted集合里有CAP_NET_BIND_SERVICE,但Effective集合里没有。那它现在还不能绑定低端口。只有通过cap_set_proc()系统调用,把这个capability从Permitted移到Effective,才能真正生效。

为什么要这么设计?说白了就是“双重确认”。Permitted是“我能用”,Effective是“我现在要用”。这样可以在需要时才启用,不需要时就关掉,减少攻击面。

2.5 本章知识体系

下面这张图,把本章的核心逻辑串起来了。你可以看到,从UID/GID的身份识别,到进程隔离的边界控制,再到文件权限的访问控制,最后到Capabilities的细粒度特权管理,层层递进,构成了Linux内核安全的基础骨架。

Linux内核安全基础:知识体系 UID/GID机制:身份识别 每个进程/应用拥有唯一UID → 内核据此判断“你是谁” 进程隔离:边界控制 虚拟地址空间 + UID隔离 → 进程间无法直接访问对方内存/数据 文件权限:访问控制 rwx三元组 + 所有者/组/其他人 → 控制文件/目录的读写执行 Capabilities机制:细粒度特权 拆分root权限为独立单元 → 按需赋予,最小权限原则 安全层级递进

从这张图可以看得很清楚:UID/GID是基础身份,进程隔离是边界,文件权限是访问规则,Capabilities是特权管理。每一层都建立在前一层之上,缺一不可。

好了,这一章的内容就到这里。Linux内核安全基础,说白了就是搞清楚“谁是谁”、“谁能碰谁”、“谁能做什么”。下一章我们会进入Android特有的安全机制——权限模型。到时候你会发现,Android的权限管理,其实就是在Linux内核的基础上,又加了一层更精细的控制。


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