跳过内容

Android平台概述

本章从架构的角度介绍了Android平台。讨论了以下五个关键领域

  1. Android架构
  2. Android安全:深度防御方法
  3. Android应用程序结构
  4. Android应用程序发布
  5. Android应用程序攻击面

访问官方Android开发者文档网站以获取有关Android平台的更多详细信息。

Android架构

Android是一个基于Linux的开源平台,由开放手机联盟(由Google领导的联盟)开发,用作移动操作系统(OS)。如今,该平台是各种现代技术的基础,例如手机、平板电脑、可穿戴技术、电视和其他智能设备。典型的Android构建版本附带一系列预安装的(“库存”)应用程序,并支持通过Google Play商店和其他市场安装第三方应用程序。

Android的软件堆栈由几个不同的层组成。每一层都定义了接口并提供特定的服务。

内核: 在最低级别,Android基于Linux内核的变体,其中包含一些重要的添加,包括低内存杀手、唤醒锁、Binder IPC驱动程序等。就MASTG而言,我们将重点关注操作系统的用户模式部分,其中Android与典型的Linux发行版有很大不同。对我们来说最重要的两个组件是应用程序使用的托管运行时(ART/Dalvik)和Bionic,Android的glibc版本,GNU C库。

HAL: 在内核之上,硬件抽象层(HAL)定义了一个标准接口,用于与内置硬件组件进行交互。几个HAL实现被打包到共享库模块中,Android系统在需要时会调用这些模块。这是允许应用程序与设备硬件交互的基础。例如,它允许库存电话应用程序使用设备的麦克风和扬声器。

运行时环境: Android应用程序用Java和Kotlin编写,然后编译为Dalvik字节码,然后可以使用运行时来执行,运行时解释字节码指令并在目标设备上执行它们。对于Android,这是Android Runtime(ART)。这类似于Java应用程序的JVM(Java虚拟机),或者.NET应用程序的Mono Runtime。

Dalvik字节码是Java字节码的优化版本。它是通过首先使用javac和kotlinc编译器将Java或Kotlin代码编译为Java字节码来创建的,生成.class文件。最后,使用d8工具将Java字节码转换为Dalvik字节码。Dalvik字节码以.dex文件的形式打包在APK和AAB文件中,并由Android上的托管运行时使用以在设备上执行它。

在Android 5.0(API级别21)之前,Android在Dalvik虚拟机(DVM)上执行字节码,在执行时,字节码被转换为机器代码,这个过程称为即时(JIT)编译。这使得运行时可以受益于编译代码的速度,同时保持代码解释的灵活性。

自从Android 5.0(API级别21)以来,Android在Android Runtime(ART)上执行字节码,它是DVM的后继者。ART通过包括Java和本机堆栈信息,提高了性能以及应用程序本机崩溃报告中的上下文信息。它使用相同的Dalvik字节码输入来保持向后兼容性。但是,ART以不同的方式执行Dalvik字节码,使用提前(AOT)、即时(JIT)和配置文件引导编译的混合组合。

  • AOT将Dalvik字节码预编译为本机代码,生成的代码将以.oat扩展名(ELF二进制文件)保存在磁盘上。dex2oat工具可用于执行编译,可以在Android设备上的/system/bin/dex2oat中找到。AOT编译在应用程序安装期间执行。这使得应用程序启动更快,因为不再需要编译。但是,这也意味着与JIT编译相比,安装时间增加了。此外,由于应用程序始终针对当前版本的操作系统进行优化,这意味着软件更新将重新编译所有先前编译的应用程序,从而导致系统更新时间显着增加。最后,AOT编译将编译整个应用程序,即使某些部分永远不会被用户使用。
  • JIT发生在运行时。
  • 配置文件引导编译是一种混合方法,在Android 7(API级别24)中引入,以对抗AOT的缺点。首先,应用程序将使用JIT编译,并且Android会跟踪应用程序中经常使用的所有部分。此信息存储在应用程序配置文件中,当设备空闲时,会运行编译(dex2oat)守护程序,该守护程序AOT编译配置文件中标识的常用代码路径。

来源:https://lief-project.github.io/doc/latest/tutorials/10_android_formats.html

沙箱: Android应用程序无法直接访问硬件资源,每个应用程序都在其自己的虚拟机或沙箱中运行。这使得操作系统可以精确控制设备上的资源和内存访问。例如,崩溃的应用程序不会影响在同一设备上运行的其他应用程序。Android控制分配给应用程序的系统资源的最大数量,防止任何一个应用程序占用过多的资源。同时,这种沙箱设计可以被认为是Android全局深度防御策略中的众多原则之一。恶意第三方应用程序,权限较低,不应能够逃脱其自身的运行时并读取同一设备上受害者应用程序的内存。在以下部分中,我们将仔细研究Android操作系统中的不同防御层。在“软件隔离”部分中了解更多信息。

您可以在Google Source文章“Android Runtime(ART)”Jonathan Levin的书“Android Internals”@_qaz_qaz的博客文章“Android 101”中找到更多详细信息。

Android安全:深度防御方法

