-
-
[翻译][原创]Android平台概述
-
发表于: 2022-8-26 15:00 4637
-
前言
此文翻译自mobile-security中的Platform Overview片段
平台概述
本章从架构的角度来介绍 Android 平台。讨论了以下五个关键领域:
- Android 架构
- Android 安全:纵深防御方法
- Android 应用结构
- Android 应用发布
- Android 应用程序攻击面
有关 Android 平台的更多详细信息,请访问官方Android 开发者文档网站。
Android 架构
Android是谷歌开发的基于Linux的开源平台,作为移动操作系统(OS)。今天,该平台是各种现代技术的基石,例如手机、平板电脑、可穿戴技术、电视和其他“智能”设备。典型的 Android 系统附带一系列预装(“库存”)应用程序,并支持通过 Google Play 商店和其他市场安装第三方应用程序。
Android 的软件栈由几个不同的层次组成。每一层都定义了相应的接口并提供特定的服务。
在最低级别,Android 基于 Linux 内核的变体。在内核之上,硬件抽象层 (HAL,Hardware Abstraction Layer) 定义了用于与内置硬件组件交互的标准接口。几个 HAL 实现被打包到Android 系统需要调用的共享库模块中。这是允许应用程序与设备硬件交互的基础。例如,它允许自带的手机应用程序使用设备的麦克风和扬声器。
Android 应用程序通常使用 Java 编写并编译为 Dalvik 字节码,这与传统的 Java 字节码有些不同。Dalvik 字节码的创建方法是首先将 Java 代码编译成 .class 文件,然后使用该d8
工具将 JVM 字节码转换为 Dalvik .dex 格式。
当前版本的 Android 在 ART虚拟机 (ART,Android Runtime) 上执行此字节码。ART 是 Android 原始运行时 Dalvik 虚拟机 (DVM) 的继承者。Dalvik 和 ART 之间的主要区别在于字节码的执行方式。
在 DVM 中,字节码在执行时被翻译成机器码,这一过程称为即时(JIT,Just-In-Time) 编译。这使执行能够受益于编译代码的速度,同时保持代码解释的灵活性。为了进一步提高性能,Android 引入了Android Runtime (ART)来替代 DVM。ART 使用混合编译模式,即超前编译(AOT)、即时编译(JIT) 和配置文件引导编译(Profile-Guided Compilation)。应用程序在安装时或操作系统进行重大更新时会在设备上重新编译。在重新编译代码时,可以使用基于特定设备的高级代码优化技术。然后将最终重新编译完的代码用于后续的所有执行。由于特定设备的优化,AOT 在降低功耗的同时将性能提高了两倍。
Android 应用程序无法直接访问硬件资源,每个应用程序都在自己的虚拟机或沙箱中运行。这使操作系统能够精确控制设备上的资源和内存访问。例如,崩溃的应用程序不会影响在同一台设备上运行的其他应用程序。Android 控制分配给应用的系统资源的最大数量,防止任何一个应用独占过多的资源。同时,这种沙盒设计也算是Android 全球纵深防御战略中的众多规范之一。具有低权限的恶意第三方应用程序不能够逃脱自己的沙箱并读取同一设备上其他受害应用程序的内存。在下一节中,我们将仔细研究 Android 操作系统中的不同防御层。
Android 安全:纵深防御方法
Android 架构实现了不同的安全层,它们共同实现了纵深防御方法。这意味着敏感用户数据或应用程序的机密性、完整性或可用性并不取决于单一的安全措施。本节概述了 Android 系统提供的不同防御层。安全策略可以大致分为四个不同的领域,每个领域都侧重于防止某些攻击模型。
- 系统范围的安全性
- 软件隔离
- 网络安全
- 反利用
系统范围的安全性
设备加密
Android 支持设备加密是从 Android 2.3.4(API 级别 10)开始的,并且从那时起发生了一些重大变化。Google 强制要求所有运行 Android 6.0(API 级别 23)或更高版本的设备都必须支持存储加密。因为这会显着影响其设备性能,所以一些低端设备可以不支持加密。
- 全盘加密(FDE,Full-Disk Encryption):全盘加密是使用密钥(密钥本身也经过加密)对 Android 设备上的所有用户数据进行编码的过程。设备经过加密后,所有由用户创建的数据在存入磁盘之前都会自动加密,并且所有读取操作都会在将数据返回给调用进程之前自动解密数据。Android 5.0(API 级别 21)及以上支持全盘加密。此加密使用受用户设备密码保护的单个密钥来加密和解密用户数据分区。这种加密现在被看作已弃用,应尽可能使用基于文件的加密。全盘加密有缺陷,例如如果用户不输入密码解锁,则无法接听电话或重启后没有有效警报。
- 基于文件的加密(FBE,File-Based Encryption):Android 7.0(API 级别 24)支持基于文件的加密。基于文件的加密允许使用不同的密钥对不同的文件进行加密,从而可以独立地对它们进行解密。支持此类加密的设备也支持阻塞开机(Direct Boot,当设备已开机但用户尚未解锁设备时,Android 7.0 将在安全的“直接启动”模式下运行)。即使用户没有解锁设备,阻塞开机也使设备能够访问警报或辅助服务等功能。
注意:您可能听说过Adiantum,这是一种为运行 Android 9(API 级别 28)及更高版本且 CPU 缺乏 AES 指令的加密方法。Adiantum 仅与 ROM 开发人员或设备供应商相关,Android 不提供 API 供开发人员从应用程序中使用 Adiantum。根据 Google 的建议,在发布带有 ARMv8 加密扩展的基于 ARM 的设备或带有 AES-NI 的基于 x86 的设备时,不应使用 Adiantum。AES 在这些平台上更快。
Android 文档中提供了更多信息。
可信执行环境 (TEE,Trusted Execution Environment)
为了让 Android 系统执行加密,它需要一种方法去安全地生成、导入和存储加密密钥。基本上我们正在将保持敏感数据安全的问题转移到保持加密密钥的安全上。如果攻击者可以转储或猜测密钥,则可以检索敏感的加密数据。
Android 在专用硬件中提供可信执行环境,以解决安全生成和保护加密密钥的问题。这意味着 Android 系统中的专用硬件组件负责去处理加密密钥资料。三个主要模块负责此操作:
硬件支持的密钥库(Hardware-backed KeyStore):该模块为 Android 操作系统和第三方应用程序提供加密服务。它使应用程序能够在 TEE 中执行加密敏感操作,而不会暴露加密密钥资料。
StrongBox:在 Android 9(Pie)中,引入了 StrongBox,这是实现硬件支持的密钥库(Hardware-backed KeyStore)的另一种方法。在 Android 9 Pie 之前,硬件支持的密钥库(Hardware-backed KeyStore)是位于 Android OS 内核之外的任何 TEE 实现。StrongBox 是一个实际完整的独立硬件芯片,它被添加到实现密钥库的设备中,并在 Android 文档中明确定义。您可以以编程方式检查密钥是否位于 StrongBox 中,如果存在,则可以确保它受到硬件安全模块的保护,该模块具有自己的 CPU、安全存储和真随机数生成器 (TRNG,True Random Number Generator)。在 StrongBox 的安全边界内,所有敏感的加密操作都发生在这个芯片上。
GateKeeper:GateKeeper 模块启用设备模式和密码认证。身份验证过程中的安全敏感操作发生在设备上可用的 TEE 内。GateKeeper 由三个主要的组件组成,(1)
gatekeeperd
,它是用来公开GateKeeper 的服务,(2) GateKeeper HAL,它是硬件接口 (3) TEE 实现,它是在 TEE 中实现 GateKeeper 功能的实际软件。
验证启动模式(Verified Boot)
我们需要有一种方法来确保在 Android 设备上执行的代码来自受信任的来源,并且其完整性不会受到损害。为了实现这一点,Android 引入了验证启动模式(Verified Boot)的概念。验证启动模式的目标是在硬件和在该硬件上执行的实际代码之间建立信任关系。在验证启动模式进行期间,从受硬件保护的信任根 (RoT,Root-of-Trust) 到正在运行的最终系统,将建立一条通过并验证所有必需的引导阶段的完整信任链。当 Android 系统最终启动时,您可以放心,系统不会被篡改。您有加密证明,正在运行的代码是 OEM 预期的代码,而不是被恶意或意外更改的代码。
Android 文档中提供了更多信息。
软件隔离
Android 用户和组
尽管 Android 操作系统是基于 Linux 的,但它并没有像其他类 Unix 系统那样实现用户帐户。在 Android 中,Linux 内核的多用户支持被用于沙箱应用程序:除了少数例外,每个应用程序都像在单独的 Linux 用户下运行一样,有效地与其他应用程序和操作系统的其余部分隔离。
system/core/include/private/android_filesystem_config.h文件包含着系统进程分配到的预先定义好的用户和组的列表。安装其他应用程序时会添加其他应用程序的 UID(用户 ID)。有关更多详细信息,请查看 Bin Chen关于 Android 沙箱的博客推文
例如,Android 7.0(API 级别 24)定义了以下系统用户:
1 2 3 4 5 6 7 | #define AID_ROOT 0 /* 传统的unix root 用户 */ #define AID_SYSTEM 1000 / * 系统服务器 */ # ... #define AID_SHELL 2000 /* adb和调试 shell 用户 */ # ... #define AID_APP 10000 /* 第一个应用用户 */ ... |
SELinux
Security-Enhanced Linux (SELinux) 使用强制访问控制 (MAC,Mandatory Access Control) 系统来进一步锁定哪些进程应该有权访问哪些资源。每个资源都有一个标签,其形式为user:role:type:mls_level
,这个标签定义了哪些用户能够对其执行哪些类型的操作。例如,一个进程可能只能读取一个文件,而另一个进程可能能够编辑或删除该文件。这样,通过采用最小特权原则,易受攻击的进程更难通过提权或横向移动来利用。
更多信息可在Android 文档中找到。
权限
Android 实现了一个广泛的权限系统,用作访问控制机制。它可确保对敏感用户数据和设备资源的受控访问。Android 将权限分为不同类型,提供不同的保护级别。
在 Android 6.0(API 级别 23)之前,应用请求的所有权限都在安装时授予(安装时权限)。从 API 级别 23 开始,用户必须在运行时批准一些权限请求(运行时权限)。
Android 文档中提供了更多信息,包括一些注意事项和最佳做法
要了解如何测试应用权限,请参阅“Android 平台 API”一章中的测试应用权限部分。
网络安全
默认情况下使用TLS协议
默认情况下,从 Android 9(API 级别 28)开始,所有网络活动都被视为在敌对环境中执行。这意味着 Android 系统将只允许应用程序通过使用传输层安全 (TLS) 协议建立的网络通道进行通信。该协议有效地加密了所有网络流量并创建了通往服务器的安全通道。出于历史遗留问题,您可能希望使用明确的流量连接。这可以通过调整res/xml/network_security_config.xml
应用程序中的文件来实现。
Android 文档中提供了更多信息。
基于 TLS 的 DNS
自 Android 9(API 级别 28)以来,系统范围内的 DNS over TLS 支持已被引入。它允许您使用 TLS 协议对 DNS 服务器执行查询。与发送 DNS 查询的 DNS 服务器建立安全通道。这可确保在 DNS 查找期间不会暴露敏感数据。
更多信息可在Android 开发者博客上找到。
反利用
ASLR、KASLR、PIE 和 DEP
地址空间布局随机化 (ASLR,Address Space Layout Randomization),自 Android 4.1(API 级别 15)以来一直是 Android 的一部分,是针对缓冲区溢出攻击的标准保护,它确保应用程序和操作系统都加载到随机内存地址且很难获得特定内存区域或库的正确地址。在 Android 8.0(API 级别 26)中,还为内核 (KASLR,Kernel Address Space Layout Randomization) 实施了这种保护。只有当应用程序可以在内存中的随机位置加载时,才有可能实现ASLR保护,这由应用程序的位置独立可执行性(PIE,Position Independent Executable)标志来表示。从 Android 5.0(API 级别 21)开始,不再支持不支持 PIE 的本机库。最后,数据执行保护 (DEP,Data Execution Prevention) 可防止在堆栈上执行代码,这也用于对抗缓冲区溢出漏洞。
更多信息可在Android 开发者博客上找到。
SECCOMP 过滤器
Android 应用程序可以包含用 C 或 C++ 编写的本机代码。这些编译后的二进制文件既可以通过 Java Native接口 (JNI) 绑定与 Android 运行时通信,也可以通过系统调用与操作系统通信。一些系统调用要么没有实现,要么不应该被普通应用程序调用。由于这些系统调用直接与内核通信,因此它们是黑客的主要目标。在 Android 8(API 级别 26)中,Android 为所有基于 Zygote 的进程(即用户应用程序)引入了对安全计算 (SECCOMP,Secure Computing) 过滤器的支持。这些过滤器将可用的系统调用限制为那些通过使用Bionic库的系统调用。
更多信息可在Android 开发者博客上找到。
Android 应用结构
与操作系统的通信
Android 应用程序通过 Android Framework与系统服务交互,这是一个提供高级 Java API 的抽象层。这些服务中的大多数是通过普通的 Java 方法来调用的,并被转换为在后台运行的系统服务的 IPC 调用。系统服务的示例包括:
- 连接性(Wi-Fi、蓝牙、NFC 等)
- 文件
- 相机
- 地理位置(GPS)
- 麦克风
该框架还提供了常见的安全功能,例如加密功能。
API 规范会随着每个新的 Android 版本而变化。关键错误修复和安全补丁通常也适用于早期版本。在撰写本文时支持的最旧的 Android 版本是 Android 8.1(API 级别 27),当前的 Android 版本是 Android 10(API 级别 29)。
值得注意的 API 版本:
- 2012 年 11 月 Android 4.2(API级别16)(SELinux介绍)
- 2013 年 7 月的 Android 4.3(API 级别 18)(默认启用 SELinux)
- 2013 年 10 月的 Android 4.4(API 级别 19)(引入了几个新的 API 和 ART)
- 2014 年 11 月的 Android 5.0(API 级别 21)(默认使用 ART 并添加了许多其他功能)
- 2015 年 10 月的 Android 6.0(API 级别 23)(许多新功能和改进,包括授予(granting);运行时设置详细的权限,而不是在安装期间赋予全部或者不给权限)
- 2016 年 8 月的 Android 7.0(API 级别 24-25)(ART 上的新 JIT 编译器)
- 2017 年 8 月的 Android 8.0(API 级别 26-27)(大量安全改进)
- 2018 年 8 月的 Android 9(API 级别 28)(限制麦克风或摄像头的后台使用,引入锁定模式,所有应用的默认 HTTPS)
- 2019 年 9 月的 Android 10(API 级别 29)(通知气泡,项目主线)
应用沙箱
应用程序在 Android 应用程序沙箱中执行,它将应用程序数据和代码执行与设备上的其他应用程序分开。如前所述,这种分离增加了第一层防御。
安装新应用程序会创建一个以应用程序包命名的新目录,从而生成以下路径:/data/data/[package-name]
. 此目录保存应用程序的数据。Linux 目录权限设置为只能使用应用程序的唯一UID进行读取和写入目录。
我们可以通过查看文件夹/data/data
中的文件系统权限来确认这一点。例如,我们可以看到谷歌浏览器和日历都被分配了一个目录,并在不同的用户帐户下运行:
1 2 | drwx - - - - - - 4 u0_a97 u0_a97 4096 2017 - 01 - 18 14 : 27 com.android.calendar drwx - - - - - - 6 u0_a120 u0_a120 4096 2017 - 01 - 19 12 : 54 com.android.chrome |
开发者希望他们的应用程序共享一个公共沙箱以回避沙箱机制。当两个应用使用相同的证书签名并明确共享相同的用户 ID(在其AndroidManifest.xml文件中具有sharedUserId)时,每个应用都可以访问对方的数据目录。请参阅以下示例以在 NFC 应用程序中实现此目的:
1 2 3 | <manifest xmlns:android = "http://schemas.android.com/apk/res/android" package = "com.android.nfc" android:sharedUserId = "android.uid.nfc" > |
Linux 用户管理
Android 利用 Linux 用户管理来隔离应用程序。这种方法与传统 Linux 环境中的用户管理使用不同,在传统 Linux 环境中,多个应用程序通常由同一用户运行。Android 为每个 Android 应用程序创建一个唯一的 UID,并在单独的进程中运行该应用程序。因此,每个应用程序只能访问自己的资源。这种保护由 Linux 内核强制执行。
通常,应用程序被分配在 10000 和 99999 范围内的 UID。Android 应用程序根据其 UID 接收用户名。例如,UID 为 10188 的应用程序接收用户名u0_a188
。如果授予应用程序请求的权限,则将相应的组 ID 添加到应用程序的进程中。比如下面这个app的用户ID是10188,属于组ID 3003(inet)。该组与 android.permission.INTERNET (联网相关)权限有关。该id
命令的输出如下所示。
1 2 | $ id uid = 10188 (u0_a188) gid = 10188 (u0_a188) groups = 10188 (u0_a188), 3003 (inet), 9997 (everybody), 50188 (all_a188) context = u:r:untrusted_app:s0:c512,c768 |
组 ID 和权限之间的关系在以下文件中定义:
frameworks/base/data/etc/platform.xml
1 2 3 4 5 6 7 8 9 10 11 12 | <permission name = "android.permission.INTERNET" > <group gid = "inet" / > < / permission> <permission name = "android.permission.READ_LOGS" > <group gid = "log" / > < / permission> <permission name = "android.permission.WRITE_MEDIA_STORAGE" > <group gid = "media_rw" / > <group gid = "sdcard_rw" / > < / permission> |
Zygote
进程Zygote
在Android 初始化期间启动。Zygote 是用于启动应用程序的系统服务。Zygote 进程是一个“基础”进程,其中包含应用程序所需的所有核心库。启动时,Zygote 打开socket/dev/socket/zygote
并侦听来自本地客户端的连接。当它接收到一个连接时,它会forks一个新进程,然后加载并执行特定于应用程序的代码。
应用生命周期
在安卓系统中,一个应用进程的寿命由操作系统控制。当一个应用组件被启动,而同一个应用还没有任何其他组件在运行时,就会创建一个新的Linux进程。当后者不再需要时,或者为了运行更重要的应用程序而需要回收内存时,Android可能会杀死这个进程。杀死一个进程的决定主要与用户与该进程的交互状态有关。一般来说,进程可以处于四种状态中的一种。
前台进程(例如,在屏幕顶部运行的活动或正在运行的BroadcastReceiver)
可见进程是用户知道的进程,因此杀死它会对用户体验产生明显的负面影响。一个示例是运行在屏幕上可见但在前台不可见的活动。
服务进程是托管已使用
startService
方法启动的服务的进程。虽然这些进程对用户来说不是直接可见的,但它们通常是用户关心的事情(例如后台网络数据上传或下载),因此系统会一直保持这些进程运行,除非没有足够的内存来保留所有前台和可见的进程。缓存进程是当前不需要的进程,因此系统可以在需要内存时随便地终止它。应用程序必须实现对许多事件作出反应的回调方法;例如,在
onCreate
首次创建应用程序进程时调用处理程序。其他回调方法包括onLowMemory
,onTrimMemory
和onConfigurationChanged
.
App Bundles
Android 应用程序可以以两种形式发布:Android Package Kit (APK) 文件或Android App Bundle (.aab)。Android App Bundle 提供应用所需的所有资源,但推迟生成APK并将其签名给Google Play。App Bundles 是经过签名的二进制文件,其中包含多个模块中的应用程序代码。基本模块包含应用程序的核心。基本模块可以使用各种模块进行扩展,这些模块包含应用程序的新的丰富内容和功能,如app bundle 的开发人员文档中进一步说明的那样。如果您有 Android App Bundle,最好使用来自 Google的bundletool 命令行工具,用于构建未签名的 APK,以便使用 APK 上的现有工具。您可以通过运行以下命令从 AAB 文件创建 APK:
1 | $ bundletool build - apks - - bundle = / MyApp / my_app.aab - - output = / MyApp / my_app.apks |
如果您想创建已签名的 APK 以准备部署到测试设备,请使用:
1 2 3 4 5 | $ bundletool build - apks - - bundle = / MyApp / my_app.aab - - output = / MyApp / my_app.apks - - ks = / MyApp / keystore.jks - - ks - pass = file : / MyApp / keystore.pwd - - ks - key - alias = MyKeyAlias - - key - pass = file : / MyApp / key.pwd |
我们建议您测试带有和不带有附加模块的 APK,以便清楚附加模块是否会引入或者修复基本模块的安全问题。
Android Manifest
每个应用程序都有一个 Android Manifest 文件,该文件以二进制 XML 格式嵌入内容。此文件的标准名称是 AndroidManifest.xml。它位于应用程序的 Android Package Kit (APK) 文件的根目录中。
清单文件描述了应用程序结构、其组件(activities、services、content providers和 intent receivers)以及请求的权限。它还包含一般应用程序元数据,例如应用程序的图标、版本号和主题。该文件可能会列出其他信息,例如兼容的 API(最小、目标和最大 SDK 版本)以及可以安装的存储类型(外部或内部)。
这是清单文件的示例,包括包名称(全类名限定,但任何字符串都可以接受)。它还列出了应用程序版本、相关 SDK、所需权限、公开的content providers、与 intent filters一起使用的 broadcast receivers 以及应用程序及其activities的描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <manifest package = "com.owasp.myapplication" android:versionCode = "0.1" > <uses - sdk android:minSdkVersion = "12" android:targetSdkVersion = "22" android:maxSdkVersion = "25" / > <uses - permission android:name = "android.permission.INTERNET" / > <provider android:name = "com.owasp.myapplication.MyProvider" android:exported = "false" / > <receiver android:name = ".MyReceiver" > <intent - filter > <action android:name = "com.owasp.myapplication.myaction" / > < / intent - filter > < / receiver> <application android:icon = "@drawable/ic_launcher" android:label = "@string/app_name" android:theme = "@style/Theme.Material.Light" > <activity android:name = "com.owasp.myapplication.MainActivity" > <intent - filter > <action android:name = "android.intent.action.MAIN" / > < / intent - filter > < / activity> < / application> < / manifest> |
可用清单选项的完整列表在官方Android 清单文件文档中。
App Components
Android 应用程序由几个高级组件组成。主要成分是:
- Activities
- Fragments
- Intents
- Broadcast receivers
- Content providers 和 services
所有这些组件都由 Android 操作系统以通过 API 提供的预定义类的形式提供。
Activities
Activities构成了任何应用程序的可见部分。每个屏幕有一个Activities,因此具有三个不同屏幕的应用程序实现了三个不同的Activities。Activities是通过扩展 Activity 类来声明的。它们包含所有用户界面的元素:fragments、views和layouts。
每个activity 都需要在 Android Manifest 中声明,语法如下:
1 2 | <activity android:name = "ActivityName" > < / activity> |
清单中未声明的Activities 无法显示,尝试启动它们会引发异常。
与应用程序一样,Activity 也有自己的生命周期,需要监控系统更改来处理它们。Activities 可以处于以下状态:active、paused、stopped和inactive。这些状态由 Android 操作系统管理。因此,activities 可以实现以下事件管理器:
- onCreate
- onSaveInstanceState
- onStart
- onResume
- onRestoreInstanceState
- onPause
- onStop
- onRestart
- onDestroy
应用程序可能不会显式实现所有事件管理器,在这种情况下会采取默认操作。通常,onCreate
管理器被应用程序开发人员覆盖。这是大多数用户界面组件的声明和初始化方式。onDestroy
必须显式释放资源(如网络连接或与数据库的连接)或在应用程序关闭时必须执行特定操作时,可能会被覆盖。
Fragments
Fragments表示activity中的行为或用户界面的一部分。Fragments 是在 Honeycomb 3.0(API 级别 11)版本的 Android 中引入的。
Fragments 旨在封装界面的一部分,以促进可重用性和适应不同的屏幕尺寸。Fragments 是自主的实体,因为它们包含所有必备的组件(它们有自己的布局、按钮等)。但是,它们必须与activities 集成才能发挥作用:Fragments 不能单独存在。它们有自己的生命周期,与实现它们的Activities 的生命周期相关联。
因为 Fragment 有自己的生命周期,所以 Fragment 类包含可以重新定义和扩展的事件管理器。这些事件管理器包括 onAttach、onCreate、onStart、onDestroy 和 onDetach。还有其他几个;读者应参考Android Fragment 规范了解更多详情。
通过扩展 Android 提供的 Fragment 类可以轻松实现 Fragment:
Java 中的示例:
1 2 3 | public class MyFragment extends Fragment { ... } |
Kotlin 中的示例:
1 2 3 | class MyFragment : Fragment() { ... } |
Fragment不需要在清单文件中声明,因为它们依赖于activities。
要管理其Fragment,activities可以使用 Fragment 管理器(FragmentManager 类)。此类使查找、添加、删除和替换相关 Fragment变得容易。
Fragment管理器可以通过以下方式创建:
Java 中的示例:
1 | FragmentManager fm = getFragmentManager(); |
Kotlin 中的示例:
1 | var fm = fragmentManager |
Fragment不一定有用户界面;它们可以是管理与应用程序用户界面相关的后台操作的一种方便有效的方式。一个Fragment 可以被声明为持久的,这样即使它的 Activity 被销毁,系统也会保留它的状态。
Content Providers
Android 使用 SQLite 永久存储数据:与 Linux 一样,数据存储在文件中。SQLite 是一种轻量级、高效、开源的关系型数据库,不需要太多的处理能力,非常适合移动使用。具有特定类(Cursor、ContentValues、SQLiteOpenHelper、ContentProvider、ContentResolver 等)的完整 API 可用。SQLite 不作为单独的进程运行;它是应用程序的一部分。默认情况下,属于给定应用程序的数据库只能由该应用程序访问。然而,Content Providers为抽象数据源(包括数据库和文件)提供了很好的机制;它们还提供了一种标准且高效的机制来在应用程序之间共享数据。要可供其他应用程序访问,需要在共享它的应用程序的清单文件(AndroidManifest.xml)中明确声明Content Providers。只要不声明Content Providers,它们就不会被导出,并且只能由创建它们的应用程序调用。
Content Providers通过 URI 寻址协议实现:它们都使用 content://
模型。无论源的类型如何(SQLite 数据库、文件等),寻址协议始终相同,从而抽象了源并为开发人员提供了独特的方案。Content Providers提供所有常规数据库操作:创建、读取、更新、删除。这意味着任何在其清单文件中具有适当权限的应用程序都可以操作来自其他应用程序的数据。
Services
服务是在后台执行任务(数据处理、启动intents和通知等)而不显示用户界面的 Android 操作系统组件(基于 Service 类)。服务意味着长期运行进程。它们的系统优先级低于活动的应用程序的系统优先级,高于非活动的应用程序的系统优先级。因此,它们不太可能在系统需要资源时被杀死,并且可以将它们配置为在有足够资源可用时自动重启。这使得服务成为运行后台任务的理想选择。请注意,服务和Activities一样,在主应用程序线程中执行。除非您另外指定,否则服务不会创建自己的线程并且不会在单独的进程中运行。
进程间通信
正如我们已经了解到的,每个 Android 进程都有自己的沙箱地址空间。进程间通信设施允许应用程序安全地交换信号和数据。Android 的 IPC 不是依赖于默认的 Linux IPC 工具,而是基于 Binder,Binder是一个 OpenBinder 的自定义实现。大多数 Android 系统服务和所有高级 IPC 服务都依赖于 Binder。
Binder一词代表许多不同的事物,包括:
Binder Driver:内核级驱动
Binder Protocol:用于与 binder 驱动程序通信的基于 ioctl 的低级协议
IBinder Interface:Binder 对象实现的明确定义的行为
Binder object:IBinder 接口的通用实现
Binder service:Binder 对象的具体实现;例如,位置服务和传感器服务
Binder client:使用 Binder 服务的对象
Binder 框架包括一个客户端-服务器通信模型。要使用 IPC,应用程序会调用代理对象中的 IPC 方法。代理对象透明地将调用参数集合到一个包中,并将事务发送到Binder服务器,该服务器被实现为字符驱动程序(/dev/binder)。服务器拥有一个线程池,用于处理传入请求并将消息传递给目标对象。从客户端应用程序的角度来看,所有这些似乎都是一个常规的方法调用,所有繁重的工作都由 Binder 框架完成。
Binder 概述 - 图片来源:Thorsten
允许其他应用程序绑定到它们的服务称为服务绑定。这些服务必须为客户端提供一个 IBinder 接口。开发人员使用 Android 接口描述符语言 (AIDL,Android Interface Descriptor Language) 为远程服务编写接口。
ServiceManager 是一个系统守护进程,负责管理系统服务的注册和查找。它维护所有已注册服务的name/Binder 对列表。使用以下静态方法addService
添加和检索服务getService
,这些方法android.os.ServiceManager
类中。
Java 中的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public static IBinder getService(String name) { try { IBinder service = sCache.get(name); if (service ! = null) { return service; } else { return getIServiceManager().getService(name); } } catch (RemoteException e) { Log.e(TAG, "error in getService" , e); } return null; } |
Kotlin 中的示例:
1 2 3 4 5 6 7 8 9 10 11 12 | companion object { private val sCache: Map <String, IBinder> = ArrayMap() fun getService(name: String): IBinder? { try { val service = sCache[name] return service ?: getIServiceManager().getService(name) } catch (e: RemoteException) { Log.e(FragmentActivity.TAG, "error in getService" , e) } return null } } |
您可以使用该service list
命令查询系统服务列表。
1 2 3 4 5 6 | $ adb shell service list Found 99 services: 0 carrier_config: [com.android.internal.telephony.ICarrierConfigLoader] 1 phone: [com.android.internal.telephony.ITelephony] 2 isms: [com.android.internal.telephony.ISms] 3 iphonesubinfo: [com.android.internal.telephony.IPhoneSubInfo] |
Intent
Intent 消息传递是一个建立在 Binder 之上的异步通信框架。该框架允许点对点和发布-订阅消息传递。Intent是一个消息传递对象,可用于从另一个应用程序组件去请求操作。尽管Intent以多种方式促进组件间通信,但存在三个基本用例:
开启一个activity
- 一个activity代表应用程序中的单个屏幕。您可以通过将Intent传递给
startActivity
. Intent描述activity并携带必要的数据。
- 一个activity代表应用程序中的单个屏幕。您可以通过将Intent传递给
启动服务
- 服务是在后台执行操作的组件,没有用户界面。在 Android 5.0(API 级别 21)及更高版本中,您可以使用 JobScheduler 启动服务。
传送广播
- 广播是任何应用程序都可以接收的消息。系统为系统事件提供广播,包括系统启动和充电初始化。您可以通过将intent来向其他应用程序发送广播,使用
sendBroadcast
或sendOrderedBroadcast
。
- 广播是任何应用程序都可以接收的消息。系统为系统事件提供广播,包括系统启动和充电初始化。您可以通过将intent来向其他应用程序发送广播,使用
有两种类型的Intent。显式Intent命名将启动的组件(完全限定的类名)。例如:
Java 中的示例:
1 | Intent intent = new Intent(this, myActivity.myClass); |
Kotlin 中的示例:
1 | var intent = Intent(this, myActivity.myClass) |
隐式Intent被发送到操作系统以对给定的数据集执行给定的操作(在我们下面的示例中为 OWASP 网站的 URL)。由系统决定哪个应用程序或类将执行相应的服务。例如:
Java 中的示例:
1 | Intent intent = new Intent(Intent.MY_ACTION, Uri.parse( "https://www.owasp.org" )); |
Kotlin 中的示例:
1 | var intent = Intent(Intent.MY_ACTION, Uri.parse( "https://www.owasp.org" )) |
Intent 过滤器是 Android Manifest 文件中的一个表达式,用于指定组件想要接收的 Intent 类型。例如,通过为 Activity 声明 Intent 过滤器,您可以让其他应用程序直接以某种 Intent 启动您的 Activity。同样,如果您没有为其声明任何Intent 过滤器,您的Activity只能以显式 Intent启动。
Android 使用 Intent 向应用广播消息(例如来电或 SMS)重要的电源信息(例如低电量)和网络变化(例如失去连接)。可以将额外数据添加到Intent (通过putExtra
/ getExtras
)。
这是操作系统发送的Intent的简短列表。所有常量都在 Intent 类中定义,整个列表在 Android 官方文档中:
ACTION_CAMERA_BUTTON
ACTION_MEDIA_EJECT
ACTION_NEW_OUTGOING_CALL
ACTION_TIMEZONE_CHANGED
为了提高安全性和隐私性,本地广播管理器用于在应用程序内发送和接收Intent,而无需将它们发送到操作系统的其余部分。这对于确保敏感和私有数据不会离开应用程序边界(例如地理位置数据)非常有用。
Broadcast Receivers
广播接收器是允许应用程序从其他应用程序和系统本身接收通知的组件。有了它们,应用程序可以对事件(内部的、由其他应用程序启动的或由操作系统启动的)做出反应。它们通常用于更新用户界面、启动服务、更新内容和创建用户通知。
有两种方法可以让系统知道广播接收器。一种方法是在 Android Manifest 文件中声明它。清单应指定广播接收器和Intent过滤器之间的关联,以指示接收器要侦听的操作。
清单中带有Intent过滤器的广播接收器声明示例:
1 2 3 4 5 | <receiver android:name = ".MyReceiver" > <intent - filter > <action android:name = "com.owasp.myapplication.MY_ACTION" / > < / intent - filter > < / receiver> |
请注意,在此示例中,广播接收器不包含该android:exported
属性。由于至少定义了一个Intent过滤器,因此默认值将设置为“true”。在没有任何过滤器的情况下,它将被设置为“false”。
另一种方法是在代码中动态创建接收器。接收者可以使用注册方法Context.registerReceiver
)
动态注册广播接收器的示例:
Java 中的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | / / Define a broadcast receiver BroadcastReceiver myReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Intent received by myReceiver" ); } }; / / Define an intent filter with actions that the broadcast receiver listens for IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction( "com.owasp.myapplication.MY_ACTION" ); / / To register the broadcast receiver registerReceiver(myReceiver, intentFilter); / / To un - register the broadcast receiver unregisterReceiver(myReceiver); |
Kotlin 中的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 | / / Define a broadcast receiver val myReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Log.d(FragmentActivity.TAG, "Intent received by myReceiver" ) } } / / Define an intent filter with actions that the broadcast receiver listens for val intentFilter = IntentFilter() intentFilter.addAction( "com.owasp.myapplication.MY_ACTION" ) / / To register the broadcast receiver registerReceiver(myReceiver, intentFilter) / / To un - register the broadcast receiver unregisterReceiver(myReceiver) |
请注意,当引发相关的Intent时,系统会自动使用已注册的接收器启动应用程序。
根据Broadcasts Overview,如果广播没有专门针对应用程序,则它被认为是“隐式”的。收到隐式广播后,Android 将列出所有在其过滤器中注册给定操作的应用程序。如果多个应用注册了同一操作,Android 将提示用户从可用应用列表中进行选择。
广播接收器的一个有趣特性是它们可以被优先处理。这样,Intent将根据其优先级传递给所有授权的接收者。可以通过android:priority
属性以及通过IntentFilter.setPriority
)方法编程方式将优先级分配给清单中的Intent过滤器。但是,请注意,具有相同优先级的接收器将以任意顺序运行。
如果您的应用不支持跨应用发送广播,请使用本地广播管理器 (LocalBroadcastManager
)。它们可用于确保仅从内部应用程序接收Intent,而来自任何其他应用程序的任何Intent都将被丢弃。这对于提高应用程序的安全性和效率非常有用,因为不涉及进程间通信。但是,请注意该类LocalBroadcastManager
类已被弃用,Google 建议使用替代品,例如: LiveData
.
有关广播接收器的更多安全注意事项,请参阅安全注意事项和最佳实践。
隐式Broadcast Receiver限制
根据Background Optimizations,针对 Android 7.0(API 级别 24)或更高版本的应用程序不再接收CONNECTIVITY_ACTION
广播,除非他们使用Context.registerReceiver()
注册. 系统也不发送ACTION_NEW_PICTURE
和ACTION_NEW_VIDEO
广播。
根据Background Execution Limits ,针对 Android 8.0(API 级别 26)或更高版本的应用程序无法再在其清单中为隐式广播注册广播接收器,但隐式广播异常中列出的除外。通过调用Context.registerReceiver
在运行时创建的广播接收器不受此限制的影响。
根据对系统广播的更改,从 Android 9(API 级别 28)开始,NETWORK_STATE_CHANGED_ACTION
广播不会接收有关用户位置或个人身份数据的信息。
Android 应用程序发布
成功开发应用程序后,下一步就是发布并与他人共享。但是,应用程序不能简单地添加到商店并共享,它们必须先签名。加密签名作为可验证的标识,它标识了应用程序的作者并确保应用程序自初始分发以来从未被修改。
签名过程
在开发过程中,应用程序使用自动生成的证书进行签名。此证书本质上是不安全的,仅用于调试。大多数商店不接受这种证书发布;因此,必须创建具有更安全功能的证书。当应用程序安装在 Android 设备上时,包管理器(Package Manager)会确保它已使用相应 APK 中包含的证书进行签名。如果证书的公钥与设备上其他 APK 的密钥匹配,则新 APK 可能会与预先存在的 APK 共享一个 UID。这促进了来自单个供应商的应用程序之间的交互。或者,可以为签名保护级别指定安全权限;这将限制对使用相同密钥签名的应用程序的访问。
APK 签名方案
Android 支持三种应用程序签名方案。从 Android 9(API 级别 28)开始,可以使用 APK 签名方案 v3(v3 方案)、APK 签名方案 v2(v2 方案)或 JAR 签名(v1 方案)来验证 APK。对于 Android 7.0(API 级别 24)及更高版本,可以使用 APK 签名方案 v2(v2 方案)或 JAR 签名(v1 方案)验证 APK。为了向后兼容,可以使用多个签名方案对 APK 进行签名,以使应用程序可以在较新和较旧的 SDK 版本上运行。旧平台忽略 v2 签名并仅验证 v1 签名。
JAR 签名(v1 方案)
应用签名的原始版本将签名的 APK 实现为标准签名的 JAR,其中必须包含META-INF/MANIFEST.MF
. 所有文件都必须使用通用证书进行签名,但是此方案不保护 APK 的某些部分,例如 ZIP 压缩文件中的文件。这种方案的缺点是APK验证者在应用签名之前需要处理不可信的数据结构,并且验证者丢弃数据结构没有覆盖的数据。此外,APK 验证程序必须解压缩所有压缩文件,这需要大量时间和内存。
APK 签名方案(v2 方案)
使用 APK 签名方案,对完整的 APK 进行哈希和签名,并创建一个 APK 签名块并将其插入 APK。在验证期间,v2 方案会检查整个 APK 文件的签名。这种形式的 APK 验证速度更快,并提供更全面的防修改保护。您可以在下面看到v2 Scheme 的 APK 签名验证过程。
APK 签名方案(v3 方案)
v3 APK 签名块格式与 v2 相同。V3 向 APK 签名块添加了有关支持的 SDK 版本和 proof-of-rotation(轮转验证,允许应用轮转其签名证书,就是当我们的签名证书快过期或者需要更换时,打包时提前将新证书打包信息打包进apk文件中,后续版本升级时即可使用新证书签名。)的信息。在 Android 9(API 级别 28)及更高版本中,可以根据 APK 签名方案 v3、v2 或 v1 方案验证 APK。旧平台忽略 v3 签名并尝试先验证 v2 然后验证 v1。
签名块的签名数据中的proof-of-rotation属性由一个单链表组成,每个节点都包含一个签名证书,用于对应用程序的先前版本进行签名。为了使向后兼容起作用,旧的签名证书对新的证书集进行签名,从而为每个新密钥提供根据,证明它应该与旧密钥一样受信任。不再独立签署 APK,因为proof-of-rotation结构必须让旧的签名证书签署新的证书集,而不是一个接一个地签署它们。您可以在下面看到APK 签名 v3 方案验证过程。
APK 签名方案(v4 方案)
APK 签名方案 v4 与 Android 11.0(API 级别 30)一起引入。这要求所有使用它启动的设备都默认启用fs-verity 。fs-verity 是一个 Linux 内核功能,由于其极其高效的文件哈希计算,主要用于文件身份验证(检测恶意修改)。仅当针对在引导期间加载到内核密钥环的可信数字证书进行验证过了之后,读取请求才会成功。
v4 签名需要补充的 v2 或 v3 签名,与以前的签名方案相比,v4 签名存储在单独的文件<apk name>.apk.idsig
中。在使用apksigner verify
验证 v4 签名的 APK 时,请记住使用--v4-signature-file
标志指定。
您可以在Android 开发人员文档中找到更多详细信息。
创建您的证书
Android 使用公共/私有证书来签名 Android 应用程序(.apk 文件)。证书是信息包;在安全性方面,密钥是这种信息中最重要的类型。公共证书包含用户的公钥,私有证书包含用户的私钥。公共证书和私有证书是相关的。证书是唯一的,不能重新生成。请注意,如果证书丢失,则无法恢复,因此无法更新使用该证书签名的任何应用程序。应用程序开发者可以重用可使用的 KeyStore(密钥库) 中的现有私钥/公钥对或生成新密钥对。在 Android SDK 中,keytool
命令会生成一个新的密钥对。以下命令创建一个密钥长度为 2048 位、有效期为 7300 天 = 20 年的 RSA 密钥对。生成的密钥对存储在文件“myKeyStore.jks”中,该文件位于当前目录中):
1 | $ keytool - genkey - alias myDomain - keyalg RSA - keysize 2048 - validity 7300 - keystore myKeyStore.jks - storepass myStrongPassword |
安全地存储您的密钥并确保它在其整个生命周期内保持私密至关重要。任何获得密钥访问权限的人都可以使用您无法控制的内容向您的应用程序发布更新(从而添加不安全的功能或使用基于签名的权限访问共享内容)。用户对应用程序及其开发人员的信任完全基于此类证书;因此,证书保护和安全管理对于声誉和客户保留至关重要,并且绝不能与其他人共享密钥。密钥存储在可以用密码保护的二进制文件中;此类文件称为KeyStores. 加密KeyStore的密码应该很强大,并且只有密钥创建者知道。出于这个原因,密钥通常存储在开发人员访问受限的专用构建机器上。Android 证书的有效期必须长于相关应用(包括应用的更新版本)的有效期。例如,Google Play 将要求证书至少在 2033 年 10 月 22 日之前保持有效。
签名应用程序
签名的目标是将应用程序文件 (.apk) 与开发人员的公钥相关联。为此,开发人员计算 APK 文件的哈希值并使用自己的私钥对其进行加密。然后,第三方可以通过使用作者的公钥解密加密的哈希并验证它与 APK 文件的实际哈希匹配来验证应用程序的真实性(例如,应用程序确实来自声称是自己是发起者的事实).
许多集成开发环境 (IDE) 集成了应用程序签名过程,以使用户使用更轻松。请注意,某些 IDE 在配置文件中以明文形式存储私钥;仔细检查这一点,以防其他人能够访问此类文件并在必要时删除信息。可以使用 Android SDK(API 级别 24 及更高级别)提供的“apksigner”工具从命令行对应用程序进行签名。它位于[SDK-Path]/build-tools/[version]
。对于 API 24.0.2 及更低版本,您可以使用“jarsigner”,它是 Java JDK 的一部分。整个过程的细节可以在Android官方文档中找到;但是,下面给出一个例子来说明这一点。
1 | $ apksigner sign - - out mySignedApp.apk - - ks myKeyStore.jks myUnsignedApp.apk |
在此示例中,将使用来自开发人员 KeyStore 'myKeyStore.jks'(位于当前目录中)的私钥对未签名的应用程序('myUnsignedApp.apk')进行签名。该应用程序将成为名为“mySignedApp.apk”的签名应用程序,并准备发布到商店。
Zipalign
该zipalign
工具始终用于在应用发布之前对齐 APK 文件。此工具对齐 APK 中的所有未压缩数据(例如图像、原始文件和 4 字节边界),有助于改善应用运行时的内存管理。
在使用 apksigner 对 APK 文件进行签名之前,必须使用 Zipalign。
发布过程
由于 Android 生态系统是开放的,因此可以从任何地方(您自己的网站、任何商店等)分发应用程序。但是,Google Play 是最知名、最受信任和最受欢迎的商店,而且 Google 自己也提供了它。Amazon Appstore 是 Kindle 设备值得信赖的默认商店。如果用户想要从不受信任的来源安装第三方应用程序,他们必须在其设备安全设置中明确允许这样做。
应用程序可以通过多种来源安装在 Android 设备上:通过 USB 本地、通过 Google 的官方应用商店(Google Play 商店)或从其他商店。
其他供应商可能会在实际发布应用程序之前对其进行审核和批准,而 Google 只会扫描已知的恶意软件签名;这最大限度地缩短了发布过程开始和公共应用程序可用性之间的时间。
发布应用程序非常简单;主要操作是使签名的 APK 文件可下载。在 Google Play 上,发布从创建帐户开始,然后通过专用界面交付应用程序。详细信息可在官方 Android 文档中找到。
Android 应用程序攻击面
Android 应用程序攻击面由应用程序的所有组件组成,包括发布应用程序和支持其运行所需的库。如果不执行以下操作,Android 应用程序可能容易受到攻击:
通过 IPC 通信或 URL 协议验证所有输入,另请参阅:
- 验证用户在输入框中的所有输入。
验证 WebView 中加载的内容,另请参见:
与后端服务器进行安全通信,或者容易受到服务器和移动应用程序之间的中间人攻击,另请参阅:
- 安全地存储所有本地数据,或从存储中加载不受信任的数据,另请参阅:
保护自己免受受损环境、重新打包或其他本地攻击,另请参阅:
Quote
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [翻译]Android 基础安全测试 23234
- [翻译][原创]Android平台概述 4638
- [原创]windows逆向自第二步自学PE 13370
- [原创]windows逆向的学习线路 11417
- [原创]学习从基础自学逆向 24243