跳过内容

Android 代码质量和构建设置

概述

应用签名

Android 要求所有 APK 在安装或运行前都必须使用证书进行数字签名。数字签名用于验证应用程序更新时所有者的身份。此过程可以防止应用程序被篡改或修改以包含恶意代码。

当 APK 签名后,会附带一个公钥证书。该证书将 APK 与开发者及其私钥唯一关联起来。当应用程序在调试模式下构建时,Android SDK 会使用专门为调试目的创建的调试密钥对应用程序进行签名。使用调试密钥签名的应用程序不应分发,并且大多数应用商店(包括 Google Play 商店)都不会接受。

应用的最终发布版本必须使用有效的发布密钥进行签名。在 Android Studio 中,可以通过手动或创建分配给发布构建类型的签名配置来签名应用。

在 Android 9 (API level 28) 之前,Android 上的所有应用更新都需要使用相同的证书进行签名,因此建议有效期为 25 年或更长。在 Google Play 上发布的应用程序必须使用有效期截至 2033 年 10 月 22 日之后的密钥进行签名。

有三种 APK 签名方案可用

  • JAR 签名 (v1 方案),
  • APK 签名方案 v2 (v2 方案),
  • APK 签名方案 v3 (v3 方案)。

v2 签名受 Android 7.0 (API level 24) 及更高版本支持,与 v1 方案相比,提供了更高的安全性和性能。v3 签名受 Android 9 (API level 28) 及更高版本支持,使应用程序能够在 APK 更新时更改其签名密钥。此功能通过允许同时使用新旧密钥来确保兼容性和应用程序的持续可用性。请注意,在撰写本文时,它仅通过 apksigner 提供。

对于每种签名方案,发布版本应始终通过其所有先前的方案进行签名。

第三方库

Android 应用程序经常使用第三方库。这些第三方库可以加速开发,因为开发者需要编写更少的代码来解决问题。库分为两类

  • 不应(或不应)打包在实际生产应用程序中的库,例如用于测试的 Mockito 和用于编译某些其他库的 JavaAssist
  • 打包在实际生产应用程序中的库,例如 Okhttp3

这些库可能导致不必要的副作用

  • 库可能包含漏洞,从而使应用程序变得脆弱。一个很好的例子是 OKHTTP 2.7.5 之前的版本,其中可能存在 TLS 链污染以绕过 SSL pinning。
  • 库可能不再维护或很少使用,这就是为什么没有漏洞被报告和/或修复的原因。这可能导致您的应用程序通过该库包含不良和/或易受攻击的代码。
  • 库可以使用例如 LGPL2.1 等许可证,这要求应用程序作者向使用该应用程序并要求查看其源代码的人提供源代码访问权限。事实上,应用程序应该被允许在修改其源代码后重新分发。这可能会危及应用程序的知识产权 (IP)。

请注意,此问题可能存在于多个层面:当您使用在 webview 中运行 JavaScript 的 webview 时,JavaScript 库也可能存在这些问题。Cordova、React-native 和 Xamarin 应用程序的插件/库也存在同样的问题。

内存损坏错误

Android 应用程序运行在虚拟机上,其中大多数内存损坏问题都已得到处理。这并不意味着没有内存损坏错误。例如,CVE-2018-9522,它与使用 Parcel 的序列化问题有关。其次,在原生代码中,我们仍然看到与我们在一般内存损坏部分中解释的相同问题。最后,我们在支持服务中看到内存错误,例如 在 BlackHat 上展示的 Stagefright 攻击。

内存泄漏也经常是一个问题。例如,当 Context 对象的引用传递给非 Activity 类,或者当您将 Activity 类的引用传递给您的辅助类时,就可能发生这种情况。

二进制保护机制

检测二进制保护机制的存在很大程度上取决于用于开发应用程序的语言。

通常,所有二进制文件都应进行测试,这包括主应用程序可执行文件以及所有库/依赖项。然而,在 Android 上,我们将重点关注原生库,因为正如我们接下来将看到的,主可执行文件被认为是安全的。