Android架构实现了不同的安全层,这些安全层共同实现了深度防御方法。这意味着敏感用户数据或应用程序的机密性、完整性或可用性不取决于单一的安全措施。本节概述了Android系统提供的不同防御层。安全策略大致可以分为四个不同的领域,每个领域都侧重于防御特定的攻击模型。

  • 系统级安全
  • 软件隔离
  • 网络安全
  • 反漏洞利用

系统级安全

设备加密

Android从Android 2.3.4(API级别10)开始支持设备加密,此后发生了一些重大变化。Google强制所有运行Android 6.0(API级别23)或更高版本的设备都必须支持存储加密,尽管一些低端设备被豁免,因为这会显着影响它们的性能。

  • 全盘加密(FDE):Android 5.0(API级别21)及更高版本支持全盘加密。此加密使用受用户设备密码保护的单个密钥来加密和解密用户数据分区。这种加密现在被认为是已弃用的,应尽可能使用基于文件的加密。全盘加密具有一些缺点,例如如果用户不输入密码来解锁,则无法在重启后接听电话或没有操作警报。

  • 基于文件的加密(FBE):Android 7.0(API级别24)支持基于文件的加密。基于文件的加密允许使用不同的密钥加密不同的文件,因此可以独立解密它们。支持此类型加密的设备也支持直接启动。直接启动使设备即使在用户未解锁设备的情况下也能访问警报或辅助功能服务等功能。

注意:您可能听说过Adiantum,这是一种加密方法,专为运行Android 9(API级别28)及更高版本且CPU缺少AES指令的设备设计。Adiantum仅与ROM开发人员或设备供应商相关,Android不提供API供开发人员从应用程序中使用Adiantum。根据Google的建议,在运送带有ARMv8加密扩展的基于ARM的设备或带有AES-NI的基于x86的设备时不应使用Adiantum。AES在这些平台上更快。

更多信息可在Android文档中找到。

可信执行环境(TEE)

为了使Android系统执行加密,它需要一种安全地生成、导入和存储加密密钥的方法。我们本质上是将保持敏感数据安全的问题转移到保持加密密钥安全的问题上。如果攻击者可以转储或猜测加密密钥,则可以检索敏感的加密数据。

Android在专用硬件中提供可信执行环境,以解决安全生成和保护加密密钥的问题。这意味着Android系统中的专用硬件组件负责处理加密密钥材料。三个主要模块负责此

  • 硬件支持的KeyStore:此模块向Android操作系统和第三方应用程序提供加密服务。它使应用程序能够在TEE中执行加密敏感操作,而无需公开加密密钥材料。

  • StrongBox:在Android 9(Pie)中,引入了StrongBox,这是实现硬件支持的KeyStore的另一种方法。虽然在Android 9 Pie之前,硬件支持的KeyStore将是位于Android操作系统内核之外的任何TEE实现。StrongBox是一个实际的完整独立硬件芯片,已添加到设备上,KeyStore在该设备上实现,并且在Android文档中有明确的定义。您可以以编程方式检查密钥是否驻留在StrongBox中,如果驻留,您可以确定它受到硬件安全模块的保护,该模块具有自己的CPU、安全存储和真随机数生成器(TRNG)。所有敏感的加密操作都发生在这个芯片上,在StrongBox的安全边界内。

  • GateKeeper:GateKeeper模块支持设备模式和密码身份验证。身份验证过程中的安全敏感操作发生在设备上可用的TEE内部。GateKeeper由三个主要组件组成,(1)gatekeeperd,它是公开GateKeeper的服务,(2)GateKeeper HAL,它是硬件接口,以及(3)TEE实现,它是实际在TEE中实现GateKeeper功能的软件。

验证启动

我们需要一种方法来确保在Android设备上执行的代码来自可信来源,并且其完整性不受损害。为了实现这一点,Android引入了验证启动的概念。验证启动的目标是在硬件和在此硬件上执行的实际代码之间建立信任关系。在验证启动序列期间,建立一个完整的信任链,从硬件保护的信任根(RoT)开始,一直到正在运行的最终系统,通过并验证所有必需的启动阶段。当Android系统最终启动时,您可以放心系统没有被篡改。您有加密证明,证明运行的代码是OEM打算使用的代码,而不是已被恶意或意外更改的代码。

更多信息可在Android文档中找到。

软件隔离

Android用户和组

即使Android操作系统基于Linux,它也没有以其他类Unix系统相同的方式实现用户帐户。在Android中,Linux内核的多用户支持用于沙箱应用程序:除少数例外,每个应用程序都像在单独的Linux用户下运行一样运行,有效地与其他应用程序和操作系统的其余部分隔离。

文件android_filesystem_config.h包含分配给系统进程的预定义用户和组的列表。随着后者的安装,会添加其他应用程序的UID(用户ID)。

例如,Android 9.0(API级别28)定义了以下系统用户

    #define AID_ROOT             0  /* traditional unix root user */
    #...
    #define AID_SYSTEM        1000  /* system server */
    #...
    #define AID_SHELL         2000  /* adb and debug shell user */
    #...
    #define AID_APP_START          10000  /* first app user */
    ...

