一:安卓体系结构

Android体系架构分为四层:应用层、应用框架层、库层、内核层。
1 应用层
这是 Android 系统中最高的一层,包含了用户直接与之交互的应用程序。Android 自带了一些核心应用,如电话、短信、日历、浏览器、相机等,此外,用户还可以通过 Google Play 等应用商店下载和安装第三方应用。这一层的应用都是用 Java 或 Kotlin 编写,并运行在应用框架层之上。
2 应用框架层
Android开发人员接触最多的就是框架层,该层提供了各种各样的系统API,开发人员通过使用这些API来构建上一层的各种各样的APP。这些API包括且不限于:Activity Manager(控制Activity的生命周期等)、Notification Manager(提供通知相关的功能)、Content Provider(实现应用程序间的数据共享)、Resource Manager(管理非代码资源,比如布局文件,图片资源,字符资源等等)、View(提供常见的视图控件)、Alarm Manager(提供闹钟相关服务)等等。
3 库层
第三层包含两部分内容:
第一部分是Native C\C++系统库层,主要提供一系列第三方类库,常见的有系统C库、多媒体库(播放媒体文件)、SGL(2D图像引擎)、Free Type(渲染位图和矢量字体)、Sqlite(轻量级数据库)、SSL(Secure Socket Layer)、Webkit(提供网络工具)等等;
第二部分是运行环境,包括Dalvik虚拟机和Java核心库。Dalvik虚拟和Oracle的JVM的区别:Dalvik是基于寄存器的,JVM是基于栈的。JVM运行.class文件,每个.class文件对应一个类;Dalvik虚拟机将.class文件转为.dex文件,只有一个.dex文件,包含了所有的类,并且通过性能优化转为.odex文件。基于寄存器的虚拟机运行速度更快,文件更小,效率更高,适合移动端。这是因为,虽然Dalvik生成的代码更多,但是它需要的指令更少。而加载代码的开销要小于指令分发。
4 内核层
这是 Android 系统的最底层,基于 Linux 内核,负责底层硬件的抽象和与硬件设备的交互。它提供了设备驱动程序、内存管理、进程管理、网络协议栈、安全设置等服务。Linux 内核使 Android 能够在不同的硬件平台上运行,并且确保了系统的安全性和稳定性。
驱动程序:内核包含了一系列驱动程序,用于管理设备硬件,如显示器、相机、蓝牙、Wi-Fi、音频等。
电源管理:管理设备的电源使用,以提高电池寿命。
文件系统:提供了数据存储和检索的功能。
安全模块:如 SELinux,增强了系统的安全性。
二:Binder
进程隔离:和其它类UNIX的系统一样,Android的进程地址空间是独立的,也就是说一个进程不能直接访问其他进程的内存空间。
IPC(进程间通信机制):早于安卓之前就有一些IPC机制,例如文件、信号、套接字、管道、信号量、共享内存和消息队列等等。安卓系统只是采用了其中的一部分,例如本地套接字。
Binder :Binder是 Android 系统中最重要的进程间通信机制(IPC),它用于在不同进程间传递数据和消息。Binder 提供了高效、安全的通信方式,使应用程序能够调用系统服务或其他进程中的方法。
AIDL: AIDL是 Android Interface Definition Language 的缩写。它是一种用于在 Android 应用程序的不同组件(如进程间)之间进行通信的接口定义语言。通过 AIDL,开发者可以定义应用程序中不同进程之间的接口,使得这些进程能够共享数据或调用彼此的服务。
通常,AIDL 会用在需要跨进程通信的场景中,比如一个服务在后台运行,另一个应用或组件需要与这个服务进行通信。AIDL 文件定义了一个接口,该接口列出了可供远程调用的方法。编译器会根据这个定义自动生成一些辅助代码,开发者可以使用这些代码来实现进程间的通信。
简单来说,AIDL 是 Android 平台上实现跨进程通信(IPC)的关键工具。
1 Binder驱动
Binder 驱动 是 Android 系统中的一个内核模块(/dev/binder
),位于 Linux 内核空间。它是 Android 特有的Binder机制的核心实现,负责在内核态和用户态之间传递数据。
Binder 驱动的主要作用是作为中介,协调客户端进程和服务端进程之间的数据交换。它负责创建和管理 Binder 对象的引用、传递 IPC 消息、处理跨进程的方法调用、以及管理系统资源。
Binder 驱动在客户端和服务端之间传递 IPC 消息。客户端通过系统调用(如 ioctl
)将请求发送到内核,Binder 驱动接收到请求后,将其路由到目标服务端。服务端处理完请求后,通过 Binder 驱动将响应数据传回客户端。
Binder 驱动为每一个创建的 Binder 对象分配一个唯一的句柄,用于标识该对象。句柄在内核中维护,并与内核中的实际 Binder 实例相关联。当客户端请求访问一个远程 Binder 对象时,Binder 驱动为客户端分配一个引用(也称为 Binder Proxy),这个引用是一个指向远程对象的代理,通过它,客户端可以间接地调用远程对象的方法。
例如,进程A创建了一个Binder对象,然后将之传递给进程B,进程B再将之传递给进程C,那么这三个进程的调用,均由同一个Binder对象处理。进程A会通过内存地址直接引用Binder对象(因为它在A的内存空间中),二=然而B和C只会收到Binder对象的句柄。
内核负责维护活动的 Binder 对象与它们在其他进程中的句柄之间的映射关系。因为一个Binder 对象的标识符是独一无二的,并且由内核负责维护,所以用户空间进程除非通过IPC 机制处理,否则不可能创建一个 Binder 对象副本或获取相关引用。因此 Binder 对象是唯一的、不可伪造的、可通信的对象,可以作为安全令牌使用。这也使得 Android 可以使用基于权能的安全模型(capability-based security)。
Binder 驱动的工作流程:
-
客户端调用:客户端进程想要与远程服务进行通信时,会创建一个 Binder 引用,并通过系统调用(如
ioctl
)将请求发送给 Binder 驱动。 -
内核处理:Binder 驱动接收到请求后,根据 Binder 引用找到对应的服务端对象,并将请求消息放入服务端进程的请求队列中。
-
服务端处理:服务端进程的 Binder 线程从请求队列中取出消息,执行相应的操作。处理完毕后,将结果返回给 Binder 驱动。
-
返回结果:Binder 驱动将服务端的响应消息传回客户端进程,客户端进程接收并处理该响应,完成一次完整的 IPC 调用。
Binder的内核驱动为A和B进程的内存空间中各定义一块Binder内存。A与B通信,内核将信息从A的那块内存直接复制到B的那块内存,并告诉B信息被复制到内存中的哪里。
2 Binder对象
Binder对象代表了服务端进程中可供客户端调用的一个接口或服务。通过 Binder 对象,客户端可以远程调用服务端进程中的方法,从而实现跨进程通信。在客户端进程中,Binder 对象的表现形式是一个 Binder Proxy
,它是 Binder 对象在客户端的代理。客户端通过 Binder Proxy
发送请求,而这些请求最终由服务端的 Binder 对象处理。
创建:Binder 对象通常是在服务端进程中创建的。服务端在启动时,会实例化其实现的 AIDL 接口(即 Binder 对象),并将其注册到系统服务管理器(ServiceManager
)中。客户端进程通过 ServiceManager
获取到对应的 Binder Proxy
,从而能够与服务端通信。
生命周期:Binder 对象的生命周期与服务端进程的生命周期密切相关。当服务端进程被销毁时,其 Binder 对象也会被销毁。相应地,客户端对该 Binder 对象的引用将失效。
工作机制:当客户端调用 Binder Proxy
上的方法时,这个调用会被封装成一个 IPC 请求,发送到内核的 Binder 驱动。Binder 驱动会将该请求转发给对应的服务端进程中的 Binder 对象。Binder 对象接收到请求后,会执行相应的处理逻辑,并将结果返回给客户端。这个过程对开发者是透明的,类似于本地方法调用。
3 Binder安全令牌
3.1 UID、PID、GID的概念
先介绍一下安卓UID、PID、GID的概念:
UID (User ID):
定义:UID 是 Android 系统中用来标识每个应用程序或用户的唯一标识符。
UID 用于访问控制和权限管理。每个应用程序在安装时都会被分配一个唯一的 UID,不同的 UID 之间无法访问彼此的资源(如文件、数据等),除非通过特定的权限(如共享 UID 或使用 Content Provider)。
系统用户的 UID 通常在 1000 到 1999 之间分配,例如 root
的 UID 是 0,system
的 UID 是 1000。应用程序的 UID 通常从 10000 开始,每个应用都有一个唯一的 UID。
PID (Process ID):
定义:PID 是 Android 中每个进程的唯一标识符。
操作系统通过 PID 管理和调度进程。每个正在运行的进程都有一个唯一的 PID,PID 是动态分配的,在进程终止后,该 PID 可能会被重新分配给新的进程。
PID 是由操作系统分配的整数,通常从 1 开始依次递增。
GID (Group ID):
定义:GID 是用于标识进程所属组的唯一标识符。
GID 用于文件系统权限管理。同一个组中的进程可以共享特定的资源权限(如文件的读写权限)。
和 UID 类似,系统组的 GID 通常在 1000 到 1999 之间分配,而应用程序组的 GID 通常从 10000 开始。
3.2 Binder对象权限检查
在 Android 中,Binder 对象可以表示权能,在这种使用方式下,它又被称作 Binder 令牌(Binder token)。一个 Binder 令牌可以是一种权能,也可以是一个目标资源。一个进程拥有一个 Binder 令牌,授权该进程对 Binder 对象的完全访问控制权限,从而使其能够在目标对象上执行 Binder 事务处理操作。如果 Binder 对象实现了多个动作(基于Binder整动态处理的code 参数,选择要执行的动作),只要调用者拥有一个 Binder 对象的引用,就可以执行任意操作,如果想实现更细粒度的访问控制,那么操作者需要实现必要的权限检查例程,这通常利用调用者进程的 PID 和EUID 来实现。
在 Android 中,一个通常的模式是:对于在 system (UID 1000) 或 root (UID 0)权限下运行的调用进程允许执行所有操作,而对于其他进程来执行额外的权限检查例程。因此要想访问一个重要的 Binder 对象,例如能够控制系统服务的对象,拥有如下两种方式:
一是通过限制谁可以获取 Binder 对象的引用;另一个是在执行该Binder 对象的动作之前,检测调用者的身份标识。(这个检查是可选的,在需要时由Binder 对象自己实现。)
3.3 Binder作为安全令牌使用
Binder 对象可以没有其他功能实现,只被作为权能使用。在这种使用模式下,多个协作的进程可同时拥有同一个 Binder 对象,而作为服务端(处理一些客户端的请求)的进程使用 Binder 令牌来验证它的客户端,就像 Web 服务器使用会话 Cookie一样。
另一种使用方式是在 Android 框架内部使用,对于应用通常是不可见的。而在公开 API中可见的一种 Binder 令牌使用方式是窗口令牌(window token)。每个 Activity 的顶层窗口都与一个 Binder 令牌(窗口令牌)相关联,这些令牌均由Android 的窗口管理器(管理应用程序窗口的系统服务)记录保存。应用程序可以获得它们自己的窗口令牌,但不能访问其他应用的窗口令牌。每个窗口请求都必须提供一个与该应用相关联的窗口令牌,因此确保了窗口请求是由应用本身或系统发起的。
3.4 利用服务发现机制访问Binder对象
虽然基于安全考虑,Android 控制着对Binder 对象的访问,并且与 Binder 对象通信的唯一方法是使用该对象的引用,但是很多 Binder 对象(尤其是系统服务)需要全局可被访问。然而分发所有系统服务的引用到每个进程是不切实际的,所以需要一种机制,来允许进程按需发现和获取系统服务的引用。
为了使用服务发现机制, Binder 框架中拥有一个专门的上下文管理器(context manager)用于维护到 Binder 对象的引用。Android上下文管理器的实现就是 servicemanager 原生守护进程。它在开机的早期阶段就已启动,因此系统服务启动时就可以进行注册。服务通常传递服务名和一个 Binder引用到服务管理器进行注册。一旦服务注册成功,任何客户端都可以通过服务名获得它的 Binder 引用。然而,大部分系统服务有额外的权限检查,所以获得引用不能自动确保对它所有功能的访问。因为在服务管理器上注册之后,任何人都可以访问该Binder 引用,所以为了安全性的考量,只有一小部分在白名单内的系统进程才可以注册系统服务。比如,只有以 UID 1002(AID_BLUETOOTH)执行的进程,才可以注册bluetooth系统服务。
三、应用程序沙箱
1 概述
Android 应用程序沙箱是基于 Linux 用户和文件系统权限的安全模型。每个应用在安装时都会被分配一个唯一的用户 ID (UID),并在其自己的沙箱环境中运行。这意味着每个应用被隔离在自己的“盒子”中,无法访问其他应用的数据或代码,除非通过特定的方式授权。
2 沙箱的工作机制
2.1 基于 Linux 的用户隔离
每个应用程序在安装时会被分配一个唯一的用户 ID (UID)。在 Android 系统中,这个 UID 用来隔离应用程序的进程和数据。不同应用有不同的 UID,这就确保了一个应用无法直接访问另一个应用的文件。
应用的数据文件存储在特定的目录中,该目录只有该应用的 UID 有权限访问。默认情况下,其他应用甚至系统用户都无法访问这些文件。
2.2 进程隔离
每个应用程序运行在其自己的进程中,由操作系统创建并分配给它。这个进程空间由操作系统的进程管理机制控制,确保进程之间的内存隔离。
Android 支持多任务处理,但即使多个应用同时运行,它们的进程仍然被严格隔离,防止跨进程的数据泄露。
2.3 安全的 IPC(进程间通信)
Android 使用 Binder 机制来处理进程间的通信 (IPC)。尽管应用程序彼此隔离,但通过 Binder,可以实现受控的进程间通信。应用需要通过权限机制显式地暴露接口,其他应用才能通过 Binder 访问这些接口。
3 应用沙箱与权限管理
3.1 权限请求
某些操作(如访问网络、使用摄像头、读取联系人)需要特定的权限。应用程序必须在 AndroidManifest.xml
文件中声明所需的权限,并且用户在安装时或运行时(针对某些高敏感权限)需要授权这些权限。
从 Android 6.0 (API Level 23) 开始,引入了动态权限机制,某些敏感权限需要用户在运行时进行授权,而不仅仅是在安装时。
3.2 防止权限滥用
最小权限原则: Android 强调应用只申请它们实际需要的最小权限。用户可以通过应用设置查看并调整权限,进一步加强了用户对应用权限的控制。
· 权限隔离: 即使应用申请了多个权限,这些权限的作用范围仍然受到沙箱机制的严格控制,避免应用对权限的滥用。
4 共享数据的机制
尽管应用程序被沙箱隔离,但 Android 提供了几种安全的方式让应用程序共享数据或进行交互:
4.1 内容提供者 (Content Providers)
定义: 内容提供者是 Android 提供的一种组件,用于在不同应用之间共享数据。应用可以通过内容提供者暴露其数据,并通过权限机制控制其他应用的访问权限。
URI 访问控制: 访问内容提供者的数据通常通过 URI 实现,开发者可以细粒度地控制哪些数据可以被共享。
4.2 Intent
定义:Intent 是 Android 用于应用间通信的机制。通过 Intent,一个应用可以请求另一个应用执行某项操作,前提是该应用暴露了相应的 Activity 或 Service。
隐式 Intent 和显式 Intent: 显式 Intent 明确指定目标组件,而隐式 Intent 则依赖系统根据 Intent Filter 进行匹配,应用可以在 Intent 中包含权限信息来确保安全性。
4.3 文件共享
临时文件访问: Android 允许通过 FileProvider
机制来临时共享文件。应用可以生成一个包含文件路径的 URI,授权其他应用临时访问这个文件。
外部存储: 对于需要共享的文件,应用可以使用外部存储,但此类文件的访问权限受到外部存储的管理策略影响。
四、安卓代码签名和平台密钥
1 安卓代码签名
1.1 概念
代码签名是指对应用程序的 APK 文件进行数字签名,以保证应用程序的完整性和来源可信。每个 Android 应用在安装时都需要经过签名,这确保了应用程序的代码在分发和安装过程中没有被篡改。
1.2 签名方式
Android 使用 Java 的 jarsigner
工具或 Android Studio 提供的内置工具对 APK 进行签名。每个开发者都有一个私有的签名证书,用于对其应用进行签名。这个证书包含了开发者的公钥和其他标识信息,生成证书时通常使用 Java 的 keytool
工具。在签名时,开发者使用其私钥对 APK 中的所有文件生成一个数字签名,这些签名会和 APK 一起打包。安装时,Android 系统使用与开发者私钥对应的公钥来验证签名,以确保文件没有被修改。
1.3 签名的作用
Android 要求应用的更新版本必须使用与原版本相同的签名证书进行签名,以确保只有原开发者才能发布更新;通过签名,Android 系统可以确保只有特定开发者签名的应用才能访问特定权限或资源;只有使用相同签名证书的应用才能共享 UID,从而实现数据和资源共享。
2.安卓平台密钥
2.1 概念
平台密钥是 Android 系统的一个重要安全机制,用于签名 Android 系统组件(如系统应用和服务)。它是一个高权限的密钥,通常由设备制造商持有,用来签名系统的核心组件。
2.2 作用
平台密钥确保了系统应用和服务的完整性和来源的可信度。系统组件(如 Settings
、SystemUI
)使用平台密钥签名,以便在系统中享有较高的权限。
Android 系统中有些特定的权限(如 signature
权限)仅授予使用平台密钥签名的应用。这些权限允许应用执行一些普通应用无法执行的操作,比如访问系统 API、修改系统设置等。
2.3 使用场景
系统应用签名:系统应用使用平台密钥进行签名,确保它们可以访问一些普通应用无法访问的资源和服务。
设备制造商控制:平台密钥通常由设备制造商管理,并用于签名设备出厂时的系统映像和系统应用。
五、SELinux简介
1.SELinux基本概念
SELinux(Security-Enhanced Linux)是 Android 系统中用于强制访问控制(MAC)的一种安全机制。它最初由美国国家安全局(NSA)开发,并被引入 Android 以增强系统的安全性。
SELinux 是一种内核级的安全模块,通过定义并执行安全策略来控制进程和资源之间的交互。它可以防止进程以不受信任的方式访问系统资源,从而减少潜在的安全漏洞。
2.强制访问控制(MAC)
SELinux 通过强制访问控制(MAC)实现其安全目标。不同于传统的自主访问控制(DAC),MAC 不允许用户或进程随意更改访问权限。系统管理员通过策略文件明确规定了哪些进程可以访问哪些资源,这些策略是强制执行的,无法被用户或进程随意修改。
3.SELinux在安卓中的引入
3.1 引入背景
Android 从 4.3(Jelly Bean)版本开始引入 SELinux,最初以“宽容模式”(permissive mode)运行,即仅记录违反 SELinux 策略的行为,不实际阻止。自 Android 5.0(Lollipop)起,SELinux 进入“强制模式”(enforcing mode),开始实际执行策略并阻止不符合策略的操作。
3.2 SELinux 的目标
减少攻击面:通过限制进程可以执行的操作,SELinux 可以有效减少潜在的攻击面。例如,限制应用访问系统资源的能力。
增强系统稳定性:SELinux 防止恶意软件或受感染的应用程序破坏系统的关键组件,从而增强系统的整体稳定性和安全性。
4. SELinux策略
4.1 策略类型
SELinux 策略由一系列规则组成,定义了系统中不同类型的对象(如文件、进程、设备)的安全上下文(Security Context),并指定了哪些操作是允许的。主要包括:
1.类型策略(Type Enforcement, TE):这是 SELinux 中最常用的策略,定义了进程可以访问哪些类型的对象以及可以执行哪些操作。
2.角色策略(Role-Based Access Control, RBAC):基于角色的访问控制策略,限制不同角色可以执行的操作。
3.多级安全策略(Multi-Level Security, MLS):用于更高级别的安全控制,适用于需要复杂安全分级的环境。
4.2 安全上下文
SELinux 为每个进程、文件、网络端口等分配一个安全上下文(Security Context),它通常由用户、角色、类型和敏感度级别组成。一个典型的安全上下文可能是这样的:
u:r:init:s0
- u 代表用户(user)
- r 代表角色(role)
- init 代表类型(type)
- s0 代表敏感度级别(sensitivity level)
4.3 策略编译与加载
SELinux 策略通常以源代码形式编写,定义了系统中所有允许的行为。然后,这些策略被编译成二进制格式并加载到内核中,内核根据这些策略对进程的操作进行实时检查和控制。
4.4 SELinux 在 Android 中的实现
初始化与加载
Android 系统启动时,会加载预先定义好的 SELinux 策略,并根据这些策略初始化系统的安全上下文。这些策略文件通常由设备制造商定义,并包含了对系统关键进程、服务和资源的严格控制。
日志与调试
在 SELinux 强制模式下,所有违反 SELinux 策略的行为都会被阻止,并记录在系统日志中。开发者可以使用 dmesg
或 logcat
查看这些日志,从而调试和修复策略相关的问题。
宽容模式和强制模式
宽容模式(Permissive Mode):系统记录策略违规行为但不实际阻止操作。这种模式主要用于调试和测试。
强制模式(Enforcing Mode):系统不仅记录违规行为,还实际阻止这些行为,强制执行 SELinux 策略。
5. SELinux 的实际应用
5.1 应用隔离
通过 SELinux,Android 能够严格隔离应用程序,防止恶意应用获取系统级权限或访问其他应用的敏感数据。
5.2 防止权限升级攻击
SELinux 限制了进程能够执行的操作,从而减少权限升级攻击的可能性,即使某个应用获得了不应有的权限,SELinux 也会限制其危害。
5.3 保护系统进程
关键的系统进程如 init
、zygote(Zygote
是 Android 系统的核心进程之一,通过预加载类和资源,以及高效的 fork
机制,它显著提高了应用程序的启动速度和系统性能)等受到 SELinux 的严格保护,只有被明确允许的进程才能与之交互。