Android 会优化其应用程序 DEX 文件(例如 classes.dex)中的 Dalvik 字节码,并生成一个包含原生代码的新文件,通常带有 .odex 或 .oat 扩展名。这个 Android 编译后的二进制文件(参见 探索应用包中的“编译后的应用二进制文件”)使用 ELF 格式进行封装,该格式是 Linux 和 Android 用于打包汇编代码的格式。

应用程序的 NDK 原生库(参见 探索应用包中的“原生库”)也使用 ELF 格式

  • PIE(位置无关可执行文件):
    • 自 Android 7.0 (API level 24) 起,主可执行文件默认启用 PIC 编译。
    • 从 Android 5.0 (API level 21) 开始,对未启用 PIE 的原生库的支持已被弃用,从那时起,PIE 由链接器强制执行
  • 内存管理:
    • 垃圾回收将直接为主二进制文件运行,二进制文件本身无需检查。
    • 垃圾回收不适用于 Android 原生库。开发者负责进行适当的手动内存管理。参见“内存损坏错误”
  • 栈溢出保护:
    • Android 应用被编译为 Dalvik 字节码,这被认为是内存安全的(至少在缓解缓冲区溢出方面)。其他框架,如 Flutter,不会使用栈保护(stack canaries)进行编译,因为它们的语言(在本例中是 Dart)以不同的方式缓解缓冲区溢出。
    • Android 原生库必须启用它,但可能难以完全确定。
      • NDK 库应启用它,因为编译器默认会这样做。
      • 其他自定义 C/C++ 库可能未启用它。

了解更多

可调试应用

调试是开发者识别和修复其 Android 应用中错误或缺陷的必要过程。通过使用调试器,开发者可以选择在其设备上调试应用,并在 Java、Kotlin 和 C/C++ 代码中设置断点。这使他们能够分析变量并在运行时评估表达式,从而帮助他们找出许多问题的根本原因。通过调试应用,开发者可以改进其应用的功能和用户体验,确保其流畅运行而不会出现任何错误或崩溃。

每个启用了调试器的进程都会运行一个额外的线程来处理 JDWP 协议包。此线程仅针对在 Android Manifest 的 Application 元素中具有 android:debuggable="true" 属性的应用程序启动。

调试符号

通常,您应该提供尽可能少解释的编译代码。一些元数据,例如调试信息、行号和描述性函数或方法名称,使逆向工程师更容易理解二进制文件或字节码,但这些在发布版本中不需要,因此可以安全地省略而不会影响应用程序的功能。

要检查原生二进制文件,请使用 nmobjdump 等标准工具来检查符号表。发布版本通常不应包含任何调试符号。如果目标是混淆库,也建议删除不必要的动态符号。

调试代码和错误日志记录

StrictMode

StrictMode 是一个开发者工具,用于检测违规行为,例如意外地对应用程序主线程进行磁盘或网络访问。它还可以用于检查良好的编码实践,例如实现高性能代码。

可以使用 ThreadPolicy BuilderVmPolicy Builder 设置不同的策略。

对检测到的策略违规的反应可以通过使用一个或多个 penalty* 方法进行设置。例如,可以启用 penaltyLog() 将任何策略违规记录到系统日志中。

下面是 StrictMode 的一个示例,其中启用了对主线程的磁盘和网络访问策略。如果检测到此情况,会将日志消息写入系统日志,并强制应用程序崩溃。

public void onCreate() {
     if (BuildConfig.DEBUG) {
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }

建议将策略包含在带有 BuildConfig.DEBUG 条件的 if 语句中,以便仅对应用程序的调试版本自动启用 StrictMode 策略。

异常处理

当应用程序进入异常或错误状态时,会发生异常。Java 和 C++ 都可能抛出异常。测试异常处理是为了确保应用程序能够处理异常并转换到安全状态,而不会通过 UI 或应用程序的日志记录机制暴露敏感信息。