SELinux

安全增强型Linux(SELinux)使用强制访问控制(MAC)系统来进一步锁定哪些进程应该可以访问哪些资源。每个资源都以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)以来,已经引入了系统范围的基于TLS的DNS支持。它允许您使用TLS协议对DNS服务器执行查询。与DNS服务器建立安全通道,通过该通道发送DNS查询。这确保在DNS查找期间不会暴露敏感数据。

更多信息可在Android开发者博客上找到。

反漏洞利用

ASLR、KASLR、PIE和DEP

地址空间布局随机化(ASLR)自Android 4.1(API级别15)以来一直是Android的一部分,是一种防止缓冲区溢出攻击的标准保护,它确保应用程序和操作系统都被加载到随机内存地址,从而难以获取特定内存区域或库的正确地址。在Android 8.0(API级别26)中,此保护也为内核(KASLR)实现。只有当应用程序可以加载到内存中的随机位置时,ASLR保护才有可能,这由应用程序的位置无关可执行文件(PIE)标志指示。自从Android 5.0(API级别21)以来,已经放弃了对非PIE启用本机库的支持。最后,数据执行保护(DEP)阻止堆栈和堆上的代码执行,这也被用于对抗缓冲区溢出漏洞。

更多信息可在Android开发者博客上找到。

SECCOMP过滤器

Android应用程序可以包含用C或C++编写的本机代码。这些编译的二进制文件既可以通过Java本机接口(JNI)绑定与Android Runtime通信,也可以通过系统调用与操作系统通信。一些系统调用要么未实现,要么不应该被正常应用程序调用。由于这些系统调用直接与内核通信,因此它们是漏洞利用开发人员的主要目标。使用Android 8(API级别26),Android已经为所有基于Zygote的进程(即用户应用程序)引入了对安全计算(SECCOMP)过滤器的支持。这些过滤器将可用的syscall限制为通过bionic公开的那些。

更多信息可在Android开发者博客上找到。

Android应用程序结构

与操作系统的通信

Android应用程序通过Android Framework与系统服务进行交互,Android Framework是一个抽象层,提供高级Java API。这些服务中的大多数都是通过正常的Java方法调用调用的,并且被转换为对在后台运行的系统服务的IPC调用。系统服务的示例包括

  • 连接(Wi-Fi、蓝牙、NFC等)
  • 文件
  • 相机
  • 地理定位(GPS)
  • 麦克风

该框架还提供常见的安全功能,例如加密。

API规范随每个新的Android版本而变化。关键错误修复和安全补丁通常也应用于早期版本。

值得注意的API版本。有关在不同Android版本中引入的安全和隐私功能的更多信息,请参阅 使用最新的minSdkVersion

Android开发版本遵循独特的结构。它们被组织成系列,并被赋予受美味佳肴启发的字母代码名称。你可以在这里找到它们。

应用沙箱

应用在Android应用沙箱中执行,该沙箱将应用数据和代码执行与其他设备上的应用分开。如前所述,这种分离增加了第一层防御。

安装新应用会创建一个以应用包命名的目录,从而导致以下路径:/data/data/[package-name]。此目录保存应用程序的数据。设置Linux目录权限,以便只能使用应用程序的唯一UID读取和写入该目录。

我们可以通过查看/data/data文件夹中的文件系统权限来确认这一点。例如,我们可以看到Google Chrome和Calendar各分配了一个目录,并在不同的用户帐户下运行

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应用程序中实现此目的

<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内核强制执行。

通常,应用程序的UID分配范围为10000到99999。Android应用程序根据其UID接收用户名。例如,UID为10188的应用程序接收用户名u0_a188。如果授予了应用程序请求的权限,则相应的组ID将被添加到应用程序的进程中。例如,以下应用程序的用户ID为10188。它属于组ID 3003(inet)。该组与android.permission.INTERNET权限相关。下面显示了id命令的输出。

$ 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和权限之间的关系在以下文件中定义

platform.xml

<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

进程 ZygoteAndroid 初始化期间启动。Zygote 是一个用于启动应用程序的系统服务。Zygote 进程是一个“基础”进程,其中包含应用程序所需的所有核心库。启动时,Zygote 会打开套接字 /dev/socket/zygote 并侦听来自本地客户端的连接。当它收到连接时,它会派生一个新的进程,然后加载并执行特定于应用程序的代码。

应用生命周期

在 Android 中,应用程序进程的生命周期由操作系统控制。当启动应用程序组件且同一应用程序尚未有任何其他组件运行时,将创建一个新的 Linux 进程。当后者不再必要或需要回收内存以运行更重要的应用程序时,Android 可能会终止此进程。终止进程的决定主要与用户与进程的交互状态相关。一般来说,进程可以处于四种状态之一。

  • 前台进程(例如,在屏幕顶部运行的活动或正在运行的 BroadcastReceiver)
  • 可见进程是用户意识到的进程,因此终止它会对用户体验产生明显的负面影响。一个例子是运行一个对用户在屏幕上可见但不在前台的活动。

  • 服务进程是托管已使用 startService 方法启动的服务的进程。虽然这些进程对用户来说不是直接可见的,但它们通常是用户关心的内容(例如后台网络数据上传或下载),因此系统将始终保持此类进程运行,除非没有足够的内存来保留所有前台和可见进程。

  • 缓存进程是当前不需要的进程,因此系统可以在需要内存时自由终止它。应用程序必须实现回调方法来响应许多事件;例如,首次创建应用程序进程时会调用 onCreate 处理程序。其他回调方法包括 onLowMemoryonTrimMemoryonConfigurationChanged

