Native用户空间层权限安全:隔离机制与攻防对抗
Native层是Java框架与内核之间的桥梁,包含系统守护进程、HAL层、原生库等核心组件。Android的应用沙箱、权限映射最终都要落到Native层的进程凭证上,同时这里也是权限突破的高发地带,大量提权攻击都发生在这一层。本章从Linux原生权限模型出发,解析Android Native层的权限实现机制、提权路径与防护体系。
3.1 Native层权限体系基础
3.1.1 Native层的安全定位与边界
Native层指用户空间的C/C++执行层,上接Java框架的JNI接口,下连内核的系统调用接口,是系统能力的落地层。
- 上边界:Java Native Interface(JNI),Java层通过JNI调用原生代码,原生代码也可回调Java接口。
- 下边界:系统调用(syscall),所有原生代码最终通过系统调用进入内核,访问硬件与系统资源。
Native层在权限安全中的特殊地位:
- 底层支撑:Java层的所有权限机制,最终都映射为Native层的进程凭证(UID/GID/Capabilities),由内核执行最终校验。
- 攻击高发:系统守护进程运行在高权限UID下,原生代码的内存漏洞(缓冲区溢出、UAF等)较多,是提权攻击的主要目标。
- 突破跳板:突破Java层沙箱后,通常要在Native层进一步提权,才能获得system或root权限,是权限升级的必经之路。
3.1.2 Linux传统权限模型在Android的适配
Android基于Linux内核,天然继承了Linux的自主访问控制(DAC)模型,并针对移动场景做了深度适配。
- DAC核心逻辑:每个文件有所有者UID、所属组GID与权限位(读/写/执行);每个进程有对应的UID与GID,内核根据进程的有效UID/GID,判断是否允许访问文件、执行系统调用。
- Android的核心适配:
- 应用UID化:每个第三方应用分配唯一的Linux UID,替代传统Linux的多用户概念,实现应用间的文件与进程隔离。
- GID能力映射:将系统底层能力(网络、蓝牙、存储等)映射为特定GID,应用获得对应权限后加入对应组,由内核控制访问。
- 系统UID分层:定义了一系列系统专用UID,如system(1000)、radio(1001)、bluetooth(1002)、media(1013)等,不同系统服务运行在不同UID下,实现系统服务间的权限隔离。
- 多用户扩展:支持多设备用户,每个用户下的应用UID独立,用户间数据完全隔离。
3.1.3 进程凭证与权限的关系
进程凭证(Process Credentials)是Native层权限的载体,内核所有权限检查都基于进程凭证判断。Linux进程的凭证包含多个维度:
- Real UID/GID:真实用户ID,进程的实际所有者,通常是启动进程的用户UID。
- Effective UID/GID:有效用户ID,内核做权限检查的核心依据。通常与Real UID一致,执行setuid程序时会变为文件所有者的UID。
- Saved Set UID/GID:保存的设置ID,用于临时降权后恢复权限,是setuid程序的权限切换基础。
- Filesystem UID/GID:文件系统UID,专门用于文件系统权限检查,Android中通常与Effective UID一致。
- 附加GID组:进程可加入多个用户组,获得组对应的权限,是Android权限GID映射的实现载体。
- 核心原则:Effective UID决定了进程的权限等级,是所有权限检查的核心。提权攻击的本质,就是通过各种手段修改进程的Effective UID,从低权限UID提升到高权限UID。
3.2 UID/GID沙箱的Native实现
3.2.1 应用UID的分配与管理
Android的UID有严格的划分规则,定义在android_filesystem_config.h与Process.java中:
- 系统UID范围:0~9999,用于系统进程、系统服务、硬件相关进程。
- 0:root UID,最高权限
- 1000:system UID,系统服务进程
- 1001~2999:各类系统专用UID,如radio、bluetooth、camera等
- 应用UID范围:10000~19999,每个第三方应用对应一个UID,计算公式为FIRST_APPLICATION_UID + 应用序号。
- 多用户偏移:每个用户有100000的UID偏移,比如用户1的应用UID从110000开始,用户2从210000开始。
UID的分配与管理:
- 分配主体:由PMS负责,应用首次安装时分配唯一UID,卸载时分配唯一UID,卸载后UID可回收复用。
- 持久化存储:UID与包名的映射关系持久化在packages.xml中,保证重启后对应关系不变。
- 共享UID:多个应用通过sharedUserId属性共享同一个UID,前提是签名一致。共享UID后,应用间可互相访问私有数据、共享权限,风险极高,Google已不推荐使用。
3.2.2 GID与系统权限的映射机制
很多底层系统能力不经过Java框架,直接由内核或原生服务管控,Android通过GID映射的方式,将Android权限与底层能力绑定,实现权限的底层落地。
- 核心原理:应用获得某项权限后,进程的附加GID组会加入对应的GID;内核在处理相关操作时,检查进程是否在对应GID组中,决定是否允许。
- 典型GID映射关系:
GID名称 | GID值 | 对应Android权限 | 管控能力 |
inet | 3003 | INTERNET | 创建网络套接字,访问网络 |
sdcard_rw | 1015 | WRITE_EXTERNAL_STORAGE | 读写外部存储 |
bluetooth | 1002 | BLUETOOTH | 访问蓝牙硬件 |
camera | 1006 | CAMERA | 访问相机设备 |
audio | 1005 | RECORD_AUDIO | 访问音频设备 |
- 配置位置:GID与权限的映射关系定义在frameworks/base/data/etc/platform.xml中,PMS在应用启动时计算应用的GID列表,传递给Zygote。
- 执行层级:GID校验在内核层执行,Java层无法绕过,是权限的底层兜底校验。例如:即使Java层绕过了INTERNET权限检查,进程没有inet GID,内核也会拒绝创建网络套接字。
3.2.3 Zygote fork的权限继承与降权
应用进程由Zygote进程fork诞生,这一过程也是应用沙箱的建立过程:
- Zygote的初始权限:Zygote进程以root UID启动,拥有较高的权限,负责预加载系统库与框架资源。
- fork前的参数设置:AMS向Zygote发送创建应用进程的请求,携带目标UID、GID、附加GID组、Capabilities等参数。
- fork与降权流程:
- Zygote调用fork()创建子进程,子进程继承父进程的所有内存与权限。
- 子进程调用setuid()、setgid()、setgroups()等系统调用,将UID/GID切换为目标应用的凭证,丢弃root权限。
- 设置NO_NEW_PRIVS标志,加载Seccomp过滤器,完成沙箱初始化。
4.应用执行:降权完成后,加载应用的原生库与Java代码,进入应用主循环。
- 关键设计:fork后立即降权,子进程永远不会以root权限执行应用代码,从根源上保证应用沙箱的安全性。Zygote本身不执行业务逻辑,只负责进程孵化,减少攻击面。
3.2.4 进程隔离的底层实现
基于UID/GID的进程隔离,是应用沙箱的Native层基石,主要体现在三个维度:
- 应用私有目录/data/data//的所有者为应用UID,权限为0700,只有所有者可读写执行。
- 内核VFS层基于DAC机制做权限检查,其他UID的进程无法访问该目录,即使Java层有漏洞也无法突破。
- 外部存储通过GID控制访问,没有对应GID的进程无法读写。
- 普通进程无法向其他UID的进程发送信号(kill),无法ptrace调试其他进程。
- 进程的内存空间独立,无法直接读取其他进程的内存。
- 传统IPC(管道、Socket、消息队列)都需要身份校验,跨UID通信必须显式开放。
- 不同UID的进程使用系统资源(如网络端口、设备节点)时,内核做权限校验。
- 设备文件(如/dev下的硬件设备)有对应的所有者与组权限,只有对应GID的进程可访问。
3.3 Linux Capabilities能力机制
3.3.1 Capabilities的核心思想
传统Linux的root权限是“全有或全无”的二元模型——进程要么是root拥有所有特权,要么是普通用户没有任何特权。这种模型严重违反最小权限原则:很多系统守护进程只需要一小部分特权,但不得不以root身份运行,一旦被攻破,攻击者就获得完整root权限。
Capabilities(能力)机制的核心,就是将root的全能权限拆分为数十项独立的细粒度能力,每个能力对应一项特定的特权操作。进程可以只保留业务必需的能力,丢弃其他所有能力,即使被攻破,攻击者也只能获得有限的权限,无法完全控制设备。
3.3.2 Android系统中的能力划分
Linux标准Capabilities共有40余项,Android中常用的核心能力包括:
- CAP_DAC_OVERRIDE:绕过文件DAC权限检查,可访问任意文件。
- CAP_NET_ADMIN:网络配置管理权限,可修改网络设置、路由表、防火墙规则。
- CAP_NET_BIND_SERVICE:绑定1024以下的特权端口。
- CAP_SYS_ADMIN:广泛的系统管理权限,包括挂载文件系统、设置主机名、配置内核参数等,是权限最大的能力之一。
- CAP_SYS_BOOT:重启系统的权限。
- CAP_KILL:可向任意进程发送信号,不受UID限制。
- CAP_SETUID/CAP_SETGID:可任意修改进程的UID/GID。
- CAP_SYS_PTRACE:可调试任意进程,读取进程内存。
Android系统中,每个原生守护进程都有定制化的能力集,只保留业务必需的能力,多余的全部裁剪。
3.3.3 系统守护进程的能力裁剪实践
能力裁剪是Android原生服务的标准安全实践,遵循“最小权限”原则:
- 裁剪时机:进程启动后,执行业务逻辑之前,调用cap_set_proc()系统调用,设置进程的允许集、可继承集、有效集,丢弃不需要的能力。
- 典型案例:
- mediaserver:仅保留与媒体硬件访问相关的少量能力,没有文件系统全局访问、系统管理、进程调试等能力。
- servicemanager:仅保留IPC相关的基础能力,权限高度收紧。
- surfaceflinger:仅保留显示相关的能力,无法访问网络、存储等无关资源。
- Zygote的能力处理:Zygote本身拥有较多能力,但fork应用进程后,子进程的所有能力会被全部清空,普通应用进程没有任何Capabilities。
3.3.4 能力的继承与传递规则
Capabilities有严格的继承与传递规则,防止权限意外扩散:
- fork继承:子进程完全继承父进程的能力集,能力不变。
- execve规则:执行新的可执行文件时,能力集根据文件能力、进程能力与安全标志重新计算,不会无条件继承。
- 文件能力:可给可执行文件设置Capabilities属性,执行时进程获得对应能力,替代传统的setuid root方式,减少风险。Android中部分系统工具使用文件能力替代setuid。
- No New Privileges约束:设置NNP标志后,execve无法获得任何新的能力,进程的权限只会降低不会升高。
3.4 No New Privileges与提权防护
3.4.1 PR_SET_NO_NEW_PRIVS的内核原理与用户态实现
No New Privileges(简称NNP)是Linux内核的进程安全标志,通过prctl(PR_SET_NO_NEW_PRIVS, 1)设置,一旦设置,进程生命周期内不可清除。
- 核心作用:进程执行execve()系统调用时,无法通过setuid/setgid、文件能力等方式获得比当前更高的权限。也就是说,设置了NNP的进程,永远无法提权。
- 内核原理:execve系统调用的权限计算逻辑中,会检查该标志;若标志置位,则禁止提升权限,所有权限只能等于或低于当前权限。
- Android中的落地:所有应用进程在启动时,由Zygote在fork后设置NNP标志。这是应用沙箱的重要防护机制,从内核层面阻断了传统提权路径。
3.4.2 setuid/setgid程序的风险与管控
setuid程序是Linux传统的提权载体,也是安全风险的重灾区。
- 风险原理:setuid程序的文件设置了s位,执行时进程的Effective UID会变为文件所有者的UID。如果程序存在内存漏洞或逻辑漏洞,攻击者可以利用漏洞在进程上下文中执行任意代码,从而获得文件所有者的权限。
- Android的管控措施:
- 大幅削减setuid程序:将原本需要setuid实现的功能,改为通过系统服务IPC提供,从根源上减少s位程序的数量。
- 严格审计留存程序:保留的少量setuid程序(如run-as、adbd),代码经过严格安全审核,定期修复漏洞。
- NNP阻断:应用进程设置NNP后,即使执行setuid程序,也无法提升UID,漏洞无法被用于提权。
- SELinux兜底:即使setuid程序被攻破,SELinux的强制访问控制仍会限制其操作范围,无法越权访问敏感资源。
3.4.3 对提权攻击的阻断作用
在NNP机制出现之前,应用层提权的经典路径是:应用进程 → 利用setuid程序漏洞 → 获得root权限。
NNP机制直接切断了这条路径:应用进程执行任何程序,都无法提升UID/GID或获得新的Capabilities,即使setuid程序有漏洞,也只能在原应用UID下执行代码,无法提权。
- 安全意义:NNP将提权攻击的门槛从“用户空间漏洞”提升到了“内核漏洞”,攻击者必须找到内核漏洞才能突破沙箱获得高权限,大幅提升了攻击难度。这是Android Native层最重要的安全加固机制之一。
3.5 Native服务与文件系统权限
3.5.1 原生系统服务的鉴权机制
Native系统服务(如SurfaceFlinger、ServiceManager、MediaPlayerService)运行在高权限UID下,对外提供IPC接口,同样需要严格的权限校验。
其鉴权体系分为三层:
- UID校验:服务端获取调用方的UID,判断是否在允许的白名单中。例如ServiceManager的add_service接口,只允许system UID注册系统服务,普通应用无法注册。
- SELinux安全校验:内核层SELinux会检查两个进程的安全上下文是否允许IPC交互,即使UID校验通过,SELinux策略不允许也会被拦截。
- 业务层鉴权:部分服务有自定义的鉴权逻辑,比如验证token、校验签名、检查权限等,实现更细粒度的访问控制。
- 典型案例:ServiceManager是Binder服务的管理者,其权限控制最为严格——注册服务、获取服务都需要校验UID与SELinux权限,防止恶意应用注册假冒系统服务。
3.5.2 系统分区的权限配置
Android的系统分区(/system、/vendor、/odm、/product)采用严格的权限配置,从文件系统层面保证系统安全:
- 只读挂载:正常运行时,系统分区以只读模式挂载,无法修改分区内的任何文件,防止恶意应用篡改系统程序、植入后门。只有系统升级时才会临时挂载为可写。
- 所有权配置:系统分区的绝大多数文件所有者为root,所属组为root或shell,普通应用UID没有修改权限,甚至很多文件普通应用无法读取。
- 权限位设置:
- 可执行文件:权限755,所有人可执行,仅root可修改。
- 库文件与配置文件:权限644,所有人可读,仅root可修改。
- 敏感配置文件:权限600,仅root可读写。
3.5.3 /data目录的沙箱隔离实现
/data分区是用户数据分区,存储所有应用数据与系统数据,其目录权限设计是应用数据隔离的核心:
- /data/:所有者root,权限771,普通应用无法列出目录内容。
- /data/data/:所有者system,权限771,应用只能进入自己的子目录,无法遍历其他应用的目录。
- /data/data//:所有者为应用UID,权限700,只有应用自身可读写,其他任何应用(包括system应用)都无法直接访问。
- /data/app/:APK安装目录,所有者system,权限755,应用可读取自身APK,但无法修改。
- /data/system/:系统配置目录,所有者system,权限770,普通应用无法访问。
这种多层级的目录权限设计,基于Linux原生DAC机制,由内核直接保证隔离,不需要上层框架参与,是应用数据安全的底层防线。
3.5.4 原生代码的内存安全防护
Native层的漏洞大多是内存安全漏洞,Android通过编译与运行时多重机制提升漏洞利用难度:
- ASLR(地址空间随机化):用户空间地址空间全随机化,包括代码段、数据段、堆、栈、库加载地址,攻击者无法直接使用固定地址构造漏洞利用。
- NX(数据不可执行):数据内存页(栈、堆)不可执行,防止直接执行注入的shellcode。
- Stack Canary(栈保护):栈帧中插入保护值,函数返回前校验,检测栈溢出攻击,检测到则直接终止进程。
- RELRO:重定位表只读,防止通过篡改GOT表实现攻击。
- FORTIFY_SOURCE:强化字符串函数的边界检查,检测缓冲区溢出。
这些编译加固选项,Android系统与主流应用都会默认开启,大幅提升了原生代码的安全性,增加了漏洞利用的难度。
3.6 Native层权限攻防实践
3.6.1 原生提权攻击的通用路径
Native层提权的目标,是从普通应用UID(u0_aXXX)提升到system UID或root UID,突破应用沙箱限制。通用攻击路径分为两类:
- 步骤1:找到运行在高权限UID下的原生服务漏洞(如mediaserver、drmserver、adbd)。
- 步骤2:构造畸形数据触发漏洞(缓冲区溢出、释放后重用、整数溢出等)。
- 步骤3:通过漏洞实现任意代码执行或内存读写,在高权限进程的上下文中执行攻击代码。
- 步骤4:完成提权操作,比如修改自身进程凭证、植入后门、访问敏感数据。
- 特点:利用用户空间进程的漏洞,不需要内核漏洞;受NNP与SELinux限制,提权后仍受安全策略约束。
- 步骤1:找到内核漏洞,通过系统调用、设备驱动触发。
- 步骤2:利用漏洞实现内核内存读写。
- 步骤3:修改当前进程的cred结构体,将UID/GID改为0,获得root权限。
- 步骤4:关闭SELinux、Seccomp等安全机制,获得完全控制权。
- 特点:突破所有用户空间安全机制,权限最高;但漏洞难度大,利用复杂。
3.6.2 典型漏洞案例分析
- 背景:Android 4.x~5.x时代,mediaserver进程负责媒体解析,代码量大、复杂度高,存在大量内存漏洞。
- 原理:构造畸形的音频、视频、图片文件,触发mediaserver的内存破坏漏洞,执行任意代码。
- 影响:获得media UID权限,可访问相机、麦克风、媒体文件等敏感资源;结合其他漏洞可进一步提权到root。
- 后续防护:Google拆分mediaserver进程,细化权限与能力;强化媒体解析的沙箱隔离;持续修复内存漏洞。
2.DirtyCow(脏牛)漏洞(CVE-2016-5195)
- 原理:Linux内核的写时复制(Copy-on-Write)机制存在竞争条件漏洞,可实现对只读文件的任意写入。
- 利用方式:修改系统中的setuid程序,注入恶意代码,执行后获得root权限。
- 影响:几乎覆盖所有Android 6.0及以下设备,是影响范围最广的经典提权漏洞之一。
- 本质:内核漏洞,突破了用户空间的所有防护机制,包括NNP。
- 原理:run-as是Android的setuid工具,用于开发者调试应用,早期版本存在UID校验逻辑漏洞,可绕过限制读取任意应用的私有数据。
- 影响:无需root即可读取其他应用的沙箱数据,突破应用隔离。
- 防护:Google修复校验逻辑,收紧run-as的权限与适用范围。
3.6.3 Native层权限加固方案
系统侧加固:
- 持续削减setuid程序,尽可能用服务化IPC替代s位程序,消除传统提权载体。
- 所有系统守护进程严格裁剪Capabilities,遵循最小权限原则,降低漏洞影响。
- 全面开启内存安全编译选项,推广MTE等硬件级内存安全特性。
- 严格执行No New Privileges机制,覆盖所有不可信进程。
- 配合SELinux强制策略,收紧每个进程的操作边界,即使进程被攻破也无法越权。
应用侧加固:
- 原生代码开启全部安全编译选项,修复内存安全漏洞。
- 敏感逻辑放在Native层,增加逆向与破解难度。
- 校验运行环境,检测root、调试器、模拟器等风险环境。
- 关键数据做加密存储,不依赖沙箱的单一防护。
3.7 Native层权限学习路径与总结
3.7.1 Linux基础能力建设
学习Native层权限,必须先打下扎实的Linux基础:
- 系统基础:Linux文件权限、UID/GID概念、进程模型、系统调用原理。
- 进阶机制:深入理解Linux Capabilities、setuid/setgid、进程凭证、ELF文件格式。
- 内存安全:学习常见内存漏洞原理(栈溢出、堆溢出、UAF、Double Free)与防护机制。
- 调试技术:掌握gdb、strace、objdump等原生调试分析工具。
推荐书籍:《深入理解Linux内核》《Linux程序设计》《程序员的自我修养:链接、装载与库》
3.7.2 Android Native调试方法
- ps -A:查看所有进程的UID、PID、名称。
- id:查看当前进程的UID/GID/附加组。
- ls -lZ:查看文件的权限、所有者、安全上下文。
- strace:跟踪进程的系统调用,分析权限检查与失败原因。
- 调试工具:NDK提供的gdb、lldb,可调试原生进程与库;配合addr2line、objdump分析崩溃与漏洞。
- 静态分析:IDA Pro、Ghidra等工具反汇编原生库,分析逻辑与漏洞。
- 源码学习:阅读Android原生系统服务、Zygote、Binder驱动的Native层源码,理解底层实现。
3.7.3 本章总结
Native层是Android权限体系的底层支撑,UID/GID沙箱是隔离的基础,Capabilities与No New Privileges是重要的提权防护机制。这一层是攻防对抗的关键战场——用户空间的提权攻击大多在此发生,而系统也构建了多重防护机制层层拦截。
理解Native层的权限机制,才能真正明白Android沙箱的本质,看懂提权攻击的原理与边界,无论是做系统开发、应用加固还是安全研究,Native层都是不可或缺的核心知识。