应用包

Android 应用程序可以以两种形式发布:Android 包套件 (APK) 文件或 Android 应用包 (.aab)。Android 应用包提供应用程序所需的所有资源,但会将 APK 的生成及其签名推迟到 Google Play。应用包是已签名的二进制文件,其中包含应用程序的代码在几个模块中。基本模块包含应用程序的核心。基本模块可以通过包含应用程序的新的丰富功能/功能的各种模块进行扩展,如 应用包的开发者文档 中进一步解释的那样。如果您有一个 Android 应用包,您可以最好地使用 Google 的 bundletool 命令行工具来构建未签名的 APK,以便在 APK 上使用现有工具。您可以通过运行以下命令从 AAB 文件创建一个 APK

bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks

如果您想创建已签名的 APK 以便部署到测试设备,请使用

$ 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 清单

每个 Android 应用程序在 APK 的根目录中都包含一个 AndroidManifest.xml 文件,以二进制 XML 格式存储。此文件定义了应用程序的结构和 Android 操作系统在安装和运行时使用的关键属性。

与安全相关的元素包括

  • 权限: 使用 <uses-permission> 声明所需的权限,例如访问互联网、相机、存储、位置或联系人。这些定义了应用程序的访问边界,应遵循最小权限原则。可以使用 <permission> 定义自定义权限,并且应包含适当的 protectionLevel,例如 signaturedangerous,以避免被其他应用程序滥用。
  • 组件: 清单列出了应用程序中声明的所有 应用程序组件,作为入口点。它们可以暴露给其他应用程序(通过 intent 过滤器或 exported 属性),因此它们对于确定攻击者可能如何与应用程序交互至关重要。主要组件类型包括
    • 活动: 定义用户界面屏幕。
    • 服务: 运行后台任务。
    • 广播接收器: 处理外部消息。
    • 内容提供程序: 公开结构化数据。
  • 深层链接: 深层链接通过带有 VIEW 操作、BROWSABLE 类别和指定 URI 模式的 data 元素的 intent 过滤器进行配置。这些可以将活动暴露给 Web 或应用程序链接,并且必须仔细验证以避免注入或欺骗风险。添加 android:autoVerify="true" 启用应用程序链接,这会将经过验证的链接的处理限制为声明的应用程序,从而降低链接劫持的风险。
  • 使用明文流量: android:usesCleartextTraffic 属性控制应用程序是否允许非加密的 HTTP 流量。从 Android 9 (API 28) 开始,默认情况下禁用明文流量,除非明确允许。此属性也可以被 networkSecurityConfig 覆盖。
  • 网络安全配置: 一个可选的 XML 文件,通过 android:networkSecurityConfig 定义,自 Android 7.0 (API level 24) 起可用,它提供了对 网络安全行为 的细粒度控制。它允许指定受信任的证书颁发机构、每个域的 TLS 要求和明文流量异常,从而覆盖 android:usesCleartextTraffic 中定义的全局设置。
  • 备份行为: android:allowBackup 属性允许或阻止 备份 应用程序数据。
  • 任务关联性和启动模式: 这些设置影响活动的组合和启动方式。如果攻击者的应用程序模仿合法的组件,错误配置可能会导致任务劫持或网络钓鱼式攻击。

可以在官方 Android 清单文件文档中找到可用清单选项的完整列表。

在构建时,清单与所有包含的库和依赖项中的清单合并。最终合并的清单可能包含开发人员未明确声明的其他权限、组件或设置。安全审查必须分析合并后的输出,以了解应用程序的真实暴露情况。

这是一个由开发人员定义的清单文件的示例。它声明了几个权限,允许备份,并定义了应用程序的主活动

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MASTestApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

如果您要从 APK 获取 AndroidManifest.xml 文件( 从 AndroidManifest 获取信息),您会看到它包含其他元素,例如 package 属性,它定义了应用程序的唯一标识符,<uses-sdk> 元素指定了 android:minSdkVersionandroid:targetSdkVersion,新的活动、提供程序和接收器,以及其他属性,例如 android:debuggable="true",它指示应用程序处于调试模式。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0"
    android:compileSdkVersion="35"
    android:compileSdkVersionCodename="15"
    package="org.owasp.mastestapp"
    platformBuildVersionCode="35"
    platformBuildVersionName="15">
    <uses-sdk
        android:minSdkVersion="29"
        android:targetSdkVersion="35"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <permission
        android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
        android:protectionLevel="signature"/>
    <uses-permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
    <application
        android:theme="@style/Theme.MASTestApp"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:debuggable="true"
        android:testOnly="true"
        android:allowBackup="true"
        android:supportsRtl="true"
        android:extractNativeLibs="false"
        android:fullBackupContent="@xml/backup_rules"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:appComponentFactory="androidx.core.app.CoreComponentFactory"
        android:dataExtractionRules="@xml/data_extraction_rules">
        <activity
            android:theme="@style/Theme.MASTestApp"
            android:name="org.owasp.mastestapp.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity
            android:name="androidx.compose.ui.tooling.PreviewActivity"
            android:exported="true"/>
        <activity
            android:name="androidx.activity.ComponentActivity"
            android:exported="true"/>
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:exported="false"
            android:authorities="org.owasp.mastestapp.androidx-startup">
            <meta-data
                android:name="androidx.emoji2.text.EmojiCompatInitializer"
                android:value="androidx.startup"/>
            ...
        </provider>
        <receiver
            android:name="androidx.profileinstaller.ProfileInstallReceiver"
            android:permission="android.permission.DUMP"
            android:enabled="true"
            android:exported="true"
            android:directBootAware="false">
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
            </intent-filter>
            ...
        </receiver>
    </application>
</manifest>

应用组件

Android 应用程序由几个高级组件组成。主要组件是

  • Activity
  • Fragment
  • Intent
  • 广播接收器
  • 内容提供程序和服务

所有这些元素都由 Android 操作系统提供,以通过 API 提供的预定义类的形式。

活动

活动构成了任何应用程序的可见部分。每个屏幕有一个活动,因此具有三个不同屏幕的应用程序实现三个不同的活动。活动通过扩展 Activity 类来声明。它们包含所有用户界面元素:片段、视图和布局。

每个活动都需要在 Android 清单中声明,语法如下

<activity android:name="ActivityName">
</activity>

未在清单中声明的活动无法显示,尝试启动它们将引发异常。

与应用程序一样,活动有自己的生命周期,需要监控系统更改以处理它们。活动可以处于以下状态:活动、暂停、停止和非活动。这些状态由 Android 操作系统管理。因此,活动可以实现以下事件管理器

  • onCreate
  • onSaveInstanceState
  • onStart
  • onResume
  • onRestoreInstanceState
  • onPause
  • onStop
  • onRestart
  • onDestroy

应用程序可能不会显式地实现所有事件管理器,在这种情况下,将采取默认操作。通常,至少 onCreate 管理器会被应用程序开发人员覆盖。这是声明和初始化大多数用户界面组件的方式。当资源(如网络连接或数据库连接)必须显式释放或应用程序关闭时必须发生特定操作时,可能会覆盖 onDestroy

片段

片段表示活动中的行为或用户界面的一部分。片段是在 Android Honeycomb 3.0(API 级别 11)版本中引入的。

片段旨在封装界面的一部分,以方便重用和适应不同的屏幕尺寸。片段是自主实体,因为它们包含所有必需的组件(它们有自己的布局、按钮等)。但是,它们必须与活动集成才能有用:片段不能单独存在。它们有自己的生命周期,这与实现它们的活动的生命周期相关联。

因为片段有自己的生命周期,所以 Fragment 类包含可以重新定义和扩展的事件管理器。这些事件管理器包括 onAttach、onCreate、onStart、onDestroy 和 onDetach。存在其他几个;读者应参考 Android 片段规范 获取更多详细信息。

可以通过扩展 Android 提供的 Fragment 类轻松实现片段

Java 示例

public class MyFragment extends Fragment {
    ...
}

Kotlin 示例

class MyFragment : Fragment() {
    ...
}

片段不需要在清单文件中声明,因为它们依赖于活动。

为了管理其片段,活动可以使用片段管理器(FragmentManager 类)。此类可以轻松查找、添加、删除和替换关联的片段。

可以使用以下方法创建片段管理器

Java 示例

FragmentManager fm = getFragmentManager();

Kotlin 示例

var fm = fragmentManager

片段不一定具有用户界面;它们可以是管理与应用程序用户界面相关的后台操作的便捷而有效的方式。可以声明片段为持久性,以便即使其 Activity 被销毁,系统也会保留其状态。

内容提供程序

Android 使用 SQLite 永久存储数据:与 Linux 一样,数据存储在文件中。SQLite 是一种轻量级、高效的开源关系数据存储技术,不需要太多的处理能力,这使其非常适合移动使用。提供了一个包含特定类(Cursor、ContentValues、SQLiteOpenHelper、ContentProvider、ContentResolver 等)的完整 API。SQLite 不作为单独的进程运行;它是应用程序的一部分。默认情况下,属于给定应用程序的数据库只能由该应用程序访问。但是,内容提供程序提供了一种用于抽象数据源(包括数据库和平面文件)的绝佳机制;它们还提供了一种标准的有效机制来共享应用程序之间的数据,包括本机应用程序。要使其他应用程序可以访问内容提供程序,需要在将共享它的应用程序的清单文件中显式声明。只要未声明内容提供程序,它们就不会被导出,并且只能由创建它们的应用程序调用。

内容提供程序通过 URI 寻址方案实现:它们都使用 content:// 模型。无论源的类型(SQLite 数据库、平面文件等)如何,寻址方案始终相同,从而抽象了源并为开发人员提供了唯一的方案。内容提供程序提供所有常规数据库操作:创建、读取、更新、删除。这意味着任何在其清单文件中具有适当权限的应用程序都可以操作来自其他应用程序的数据。

服务

服务是 Android 操作系统组件(基于 Service 类),可以在后台执行任务(数据处理、启动 intent 和通知等),而无需显示用户界面。服务旨在长期运行进程。它们的系统优先级低于活动应用程序的优先级,但高于非活动应用程序的优先级。因此,当系统需要资源时,它们不太可能被终止,并且可以配置为在有足够的可用资源时自动重新启动。这使得服务成为运行后台任务的绝佳选择。请注意,服务与活动一样,在主应用程序线程中执行。服务不会创建自己的线程,也不会在单独的进程中运行,除非您另行指定。

进程间通信

正如我们已经了解到的,每个 Android 进程都有自己的沙盒地址空间。进程间通信设施允许应用程序安全地交换信号和数据。Android 的 IPC 不是依赖于默认的 Linux IPC 设施,而是基于 Binder,这是 OpenBinder 的自定义实现。大多数 Android 系统服务和所有高级 IPC 服务都依赖于 Binder。

术语 Binder 代表许多不同的事物,包括

  • Binder 驱动程序:内核级驱动程序
  • Binder 协议:用于与 binder 驱动程序通信的底层基于 ioctl 的协议
  • IBinder 接口:Binder 对象实现的明确定义的行为
  • Binder 对象:IBinder 接口的通用实现
  • Binder 服务:Binder 对象的实现;例如,位置服务和传感器服务
  • Binder 客户端:使用 Binder 服务的对象

Binder 框架包括客户端-服务器通信模型。要使用 IPC,应用程序在代理对象中调用 IPC 方法。代理对象透明地将调用参数 marshallsparcel 中,并将事务发送到 Binder 服务器,该服务器实现为字符驱动程序 (/dev/binder)。服务器拥有一个线程池来处理传入的请求,并将消息传递到目标对象。从客户端应用程序的角度来看,所有这些都像一个常规方法调用,所有繁重的工作都由 Binder 框架完成。

允许其他应用程序绑定到它们的服务称为 绑定服务。这些服务必须向客户端提供 IBinder 接口。开发人员使用 Android 接口描述语言 (AIDL) 为远程服务编写接口。

ServiceManager 是一个系统守护程序,用于管理系统服务的注册和查找。它维护所有注册服务的名称/Binder 对列表。服务使用 addService 添加,并使用 android.os.ServiceManager 中的静态 getService 方法按名称检索。

Java 示例

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 示例

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 命令查询系统服务列表。

$ 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 以多种方式促进组件间通信,但有三种基本用例

  • 启动活动
    • 活动表示应用程序中的单个屏幕。您可以通过将 intent 传递给 startActivity 来启动活动的新实例。Intent 描述了活动并携带必要的数据。
  • 启动服务
    • 服务是在后台执行操作而无需用户界面的组件。使用 Android 5.0(API 级别 21)及更高版本,您可以使用 JobScheduler 启动服务。
  • 传递广播
    • 广播是任何应用程序都可以接收的消息。系统会传递系统事件的广播,包括系统启动和充电初始化。您可以通过将 intent 传递给 sendBroadcastsendOrderedBroadcast 向其他应用程序传递广播。

Intent 有两种类型。显式 intent 命名将要启动的组件(完全限定的类名)。例如

Java 示例

Intent intent = new Intent(this, myActivity.myClass);

Kotlin 示例

var intent = Intent(this, myActivity.myClass)

隐式 intent 被发送到操作系统以对给定的数据集执行给定的操作(在我们的示例中是 OWASP 网站的 URL)。系统可以决定哪个应用程序或类将执行相应的服务。例如

Java 示例

Intent intent = new Intent(Intent.MY_ACTION, Uri.parse("https://www.owasp.org"));

Kotlin 示例

var intent = Intent(Intent.MY_ACTION, Uri.parse("https://www.owasp.org"))

intent 过滤器是 Android 清单文件中的一个表达式,用于指定组件希望接收的 intent 类型。例如,通过为活动声明 intent 过滤器,您可以使其他应用程序可以使用某种 intent 直接启动您的活动。同样,如果您没有为其声明任何 intent 过滤器,则只能通过显式 intent 启动您的活动。

Android 使用 intent 将消息广播到应用程序(例如来电或短信)、重要的电源信息(例如电池电量低)和网络更改(例如连接丢失)。可以将额外数据添加到 intent(通过 putExtra/getExtras)。

这是操作系统发送的 intent 的简短列表。所有常量都在 Intent 类中定义,完整的列表在官方 Android 文档中

  • ACTION_CAMERA_BUTTON
  • ACTION_MEDIA_EJECT
  • ACTION_NEW_OUTGOING_CALL
  • ACTION_TIMEZONE_CHANGED

为了提高安全性和隐私性,本地广播管理器用于在应用程序内发送和接收 intent,而无需将其发送到操作系统的其余部分。这对于确保敏感和私人数据不会离开应用程序范围(例如地理位置数据)非常有用。

广播接收器

广播接收器是允许应用程序接收来自其他应用程序和系统本身的通知的组件。使用它们,应用程序可以响应事件(内部、由其他应用程序启动或由操作系统启动)。它们通常用于更新用户界面、启动服务、更新内容和创建用户通知。

有两种方法使广播接收器为系统所知。一种方法是在 Android 清单文件中声明它。清单应指定广播接收器和 intent 过滤器之间的关联,以指示接收器要侦听的操作。

在清单中使用 intent 过滤器的广播接收器声明示例

<receiver android:name=".MyReceiver" >
    <intent-filter>
        <action android:name="com.owasp.myapplication.MY_ACTION" />
    </intent-filter>
</receiver>

请注意,在此示例中,广播接收器不包含 android:exported 属性。由于至少定义了一个过滤器,因此默认值将设置为“true”。在没有任何过滤器的情况下,它将设置为“false”。

另一种方法是在代码中动态创建接收器。然后,接收器可以使用方法 Context.registerReceiver 进行注册。

动态注册广播接收器的示例

Java 示例

// 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 示例

// 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 时,系统会自动启动带有已注册接收器的应用程序。

根据 广播概述,如果广播未专门针对应用程序,则将其视为“隐式”。收到隐式广播后,Android 将列出所有在其过滤器中注册了给定操作的应用程序。如果多个应用程序已注册同一操作,则 Android 将提示用户从可用应用程序列表中进行选择。

广播接收器的一个有趣的功能是它们可以被赋予优先级;这样,intent 将根据其优先级传递给所有授权的接收器。可以通过清单中的 android:priority 属性以及通过 IntentFilter.setPriority 方法以编程方式将优先级分配给 intent 过滤器。但是,请注意,具有相同优先级的接收器将 以任意顺序运行

如果您的应用程序不应跨应用程序发送广播,请使用本地广播管理器(LocalBroadcastManager)。它们可用于确保仅从内部应用程序接收 intent,并且将丢弃来自任何其他应用程序的任何 intent。这对于提高安全性和应用程序的效率非常有用,因为不涉及进程间通信。但是,请注意,LocalBroadcastManager 类已 弃用,Google 建议使用 LiveData 等替代方案。

有关广播接收器的更多安全注意事项,请参阅 安全注意事项和最佳实践

隐式广播接收器限制

根据 后台优化,除非应用程序使用 Context.registerReceiver() 注册其广播接收器,否则以 Android 7.0(API 级别 24)或更高版本为目标的应用程序不再接收 CONNECTIVITY_ACTION 广播。系统也不会发送 ACTION_NEW_PICTUREACTION_NEW_VIDEO 广播。

根据 后台执行限制,以 Android 8.0(API 级别 26)或更高版本为目标的应用程序不再可以在其清单中注册隐式广播的广播接收器,但 隐式广播例外 中列出的除外。通过调用 Context.registerReceiver 在运行时创建的广播接收器不受此限制的影响。

根据 系统广播的更改,从 Android 9(API 级别 28)开始,NETWORK_STATE_CHANGED_ACTION 广播不会接收有关用户位置或个人身份数据的信息。

Android 应用程序发布

成功开发应用程序后,下一步是发布并与他人共享。但是,应用程序不能简单地添加到商店并共享,它们必须首先签名。加密签名用作应用程序开发人员放置的可验证标记。它标识应用程序的作者并确保自初始分发以来应用程序未被修改。

签名过程

在开发期间,应用程序使用自动生成的证书进行签名。此证书本质上是不安全的,仅用于调试。大多数商店不接受此类证书进行发布;因此,必须创建具有更安全功能的证书。当应用程序安装在 Android 设备上时,软件包管理器确保它已使用包含在相应 APK 中的证书进行签名。如果证书的公钥与用于对设备上的任何其他 APK 进行签名的密钥匹配,则新的 APK 可以与预先存在的 APK 共享 UID。这有助于单个供应商的应用程序之间的交互。或者,可以指定 Signature 保护级别的安全权限;这将限制对已使用同一密钥签名的应用程序的访问。

APK 签名方案

Android 支持多种应用程序签名方案

  • 低于 Android 7.0(API 级别 24):应用程序只能使用 JAR 签名 (v1) 方案,该方案不会保护 APK 的所有部分。此方案被认为是不安全的。
  • Android 7.0(API 级别 24)及更高版本:应用程序可以使用 v2 签名方案,该方案对整个 APK 进行签名,与较旧的 v1 (JAR) 签名方法相比,提供更强的保护。
  • Android 9(API 级别 28)及更高版本:建议同时使用 v2 和 v3 签名方案。v3 方案支持 密钥轮换,使开发人员能够在出现泄露的情况下替换密钥,而不会使旧签名无效。
  • Android 11(API 级别 30)及更高版本:应用程序可以选择包括 v4 签名方案 以实现更快的增量更新。

为了向后兼容,可以使用多个签名方案对 APK 进行签名,以便应用程序可以在较新和较旧的 SDK 版本上运行。例如,较旧的平台忽略 v2 签名,仅验证 v1 签名

JAR 签名(v1 方案)

应用程序签名的原始版本将签名 APK 实现为标准签名 JAR,该 JAR 必须包含 META-INF/MANIFEST.MF 中的所有条目。所有文件必须使用通用证书进行签名。此方案不会保护 APK 的某些部分,例如 ZIP 元数据。此方案的缺点是 APK 验证器需要在应用签名之前处理不受信任的数据结构,并且验证器会丢弃数据结构未涵盖的数据。此外,APK 验证器必须解压缩所有压缩文件,这需要大量的时间和内存。

此签名方案被认为是不安全的,例如,它受到 Janus 漏洞 (CVE-2017-13156) 的影响,该漏洞可能允许恶意行为者修改 APK 文件,而不会使 v1 签名失效。因此,对于运行 Android 7.0 及更高版本的设备,绝不应依赖 v1

APK 签名方案(v2 方案)

使用 APK 签名方案,将完整 APK 进行哈希和签名,并创建一个 APK 签名块并将其插入到 APK 中。在验证期间,v2 方案会检查整个 APK 文件的签名。这种形式的 APK 验证速度更快,并且可以更全面地防止修改。您可以在下面看到 v2 方案的 APK 签名验证过程

APK 签名方案(v3 方案)

v3 APK 签名块格式与 v2 相同。V3 将有关受支持的 SDK 版本的信息和轮换证明结构添加到 APK 签名块。在 Android 9(API 级别 28)及更高版本中,可以根据 APK 签名方案 v3、v2 或 v1 方案验证 APK。较旧的平台忽略 v3 签名,并尝试验证 v2,然后是 v1 签名。

签名块的签名数据中的轮换证明属性由单链列表组成,每个节点都包含用于对应用程序的先前版本进行签名的签名证书。为了使向后兼容性起作用,旧的签名证书对新的证书集进行签名,从而为每个新密钥提供证据,证明它应该与旧密钥一样受信任。不再可能独立签署 APK,因为轮换证明结构必须具有对新证书集进行签名的旧签名证书,而不是逐个签名。您可以在下面看到 APK 签名 v3 方案验证过程

APK 签名方案(v4 方案)

APK 签名方案 v4 与 Android 11(API 级别 30)一起引入,并且要求所有使用 Android 11 启动的设备默认启用 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 位的 RSA 密钥对,有效期为 7300 天 = 20 年。生成的密钥对存储在当前目录中的文件“myKeyStore.jks”中

keytool -genkey -alias myDomain -keyalg RSA -keysize 2048 -validity 7300 -keystore myKeyStore.jks -storepass myStrongPassword

安全地存储您的密钥并在其整个生命周期中确保其保持机密至关重要。任何获得密钥访问权限的人都将能够发布您不控制的内容的应用程序更新(从而添加不安全的功能或访问具有基于签名权限的共享内容)。用户对应用程序及其开发人员的信任完全基于此类证书;因此,证书保护和安全管理对于声誉和客户保留至关重要,并且绝不能与其他个人共享密钥。密钥存储在可以使用密码保护的二进制文件中;此类文件称为 KeyStore。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 及更低版本,您可以使用 Java JDK 的一部分“jarsigner”。有关整个过程的详细信息,请参见官方 Android 文档;但是,下面给出了一个示例来说明这一点。

apksigner sign --out mySignedApp.apk --ks myKeyStore.jks myUnsignedApp.apk

在此示例中,未签名的应用 ('myUnsignedApp.apk') 将使用开发者密钥库 'myKeyStore.jks'(位于当前目录中)中的私钥进行签名。该应用将变成一个名为 'mySignedApp.apk' 的签名应用,并且可以发布到应用商店。

Zipalign

在分发 APK 文件之前,应始终使用 zipalign 工具对齐 APK 文件。此工具对齐 APK 中所有未压缩的数据(例如图像、原始文件和 4 字节边界),这有助于改善应用运行时期间的内存管理。

必须在用 apksigner 签名 APK 文件之前使用 Zipalign。

发布流程

由于 Android 生态系统是开放的,因此可以从任何地方(您自己的网站、任何商店等)分发应用。但是,Google Play 是最广为人知、最受信任和最受欢迎的商店,并且 Google 本身也提供它。Amazon Appstore 是 Kindle 设备的受信任默认商店。如果用户要从不受信任的来源安装第三方应用,他们必须在其设备安全设置中明确允许这样做。

可以从各种来源在 Android 设备上安装应用:通过 USB 本地安装,通过 Google 的官方应用商店(Google Play 商店)或来自其他商店。

其他供应商可能会在实际发布应用之前对其进行审核和批准,而 Google 只会扫描已知的恶意软件签名;这最大限度地减少了发布过程的开始与公开应用可用性之间的时间。

发布应用非常简单;主要操作是使签名的 APK 文件可供下载。在 Google Play 上,发布从帐户创建开始,然后通过专用界面交付应用。详细信息请参见官方 Android 文档