[翻译]OWASP 安卓测试指南(v1.2 - 14 May 2020)节选

原文链接:
平台概述https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05a-platform-overview
Android 基础安全测试https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05b-basic-security_testing
Android 反逆向防御https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05j-testing-resiliency-against-reverse-engineering

目录

平台概述

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

  1. Android安全架构
  2. Android应用程序结构
  3. 进程间通信(IPC)
  4. Android应用发布
  5. Android应用攻击面
    访问官方的 Android 开发者文档网站(https://developer.android.com/),可以获取更多有关 Android 平台的详细信息。

Android 安全架构

Android 是谷歌基于 Linux 开发的开源平台,它充当移动操作系统(OS)。如今,该平台是各种现代技术的基础,如手机、平板电脑、可穿戴技术、电视和其他"智能"设备。典型的 Android 版本具有一系列预装("内置")的应用程序,并支持通过 Google Play 商店和其他市场安装第三方应用。

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

Android 软件栈

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

Android 应用程序通常使用 Java 编写并编译为 与传统的 Java 字节码有些不同的 Dalvik 字节码。首先将 Java 代码编译为 .class 文件,然后使用 dx 工具将 JVM 字节码转换为 Dalvik 的 .dex 格式,这样就创建了 Dalvik 字节码。

Java vs Dalvik

当前版本的安卓在 Android 运行时(ART)上执行此字节码。ART 是安卓原始运行时 Dalvik 虚拟机的后继者。 Dalvik 和 ART 之间的关键区别在于字节码的执行方式。

在Dalvik中,字节码在执行时转换为机器码,这一过程称为即时(JIT)编译。JIT编译会对性能产生不利的影响:每次执行应用程序时都必须进行编译。为了提高性能,ART 引入了提前(AOT)编译。顾名思义,应用会在首次执行之前进行预编译。预编译的机器代码会用于所有后续的执行。AOT 将性能提高了两倍,同时降低了功耗。

Android 应用程序不能直接访问硬件资源,并且每个应用程序都运行在自己的沙盒中。这允许对资源和应用程序进行精确的控制:例如,崩溃的应用程序不会影响设备上运行的其他应用程序。同时,Android 运行时会控制分配给应用程序的最大系统资源数量,防止任何一个应用程序垄断过多的资源。

安卓用户和组

尽管 Android 操作系统是基于 Linux,但它并不像其他类 Unix 系统那样实现用户帐户。在 Android 中 Linux 内核对应用沙盒的多用户支持:除了少数的例外,每个应用程序都像运行在一个单独的 Linux 用户下,有效地与其他应用和操作系统的剩余部分隔离。

文件 `system/core/include/private/android_filesystem_config.h`(http://androidxref.com/7.1.1_r6/xref/system/core/include/private/android_filesystem_config.h)包含了一个系统进程分配的预定义用户和组的列表。安装其他应用程序时,会添上它们的 UID(userID)。更多详细信息,参阅 Bin Chen 关于 Android 沙箱的博客文章(https://pierrchen.blogspot.com/2016/09/an-walk-through-of-android-uidgid-based.html)。

例如,Android 7.0(API 等级24)定义了以下系统用户:

#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              10000 /* first app user */
...

Android 设备加密

安卓从 Android 2.3.4(API 等级10)开始支持设备加密,此后发生了一些大的变化。Google 强制所有的设备运行 Android 6.0(API 等级23)或支持存储加密的更高版本,有些低端设备被豁免,因为这会严重影响性能。在下面各节中,你可以找到有关设备加密及其算法的信息。

全盘加密

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

基于文件加密

Android 7.0(API 等级24)支持基于文件的加密。基于文件的加密允许使用不同的密钥对不同的文件进行加密,以便可以独立解密它们。支持这种加密方式的设备也支持 Direct Boot 模式,Direct Boot 模式下设备可以在用户未解锁设备时访问警报或辅助服务等能。

Adiantum

AES 在大多数现代 Android 设备上用于存储加密。实际上,AES 已经成为一种广泛使用的算法,最新的处理器实现有专用的指令集来提供硬件加速加密与解密操作,例如带有 Cryptography Extensions 的 ARMv8 或具有AES-NI 扩展的 x86。但是,并非所有设备都能及时使用 AES 进行存储加密。尤其是运行Android Go 的低端设备。这些设备通常使用低端处理器,例如 ARM Cortex-A7,它们没有硬件加速的 AES。

Adiantum 是由 Google 的 Paul Crowley 和 Eric Biggers 设计的密码结构,用于填补那些不能以至少 50 MiB/s 速度运行 AES 的设备的空白。Adiantum 仅依赖于加法、移位旋转和异或,所有处理器都支持这些操作,与使用 AES 相比,低端处理器的加密速度是 AES 的4倍,解密速度是 AES 的5倍。

  • Adiantum 由其他密码组成:
  • NH:哈希函数。
  • Poly1305:消息认证代码(MAC)。
  • XChaCha12:一种流密码。
  • AES-256:AES的一次调用。

Adiantum是一种新的密码,但只要 ChaCha12 和 AES-256 被认为是安全的,它就是安全的。它的设计者没有创建任何新的加密基元,而是依赖于其他著名的、经过深入研究的基元来创建一个新性能的算法。

Adiantum 适用于 Android 9(API 等级 28)和更高版本。Linux 内核5.0及更高版本支持该功能,而内核4.19、4.14 和4.9需要修补。Android 不向应用程序开发人员提供使用 Adiantum 的API。这个加密将由 ROM 开发人员或设备供应商希望在不牺牲低端设备性能的情况下提供全磁盘加密时考虑并实施。在撰写本文时,尚无公共密码库实现此加密可在 Android 应用程序上使用。应该注意的是,AES 在具有 AES指令集的设备上运行得更快,在这种情况下强烈不建议使用 Adiantum。

Android 安全强化

Android 包含许多不同的功能,试图使恶意应用程序更难逃逸出沙盒。由于应用程序在您的设备上有效地运行代码,很重要的一点就是,即使应用程序本身不可信但也可以安全地执行。以下各节解释了哪些缓解措施可以防止应用程序滥用漏洞。需要注意的是,操作系统从来都不是 100% 安全的,即使采取了这些缓解措施,新的漏洞也仍会定期被发现。

SELinux

安全强化的 Linux(SELinux)使用强制访问控制(MAC)系统来进一步锁定哪些进程可以访问哪些资源。每个资源都有一个标签,其形式是 user:role:type:mls_level,它定义了哪些用户可以对其执行哪种类型的操作。例如,一个进程可能只能读取文件,而另一个进程可以编辑或删除该文件。这样,通过使用最小特权原则,易受攻击的进程更难通过权限提升或内网漫游被漏洞利用。

有关更多信息,请访问 Android 安全网站(https://source.android.com/security/selinux)。

ASLR, KASLR, PIE and DEP

自 Android 4.1(API 等级 15)以来,地址随机化(ASLR)就已成为 Android 的一部分,它是防止缓冲区溢出攻击的标准保护措施,这样可以确保将应用程序和操作系统都加载到随机的内存地址,从而很难获取特定的内存区域或库的正确地址。在 Android 8.0(API 等级26)中,也为内核实现了这种保护(KASLR)。仅当可以将应用程序加载到内存中的随机位置时才可以使用ASLR保护,这由应用程序的位置无关可执行文件(PIE)标志指示。从 Android 5.0(API 等级 21)开始,不再支持未启用 PIE 的原生库。最后,数据执行保护(DEP)可以防止代码在堆栈和堆上执行,这也可以用来防止缓冲区溢出漏洞。

有关更多信息,请访问 Android 开发者博客(https://android-developers.googleblog.com/2016/07/protecting-android-with-more-linux.html)。

SECCOMP

Android 应用程序可以包含用C或C ++编写的本机代码。这些编译的二进制文件可以通过 Java 本地接口(JNI)绑定与 Android 运行时通信,也可以通过系统调用与 OS 通信。某些系统调用没用被实现,或者不应由普通应用程序调用。由于这些系统调用直接与内核通信,因此它们是漏洞利用开发人员的主要目标。在 Android 8(API 等级26)上,Android 引入了对所有基于 Zygote 的进程(即用户应用程序)的 Secure Computing(SECCOMP)过滤器的支持, 这些过滤器将可用的系统调用限制为 bionic 公开的系统调用。

有关更多信息,请访问 Android 开发者博客(https://android-developers.googleblog.com/2017/07/seccomp-filter-in-android-o.html)。


Android 上的应用程序

与操作系统通信

Android 应用程序通过与提供高级 Java API 的抽象层即 Android 框架层与系统服务进行交互,这些服务大多数是通过常规的 Java 方法来调用的,并转换为对在后台运行的系统服务的 IPC 调用。系统服务的实例有:

  • 连接性(Wi-Fi,蓝牙,NFC等)
  • 文件
  • 照相机
  • 地理位置(GPS)
  • 麦克风

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

每个新的 Android 版本都会更改API规范。关键的漏洞修复和安全补丁通常也适用于早期的版本,在撰写本文时,支持的最旧的 Android 版本是 Android 8.1(API 等级27),当前的 Android 版本是Android 10(API 等级29)。

值得注意的API版本:

  • Android 4.2(API 等级16),2012年11月发布 (引入 SELinux)
  • Android 4.3(API 等级18),2013年7月发布(默认启用SELinux)
  • Android 4.4(API 等级19),2013年10月发布(引入了几个新的 API 和 ART)
  • Android 5.0(API 等级21),2014年11月发布(默认使用 ART,并添加了许多其他功能)
  • Android 6.0(API级别23),2015年10月发布(许多新功能和改进,包括在运行时授予详细的权限设置,而不是在安装期间授予全部权限或不提供任何权限)
  • Android 7.0(API 等级24-25),2016年8月发布(ART 上的新 JIT 编译器)
  • Android 8.0(API 等级26-27),2017年8月发布(安全性方面做了很多改进)
  • Android 9(API 等级28),2018年8月发布(限制麦克风或摄像头的后台使用,引入锁定模式,所有应用程序默认 HTTPS)
  • Android 10(API级别29),2019年9月发布(通知气泡,Project Mainline)

普通应用程序的Linux UID / GID

Android 利用 Linux 用户管理来隔离应用程序。但 这种方法不同于传统 Linux 环境下多个应用程序常由同一用户运行的用户管理用法。安卓为每个 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和权限之间的关系在下面的文件中定义:

frameworks/base/data/etc/platform.xml(http://androidxref.com/7.1.1_r6/xref/frameworks/base/data/etc/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>

应用沙盒

应用程序在`Android`的应用沙盒中执行,应用沙盒可以将应用程序的数据和代码执行与设备上的其他应用程序隔离,这种隔离增加了一个安全层。

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

Sandbox

我们可以通过查看`/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">

Zygote

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

App 生命周期

在 Android 中,应用程序进程的生存期由操作系统控制。当启动应用程序组件并且该应用程序尚未运行任何其他组件时,一个新的 Linux 进程会被创建。当不再需要进程或需要回收内存以运行更重要的应用程序时,Android 可能杀死该进程。 进程是否杀死主要和用户与进程交互的状态有关。通常,进程可能处于四种状态之一。

  • 前台进程(例如,在屏幕顶部运行的活动或正在运行的广播接收器)

  • 可见进程是用户意识到的进程,因此终止该过程将对用户体验产生明显的负面影响。一个例子是运行一个用户在屏幕上可见但在前台不可见的活动。

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

  • 缓存进程是当前不需要的进程,因此系统可以在需要内存杀死它。
    应用程序必须实现对多个事件做出反应的回调方法。例如,在首次创建应用进程时会调用`onCreate`处理程序,其他回调方法包括 `onLowMemory`,`onTrimMemory` 和 `onConfigurationChanged`。

App Bundles

Android 应用程序能以两种形式发布:A
Android Package Kit(APK)文件或
Android App Bundle(.aab)。Android App Bundle 提供了应用程序需要的所有资源,但推迟了 APK 的生成及向 Google Play 的签名。
App Bundles 是经过签名的二进制文件,包含有应用程序的代码在几个模块中。基本模块包含应用程序的核心,可以通过各种包含应用程序的新的功能的模块进行扩展,这在 app bundle 开发者文档(https://developer.android.com/guide/app-bundle)中有进一步的说明。如果您有一个Android App Bundle,最好使用 Google 的bundletool(https://developer.android.com/studio/command-line/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 Manifest

每个应用程序都有一个 Android Manifest 文件,该文件以二进制 XML 格式嵌入内容。这个文件的标准名称是 AndroidManifest.xml,它位于应用程序的 Android Package Kit(APK)文件的根目录中。

Manifest 文件描述了应用程序的结构、的组件(活动、服务、内容提供者和 intent 接收者)和请求的权限。它还包含通用的应用程序的元数据,比如应用程序的图标、版本号和主题。该文件可能会列出其他信息,比如兼容的 API(最小、目标和最大的SDK版本)和可以安装的存储类型(外部或内部)(https://developer.android.com/guide/topics/data/install-location.html)。

下面是一个清单文件的示例,包括包名(惯例是反向的URL,但任何字符串都可以接受)。它还列出了应用程序版本,相关 SDK,需要的权限,给出了内容提供者,使用 intent 过滤器的广播接收器和应用程序及其活动的描述:

<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 Manifest file 文档(https://developer.android.com/guide/topics/manifest/manifest-intro.html)中。

App 组件

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

  • 活动
  • 碎片
  • Intent
  • 广播接收器
  • 内容提供者和服务

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

活动

Activity 组成了应用程序的可见部分。每个页面都有一个活动,所以一个有三个不同页面的应用程序会实现三个不同的活动。活动通过继承 Activity 类来声明,包含所有用户界面元素:碎片、视图和布局。

每个活动都需要在 Android 清单中使用以下语法声明:

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

未在清单文件中声明的活动不会被显示,试图启动它们将引发异常。

和应用程序一样,活动也有自己的生命周期,并且需要监视系统的变化来处理它们。活动可以处于以下状态:活动、暂停、停止和不活动,这些状态由 Android 操作系统管理。对应的, 活动可以实现以下事件管理器:

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

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

碎片

Fragment 表示活动中的一个行为或用户界面的一部分。Android 在 Honeycomb 3.0 版本 (API 等级11)中引入了碎片。

碎片用于封装界面的各个部分,以促进可重用性和适应不同屏幕大小。碎片是自治的实体,它们包含了所有需要的组件(它们有自己的布局、按钮等等)。然而,它们必须与活动集成才能有效:碎片不能单独存在。它们有自己的生命周期,与实现它们的活动的生命周期相关联。

因为碎片有自己的生命周期,所以碎片类包含可以重新定义和继承的事件管理器。这些事件管理器包括 onAttach、onCreate、onStart、onDestroy 和 onDetach。其他一些例子,读者应该参考 Android Fragment 规范(https://developer.android.com/guide/components/fragments)以获得更多细节。

通过继承 Android 提供的 Fragment 类,可以轻松实现碎片:

Java 示例:

public class MyFragment extends Fragment {
    ...
}

Kotlin 示例:

class MyFragment : Fragment() {
    ...
}

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

为了管理碎片,活动可以使用碎片管理器 (`FragmentManager`类)。这个类使得查找、添加、删除和替换相关的碎片变得很容易。

碎片管理器可以通过以下方式创建:

Java 示例:

FragmentManager fm = getFragmentManager();

Kotlin 示例:

var fm = fragmentManager

碎片不一定有用户界面,它们可以成为管理和应用程序用户界面相关的后台操作的一种方便而高效的方式。一个片段可以被声明为持久的,这样即使活动被破坏,系统仍然保持它的状态不变。

进程间通信

我们已经了解到,每个`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`方法。代理对象透明地将调用参数打包并将事务发送到`Binder`服务器,`Binder`服务器实现为一个字符驱动程序(`/dev/Binder`)。服务器持有一个线程池,用于处理传入请求并将消息传递给目标对象。从客户端应用程序的角度来看,所有这些都像是一个常规的方法调用,但繁重的工作都是由`Binder`框架完成的。

Binder 概述

`Binder`概述图片来源:
Android Binder by Thorsten Schreiber(https://www.nds.ruhr-uni-bochum.de/media/attachments/files/2011/10/main.pdf

允许其他应用程序绑定的服务称为绑定服务,这些服务必须向客户机提供`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 消息传递是建立在 Binder 之上的异步通信框架。此框架允许点对点和发布-订阅消息传递。Intent是一个消息传递对象,可以用来请求另一个应用组件的动作。Intent 以几种方式促进组件间的通信,下面是三个基本用例:

  • 开启活动
    活动表示应用程序中的一个界面。你可以通过将 intent 传递给 startActivity 来启动活动的一个新的实例。Intent 描述了活动并携带必要的数据。

  • 开启服务
    服务是在后台执行操作的组件,没有用户界面。在 Android 5.0(API 等级21)及更高版本中,你可以使用 JobScheduler 启动服务。

  • 传递广播
    广播是任何应用程序都能接收到的信息。系统为系统事件传递广播,包括系统引导和充电初始化。你可以通过传递`intent`给`sendBroadcast`或`sendOrderedBroadcast`,来将一个广播传递给其他应用程序。

`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 Manifest`文件中的一个表达式,它指定组件想要接收的`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`,而不能将`intent`发送到操作系统的其他部分。这对于确保敏感和私有数据不会离开应用程序的边界(例如地理位置数据)非常有用。

广播接收器

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

有两种方法可以让系统知道一个广播接收器。一种方法是在`Android Manifest`文件中声明它,清单中应该指定广播接收器和`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`被发出时,系统会自动启动一个带有相关已注册接收器的应用程序。

根据广播概述(https://developer.android.com/guide/components/broadcasts),如果不是专门针对应用程序,广播会被认为是“隐式的”。在接收到一个的隐式广播后,Android 会列出所有在其过滤器中注册了特定动作的应用程序。如果有多个应用程序注册了相同的操作,`Android`会提示用户从可用的应用程序列表中进行选择。

广播接收器的一个有趣功能是可以对它们进行优先级排序;通过这种方式,`intent`将根据优先级被传递给所有授权的接收者。优先级可以通过清单中的`android:priority`属性来分配,也可以通过`IntentFilter.setPriority`方法编程分配。但是请注意,具有相同优先级的接收者将以任意顺序运行(https://developer.android.com/guide/components/broadcasts.html#sending-broadcasts)。

如果你的应用不应该跨应用程序发送广播,可以使用本地广播管理器(`LocalBroadcastManager`)。它们可以用来确保只接收来自内部应用程序的`intent`,而来自其他应用程序的`intent`将被丢弃。这对于提高应用程序的安全性和效率非常有用,因为不涉及进程间通信。但是请注意 LocalBroadcastManager 类已不推荐使用(https://developer.android.com/reference/androidx/localbroadcastmanager/content/LocalBroadcastManager.html),谷歌建议使用`LiveData`(https://developer.android.com/reference/androidx/lifecycle/LiveData.html)等替代方法。

有关广播接收器的更多安全注意事项,请参见安全注意事项和最佳做法(https://developer.android.com/guide/components/broadcasts.html#security-and-best-practices)。

隐式广播接收器限制

根据后台优化(https://developer.android.com/topic/performance/background-optimization),针对 Android 7.0(API 等级24)或更高的应用不再接收`CONNECTIVITY_ACTION`广播,除非使用`Context.registerReceiver()`注册广播接收器。系统也不再发送`ACTION_NEW_PICTURE`和`ACTION_NEW_VIDEO`广播。

根据后台执行限制(https://developer.android.com/about/versions/oreo/background.html#broadcasts),针对Android 8.0(API 等级26)及以上的应用,在隐式广播例外情况中列出的除外(https://developer.android.com/guide/components/broadcast-exceptions),不能在其清单中注册隐式广播接收器。在运行时通过调用`Context.registerReceiver`创建广播接收器不受此限制的影响。

根据系统广播发生的更改,从Android 9(API 等级28)开始,`NETWORK_STATE_CHANGED_ACTION`广播不再接收用户的位置信息或个人身份信息。

内容提供者

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

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

服务

服务是 Android OS 的组件(基于 Service 类),它们在后台执行任务(数据处理、启动 intent 和通知等),而不提供用户界面。服务是用来长期运行进程的,它们的系统优先级低于活动应用程序,而高于不活动应用程序。因此,当系统需要资源时,它们不太可能被杀死,并且可以将它们配置为在有足够的可用资源时自动重启,这使得服务非常适合运行后台任务。请注意,服务和活动一样,都是在主应用程序线程中执行的。服务不会创建自己的线程,也不会在单独的进程中运行,除非另行指定。

权限

由于 Android 应用程序是安装在沙箱中,最初无法访问用户信息和系统组件(比如摄像头和麦克风),Android 为应用程序可以请求的特定任务提供了一套预定义的权限。例如,如果你想让你的应用程序使用手机的摄像头,你必须请求 android.permission.CAMERA 权限。在Android 6.0(API 等级23)之前,应用程序请求的所有权限都在安装时授予。从 API 等级23开始,用户只能在应用程序执行期间同意一些权限请求。

保护等级

Android 的权限是根据其提供的保护级别进行排序的,分为四种不同的类别:

  • Normal:较低的保护级别。 它使应用程序可以访问隔离的应用程序级功能,而对其他应用程序、用户或系统的风险最小。它是在应用安装过程中授予的,并且是默认的保护级别。
    例如:`android.permission.INTERNET`

  • Dangerous:此权限允许应用程序执行可能影响用户隐私或用户设备正常运行的操作。安装期间可能不会授予此级别的权限,用户必须决定应用程序是否应该有这个权限。
    例如:`android.permission.RECORD_AUDIO`

  • Signature:只有在请求应用程序与声明该权限的应用程序使用相同的证书签名时,才会授予此权限。如果签名匹配,则自动授予权限。
    例如:`android.permission.ACCESS_MOCK_LOCATION`

  • SystemOrSignature:此权限仅授予系统映像中嵌入的应用程序或使用与声明该权限的应用相同的证书进行签名的应用程序。
    例如:`android.permission.ACCESS_DOWNLOAD_MANAGER`

请求权限

应用程序可以通过在其清单中包含 `

<uses-permission />` 标签来请求正常、危险和签名保护级别的权限。下面的例子是一个请求读取短信权限的 AndroidManifest.xml 示例:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.permissions.sample" ...>
 
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    <application>...</application>
</manifest>

声明权限

应用程序可以向系统上安装的其他应用程序公开功能和内容。为了限制对自己组件的访问,可以使用任何 Android 的预定义权限(https://developer.android.com/reference/android/Manifest.permission.html)或定义自己的权限,使用元素`<permission>`声明一个新的权限。下面的例子展示了一个应用程序的权限声明:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.permissions.sample" ...>
 
    <permission
    android:name="com.permissions.sample.ACCESS_USER_INFO"
    android:protectionLevel="signature" />
    <application>...</application>
</manifest>

上面的代码定义了一个新的权限`com.permissions.sample.ACCESS_USER_INFO`,其保护级别为`Signature`。任何受此权限保护的组件只能由相同开发者证书签名的应用程序访问。

Android组件的强制权限

Android 组件可以使用权限机制来保护它们的接口。可以通过在`AndroidManifest.xml`中的相应组件标签中添加属性`android:permission`来在活动、服务和广播接收器使用强制权限。

<receiver
    android:name="com.permissions.sample.AnalyticsReceiver"
    android:enabled="true"
    android:permission="com.permissions.sample.ACCESS_USER_INFO">
    ...
</receiver>

内容提供者有一些不同,它们支持一组单独的权限,使用内容 URI 来读取,写入和访问内容提供者。

  • `android:writePermission`,`android:readPermission`:开发人员可以设置单独的读取或写入权限。
  • `android:permission`:控制对内容提供者读写的一般权限。
  • `android:grantUriPermissions`:如果可以使用内容URI 访问内容提供者,则为“ true”(访问暂时绕过其他权限的限制),否则为“ false”。

签名和发布流程

一旦应用程序成功开发,下一步就是发布分享给其他人。然而,应用程序不能简单地添加到商店并共享,它们必须首先签名,该加密签名作为应用程序开发者放置的可验证标记。它识别应用程序的作者,并确保应用程序自最初发布以来没有被修改过。

签名过程

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

APK 签名方案

Android 支持三种应用签名方案。从 Android 9(API level 28)开始,APK 可以通过 APK 签名方案 v3 、APK 签名方案 v2 或 JAR 签名(v1 方案)进行验证。对于 Android 7.0 (API 等级24)及以上版本,可以通过 APK 签名方案 v2 或 JAR 签名(v1方案)对 APK 进行验证。为了向后兼容,APK 可以使用多种签名方案进行签名,以使应用程序在较新的和较旧的 SDK 版本上都运行。旧的平台会忽略 v2 签名,只验证 v1 签名(https://source.android.com/security/apksigning/)。

JAR 签名(v1 方案)

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

APK 签名方案(v2方案)

在 APK 签名方案中,对完整的 APK 进行哈希和签名,然后创建一个 APK 签名分块并插入到 APK 中。在验证期间,v2 方案检查整个 APK 文件的签名。这种形式的 APK 验证速度更快,并且提供了更全面的防篡改保护。你可以在下面看到 v2 方案的 APK 签名验证过程(https://source.android.com/security/apksigning/v2#verification)。

apk 签名验证过程 v2 方案

APK 签名方案(v3 方案)

v3 APK 签名块格式与 v2相 同。v3 将有关受支持的 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 方案验证过程。(https://source.android.com/security/apksigning/v3

apk 签名验证过程 v3 方案

创建证书

Android 使用公共/私有证书来签名 Android 应用程序(.apk文件)。证书包含了大量的信息,就安全性而言,密钥是是最重要的信息类型。公共证书包含用户的公钥,私有证书包含用户的私钥。公共证书和私有证书是链接的。证书是唯一的不能重新生成,请注意,如果一个证书丢失了它就无法恢复,因此更新任何使用该证书签名的应用程序将变得不可能。应用程序创建者可以重用可用密钥存储库中现有的私有/公共密钥对,也可以生成新的密钥对。在 Android SDK 中,使用 keytool 命令生成一个新的密钥对。下面的命令创建一个密钥长度为2048位、过期时间为7300天即20年的 RSA 密钥对。生成的密钥对存储在当前目录中的文件 'myKeyStore.jks' 中):

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

安全地存储你的密钥并确保它在整个生命周期中保持机密性是极其重要的。任何获得密钥访问权限的人都可以将你无法控制的内容(从而添加不安全的特性或使用基于签名的权限访问共享内容)更新到你的应用程序中。用户对应用程序及其开发者的信任完全基于这些证书,因此,证书保护和安全管理对于声誉和客户去留至关重要,密钥永远不能与其他个人共享。密钥存储在可以用密码保护的二进制文件中,这样的文件被称为密钥存储库。密钥存储库的密码应该是强壮的,并且只有密钥创建者知道。因此,密钥通常存储在开发人员对其访问受限的专用构建机器上。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 官方文档中找到,下面给出了一个例子来说明这一点。

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

在这个示例中,一个未签名的应用程序(`myUnsignedApp.apk`)将使用来自开发人员密钥存储库`myKeyStore.jks`(位于当前目录中)的私钥进行签名。该应用程序将成为名为`mySignedApp.apk`的已签名应用程序,并准备发布到商店。

Zipalign

在发布之前,应该始终使用`zipalign`工具来对齐 APK 文件。这个工具在 APK 中对齐所有未压缩的数据(如图像、原始文件和4字节边界),这有助于在应用程序运行时改进内存管理。

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

发布过程

因为 Android 生态系统是开放的,所以可以在任何地方(你自己的网站,任何商店等等)发布应用程序。然而,Google Play 是最知名、最受信任和最受欢迎的商店,Google 本身就提供这种服务。亚马逊应用商店是 Kindle 设备的默认可信的商店。如果用户想要从不受信任的来源安装第三方应用程序,他们必须在设备安全设置中明确允许这样做。

安卓设备上可以通过多种渠道安装应用:本地USB、Goolgle 官方应用商店(Google Play Store)或其他商店。

鉴于其他供应商可能会在应用程序真正发布前对其进行审查和批准,谷歌只会扫描已知的恶意软件签名,这将缩短发布过程的开始到公开应用程序可用性之间的时间。

发布一个应用程序非常简单,主要操作是使签名 APK 文件可下载。在 Google Play 上,发布从创建账号开始,然后通过专用界面发布应用程序。详细信息可以在 Android 官方文档(https://developer.android.com/distribute/best-practices/launch)中找到。

安卓应用攻击层面

Android 应用程序攻击面由应用程序的所有组件组成,包括发布应用程序和支持其功能所需的支持材料。Android应用程序可能容易受到攻击如果不这样做:

  • 通过 IPC 通信或 URL 方案验证所有输入,见:
    通过 IPC 测试敏感功能暴露
    测试自定义URL方案

  • 验证用户在输入字段中的所有输入。

  • 验证在 WebView 中加载的内容,见:
    在 Webview 中测试 JavaScript 执行
    测试 WebView 协议处理程序
    确定是否通过 Webview 公开 Java 对象

  • 安全地与后端服务器通信,否则易在服务器和移动应用程序之间受到中间人攻击
    测试网络通信
    安卓网络 API

  • 安全存储所有本地数据,不从存储中加载不可信的数据,参见:
    Android 上的数据存储

  • 保护自己免受环境破坏,如重新打包或其他本地攻击,见:
    Android反逆向防御

Android 基础安全测试

基础的安卓测试步骤

到目前为止,你应该对 Android 应用程序的结构和部署方式有了基本的了解。在本章中,我们将讨论如何建立一个安全测试环境,并描述您将使用的基本测试流程。本章是后面章节中讨论的更详细的测试方法的基础。
您可以在几乎所有运行 Windows、Linux 或 Mac OS 的机器上设置一个功能完整的测试环境。

主机设备

至少,你需要 Android Studio(附带 Android SDK)平台工具、一个模拟器和一个应用程序来管理各种 SDK 版本和框架组件。Android Studio 还附带了一个用于创建模拟器映像的 Android 虚拟设备(AVD)管理器应用程序。确保你的系统上安装了最新的 SDK 工具平台工具包

此外,如果你打算使用包含原生库的应用程序,你可能想要通过安装 Android NDK来完成主机设置(这在“Android 上的篡改和逆向工程”一章中也会提到)。

设置 Android SDK

通过 Android Studio 来管理本地的 Android SDK 安装。在 Android Studio 中创建一个空项目并选择 Tools -> SDK Manager 来打开 SDK Manager GUI。SDK Platform 选项卡上可以安装多个 API 等级的 SDK。最近的 API 等级为:

  • Android 10.0(API 等级29)
  • Android 9.0(API 等级28)
  • Android 8.1(API 等级27)
  • Android 8.0(API 等级26)

所有 Android 代号、版本号和 API 等级的概述可以在 Android 开发者文档中找到。

已安装的 SDK 位于以下路径:

Windows:

C:\Users\<username>\AppData\Local\Android\sdk

MacOS:

/Users/<username>/Library/Android/sdk

注意:在`Linux`上,你需要选择一个SDK目录,`/opt`、`/srv` 和 `/usr/local`是常见的选择。

设置 Android NDK

Android NDK 包含原生编译器和工具链的预编译版本。GCC 和 Clang 编译器在传统上都得到了支持,但是对 GCC 的积极支持在 NDK第14版结束了。设备体系结构和主机操作系统决定适当的版本,预编译的工具链在 NDK 的`toolchains`目录中,每种架构包含一个相应的子目录。

除了选择正确的体系结构之外,你还需要为想要目标 Native API 等级指定正确的 sysroot。sysroot 是一个包含目标系统头文件和库的目录。Native API 因 Android API 等级的不同而不同。每个Android API 等级的 sysroot 目录都可以在 $NDK/platforms/ 中找到。每个API级别目录都包含各种 CPU 和体系结构的子目录。

设置编译系统的一种可能是将编译器路径和必要的标记导出为环境变量。不过,为了让事情变得更简单,NDK 允许你创建一个所谓的独立工具链,这是一个临时的工具链,包含了所需的设置。

要建立一个独立的工具链,需要下载 NDK 最新的稳定版本。解压缩 ZIP 文件,切换到 NDK 根目录,运行以下命令:

$ ./build/tools/make_standalone_toolchain.py --arch arm --api 24 --install-dir /tmp/android-7-toolchain

这将在`/tmp/android-7-toolchain`目录中为Android 7.0(API 等级24)创建一个独立的工具链。为了方便,可以导出一个指向工具链目录的环境变量(我们将在示例中使用它)。运行以下命令或将它添加到 .bash_profile 中或其他启动脚本:

$  export TOOLCHAIN=/tmp/android-7-toolchain

测试设备

为了进行动态分析,你需要一个 Android 设备来运行目标应用程序。原则上,你可以在没有真正的 Android 设备的情况下进行测试,并且只使用模拟器。然而,应用程序在模拟器上执行得相当慢,模拟器可能不会给出真实的结果。在真实的设备上进行测试可以使过程更加顺畅,环境也更加真实。另一方面,模拟器允许你轻松地更改 SDK 版本或创建多个设备。下表列出了每种方法的优缺点。

Property Physical Emulator/Simulator
恢复能力 变成软砖总是可能的,但通常仍可以刷机,变成黑砖很少见。 模拟器可能会崩溃或损坏,但可以创建一个新的模拟器或恢复一个快照。
重置 可以恢复到工厂设置或刷机。 可以删除和重新创建模拟器。
快照 不可能。 更好地支持恶意软件分析。
速度 比模拟器快得多。 通常比较慢,但正在进行改进。
成本 可用的设备一般起价200美元,你可能需要不同的设备,例如带有或不带有生物识别传感器的设备。 免费的和商业的解决方案都存在。
root 难度 高度依赖于设备。 通常默认 root
模拟器检测难度 不是一个模拟器,模拟器检测不使用 将存在许多伪像,从而可以轻松检测到该应用程序正在模拟器中运行。
root 检测难度 隐藏 root 更容易,因为许多 root 检测算法都会检查模拟器属性, 借助Magisk Systemless,root 几乎无法检测到。 模拟器几乎总是会触发 root 检测算法,这是因为模拟器是为测试许多可发现的伪像而编译的。
硬件交互 通过蓝牙,NFC,4G,Wi-Fi,生物识别,相机,GPS,陀螺仪...轻松交互 通常相当有限,模拟硬件输入(例如,随机GPS坐标)
API 等级支持 取决于设备和社区。活跃的社区会不断发布更新的版本(比如 LineageOS),而不太流行的设备可能只会收到一些更新。在版本之间切换需要刷机,这是一个乏味的过程。 始终支持最新版本,包括 beta 版本。可以轻松下载和启动包含特定 API 等级的模拟器。
原生库支持 原生库通常是为 ARM 设备编译的,所以它们会在一个物理设备上工作 一些模拟器运行在 x86 CPU 上,因此它们可能不能运行打包的原生库。
恶意软件危险 恶意软件样本可以感染设备,但如果你可以清除设备存储并刷机,从而将其恢复到出厂设置,这应该不是问题。请注意,有些恶意软件样本会试图利用 USB 网桥。 恶意软件样本可以感染模拟器,但模拟器可以简单地删除和重新创建。也可以创建快照并比较不同的快照来帮助分析恶意软件。请注意,有证据存在试图攻击 hypervisor 的恶意软件。

在一个真实设备上测试

几乎任何物理设备都可以用于测试,但是有一些需要考虑的事项。首先,设备需要 root,通常通过漏洞利用或未锁定的引导加载程序来完成此操作。 漏洞利用并不总是可用的,并且引导加载程序可能被永久锁定,或者只有在运营商合同终止后才能被解锁。

最佳候选设备是为开发者打造的旗舰 Google pixel 设备。这些设备通常都带有未锁定的引导加载程序、开源的固件、内核、在线电台和官方操作系统源代码。开发者社区更喜欢 Google 设备,因为该操作系统最接近 android 开源项目。 这些设备通常具有最长的支持窗口,具有2年的 OS 更新和1年的安全更新。

另外,Google 的 Android One 项目包含的设备将会得到同样的支持窗口(2年的操作系统更新,1年的安全更新),并且有接近原生的经验。虽然它最初是一个针对低端设备的项目,但该项目已经发展到包括中端和高端智能手机,其中许多都得到了 modding 社区的积极支持。

LineageOS项目支持的设备也是测试设备的很好的候选设备。他们有一个活跃的社区,易于遵循的刷机和 root 说明,并且通常可以通过 Lineage 安装快速获得最新的 Android 版本。在 OEM 停止发布更新后很长一段时间里,LineageOS 还继续支持新的 Android 版本。

当使用 Android 物理设备时,你需要启用开发者模式和设备上的 USB 调试,以便使用 ADB 调试接口。自 Android 4.2(API 等级16)以来,手机设置菜单中开发者选项默认是隐藏的,要激活它,点击关于手机中的版本号部分七次。请注意,版本号字段的位置因设备略有不同。例如,在LG手机上,它是 About phone -> Software information。一旦你这样做了,开发者选项将显示在设置菜单的底部。一旦开发者选项被激活,你可以使用 USB 调试开关启用调试。

在模拟器上调试

存在多种模拟器,它们各有优缺点:

免费模拟器:

商业模拟器:

  • Genymotion - 具有许多特性的成熟仿模拟器,可以作为本地和基于云的解决方案,提供非商业使用的免费版本。

  • Corellium - 通过基于云或本地的解决方案提供自定义设备虚拟化。

虽然有其他一些免费的 Android 模拟器,但我们推荐使用 AVD,因为它提供了与其他模拟器相比更适合测试应用程序的增强功能。在本指南的其余部分中,我们将使用官方的 AVD 来执行测试。

AVD 支持一些硬件仿真,如 GPSSMS 和运动传感器

你可以使用 Android Studio 中的 AVD 管理器启动 Android 虚拟设备(AVD),也可以在命令行中使用`android`命令启动 AVD 管理器,在 Android SDK 的 tools 目录中可以找到:

$ ./android avd

可以使用一些工具和 VM 在模拟器环境中测试应用程序:

获取特权访问

Rooting(例如,修改 OS 以便你可以作为 root 用户运行命令)建议在真实设备上进行测试。这使你能够完全控制操作系统,并允许您绕过应用程序沙盒之类的限制。这些特权又允许你更容易地使用代码注入和函数 hook 等技术。

注意,root 是有风险的,在你继续之前,需要弄清三个主要后果。Root 可产生以下负面影响:

  • 取消设备保修(在采取任何行动之前,一定要检查制造商的政策)
  • 设备变砖,即设备无法操作和使用
  • 创建额外的安全风险(因为通常会删除内置的漏洞缓解措施)

你不应该 root 一个存储着私人信息的个人设备,我们建议购买一个便宜的专用测试设备。许多老的设备,比如谷歌的 Nexus 系列,都可以运行最新的 Android 版本,用来测试非常好。

你需要明白,root 你的设备最终是你自己的决定,OWASP 不会对任何损坏负责。如果你不确定,在开始 root 过程之前应该寻求专家的建议。

哪些手机可以 root

实际上,任何 Android 手机都可以 root。Android OS(在内核级是 Linux OS 的演化)的商业版本针对移动世界进行了优化。这些版本中,一些特性被删除或禁用了,例如,非特权用户可以成为 root 用户(拥有提升的特权)。Root 手机意味着允许用户成为 root 用户,例如,添加一个名为 su 的标准 Linux 可执行文件,可用于更改为另一个用户帐户。

要 root 一个移动设备,首先解锁它的启动引导程序,解锁的过程取决于设备制造商。然而,出于实际的原因,root 某些移动设备要比其他设备更受欢迎,尤其是在安全测试方面:Google 开发的,Samsung、LG 和 Motorola 等公司制造的设备最受欢迎,尤其是因为它们被许多开发人员使用。当引导加载程序被解锁时,设备保证不会失效,谷歌提供了许多工具来支持 root。XDA 论坛上发布了精选的所有主流品牌设备的 root 指南。

使用 Magisk 获得 root 权限

Magisk(“Magic Mask”)是一种 root 你的 Android 设备的方法,它的特殊性在于对系统进行修改的方式。当其他的 root 工具改变系统分区上的实际数据时,Magisk 不会(它被称为“systemless“)。这样就可以隐藏对 root 敏感的应用程序(例如银行或游戏)的修改,并且在 root 的情况下允许使用官方的 Android OTA 升级

你可以通过阅读 GitHub 上的官方文档来熟悉 Magisk。如果您没有安装 Magisk,可以在文档中找到安装说明。如果你使用正式的 Android 版本并计划升级,Magisk 在 GitHub 上提供了一个教程

此外,开发人员可以使用 Magisk 的强大功能创建自定义模块,并将它们提交到官方的 Magisk 模块库,提交的模块可以安装在 Magisk 管理器应用程序中。著名的 Xposed 框架的 systemless 版本(适用于高达27的 SDK 版本)是这些可安装模块之一。

Root 检测

Root 检测方法的一个详细列表在“在 Android 上测试反逆向防御”一章中被提出。

对于典型的移动应用程序安全性编译版本,你通常希望测试一个禁用 root 检测的调试版本编译。如果这样的编译无法用于测试,你可以使用各种方法禁用 root 检测,这将在本书后面介绍。

Android 设备上推荐的工具

本指南中使用了许多工具和框架来评估 Android 应用程序的安全性。在下一节中,你将了解更多一些命令和有趣的使用案例,请参阅官方文档了解有关下列工具的安装说明:

  • APK Extractor:不需要 root 即可提取 APK 的应用程序。

  • Frida server:Frida 的服务器,适用于开发人员、逆向工程人员和安全研究人员的动态工具套件。有关更多信息见下面的 Frida 部分。

  • Drozer代理:drozer 的代理,该框架使您可以搜索应用程序和设备中的安全漏洞。有关更多信息见下面的 Drozer 部分。

  • Busybox:Busybox 将多个常见的 Unix 实用程序组合成一个小的可执行文件。通常,所包含的实用程序比其功能齐全的同类程序 GNU 具有更少的选择,但足以在小型或嵌入式系统上提供完整的环境。Busybox 可以安装在 root 设备上,通过从 Google Play 商店可以下载 Busybox 应用程序。你也可以直接从 Busybox 网站下载二进制文件。下载后,运行`adb push busybox /data/local/tmp`传输文件到手机上。在 Busybox FAQ 中可以找到有关如何安装和使用 Busybox 的快速概述。

Xposed

Xposed 是一个“无需更改 APK 即可更改系统和应用程序行为的模块框架。”从技术上讲,它是Zygote的扩展版本,在启动新进程时会导出用于运行 Java 代码的 API。在新实例化的应用程序上下文中运行 Java 代码可以解析、hook 和重写属于该应用程序的 Java 方法。Xposed 使用反射来检测和修改正在运行的应用程序,修改会应用到内存中,并且仅在进程的运行时持久保存,并未修改应用程序二进制文件。

要使用 Xposed,首先需要像 XDA-Developers Xposed framework hub 上解释的那样,在一个 root 设备上安装 Xposed 框架。模块可以通过 Xposed 安装程序安装,通过 GUI 打开和关闭它们。

注意:考虑到 SafetyNet 很容易检测到 Xposed 框架的简单安装,我们建议使用 Magisk 来安装 Xposed。这样,带有 SafetyNet 认证的应用程序会具有更高的使用 Xposed 模块进行测试的机会。

Xposed 已与 Frida 进行了比较。 在 root 设备上运行 Frida server 时,最终你将得到一个同样有效的设置。当你要进行动态检测时,这两个框架都十分有用。当 Frida 使应用程序崩溃时,你可以尝试在 Xposed 上使用类似的方法。接下来,类似于大量的 Frida 脚本,你可以轻松地使用 Xposed 的众多的模块,例如前面讨论的绕过 SSL pinning 模块(JustTrustMe 和 SSLUnpinning)。Xposed 还包括其他模块,例如 Inspeckage,它使你能够进行应用程序更多的深度测试。最重要的是,你还可以创建自己的模块,以修改 Android 应用程序的常用安全机制。

Xposed 也可以通过下面的脚本安装在模拟器上:

#!/bin/sh
echo "Start your emulator with 'emulator -avd NAMEOFX86A8.0 -writable-system -selinux permissive -wipe-data'"
adb root && adb remount
adb install SuperSU\ v2.79.apk #binary can be downloaded from http://www.supersu.com/download
adb push root_avd-master/SuperSU/x86/su /system/xbin/su
adb shell chmod 0755 /system/xbin/su
adb shell setenforce 0
adb shell su --install
adb shell su --daemon&
adb push busybox /data/busybox #binary can be downloaded from https://busybox.net/
# adb shell "mount -o remount,rw /system && mv /data/busybox /system/bin/busybox && chmod 755 /system/bin/busybox && /system/bin/busybox --install /system/bin"
adb shell chmod 755 /data/busybox
adb shell 'sh -c "./data/busybox --install /data"'
adb shell 'sh -c "mkdir /data/xposed"'
adb push xposed8.zip /data/xposed/xposed.zip #can be downloaded from https://dl-xda.xposed.info/framework/
adb shell chmod 0755 /data/xposed
adb shell 'sh -c "./data/unzip /data/xposed/xposed.zip -d /data/xposed/"'
adb shell 'sh -c "cp /data/xposed/xposed/META-INF/com/google/android/*.* /data/xposed/xposed/"'
echo "Now adb shell and do 'su', next: go to ./data/xposed/xposed, make flash-script.sh executable and run it in that directory after running SUperSU"
echo "Next, restart emulator"
echo "Next, adb install XposedInstaller_3.1.5.apk"
echo "Next, run installer and then adb reboot"
echo "Want to use it again? Start your emulator with 'emulator -avd NAMEOFX86A8.0 -writable-system -selinux permissive'"

请注意,在撰写本文时,Xposed 无法在 Android 9(API 等级28)上运行。但是在2019年它以 Edxposed 的名字被非正式地移植,支持Android 8-10(API 等级26至29),你可以在 EdXposed 的 Github 仓库中找到代码和用法示例。

主机上推荐的工具

为了分析Android应用程序,你应该在你的主机上安装以下工具。请在官方文档中查看以下工具或框架的安装说明,我们将在指南中提到它们。

Adb

adb(Android Debug Bridge),随 Android SDK 一起提供,连接本地开发环境和已连接的 Android 设备。你通常会使用它在模拟器或通过 USB 或 Wi-Fi 连接的设备上测试应用程序。使用`adb devices`命令可以列出连接的设备,执行时并加上`-l`参数可以检索有关这些设备的更多细节。

$ adb devices -l
List of devices attached
090c285c0b97f748 device usb:1-1 product:razor model:Nexus_7 device:flo
emulator-5554    device product:sdk_google_phone_x86 model:Android_SDK_built_for_x86 device:generic_x86 transport_id:1

adb 提供了其他有用的命令,比如`adb shell`在目标设备上启动交互式的`shell`,以及`adb forward`将特定主机端口上的流量转发到连接设备上的不同端口。

$ adb forward tcp:<host port> tcp:<device port>
$ adb -s emulator-5554 shell
root@generic_x86:/ # ls
acct
cache
charger
config
...

请注意,如果连接了多个设备,则必须使用`-s`参数定义目标设备的序列号(如上一代码片段所示)。

Angr

Angr 是一个用于分析二进制文件的 Python 框架。它对于静态和动态符号分析都是有用的。换句话说:给定一个二进制和一个请求状态,Angr将尝试到达那个状态,使用形式化的方法(一种用于静态代码分析的技术)来找到一条路径,以及强制执行。使用 angr 来获得请求的状态通常比手动调试和搜索通往所需状态的路径要快得多。Angr 使用VEX 中间语言进行操作,并带有 ELF/ARM 二进制文件加载程序,因此非常适合处理原生代码,例如原生 Android 二进制文件。

Angr 允许使用大量插件来进行反汇编、程序检测、符号执行、控制流分析、数据依赖分析、反编译等等。

自从版本8,Angr 是基于 Python3 并可以使用 pip 安装在*nix操作系统、macOS 和 Windows 上:

$ pip install angr

angr 的一些依赖项包含 Python 模块 Z3 和 PyVEX 的派生版本,这将覆盖原始版本。如果你将这些模块用于其他用途,则应该使用 Virtualenv 创建一个专用的虚拟环境。另外,你也可以使用提供的 docker 容器。有关更多细节,请参阅安装指南

在 Angr 的 Gitbooks 页面上有全面的文档,包括安装指南、教程和使用示例,还提供了完整的 API 参考

你可以通过 Python REPL(如 iPython)使用angr,也可以编写方法脚本。尽管 angr 的学习曲线有些陡峭,但是尽管 angr 的学习曲线有些陡峭,但是当你想要通过强制的方式得到一个可执行文件的给定状态,我们还是建议你使用它。请见“逆向工程和篡改”一章的“符号执行”部分,可以作为一个很好的例子来说明它是如何工作的。

Apktool

Apktool 用于解压 Android 应用程序包(APK)。简单地用标准 unzip工具解压 APK 会留下一些不可读的文件。AndroidManifest.xml 被加密成二进制 XML 格式,文本编辑器无法读取。此外,应用程序资源仍然被打包到一个归档文件中。

当使用默认命令行参数运行时,apktool 会自动将 Android 清单文件解密为为基于文本的 XML 格式并提取文件资源(它还会将 .DEX 文件反汇编为 smali 代码——这个特性我们将在本书后面介绍)。

$ apktool d base.apk
I: Using Apktool 2.1.0 on base.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /Users/sven/Library/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
$ cd base
$ ls -alh
total 32
drwxr-xr-x    9 sven  staff   306B Dec  5 16:29 .
drwxr-xr-x    5 sven  staff   170B Dec  5 16:29 ..
-rw-r--r--    1 sven  staff    10K Dec  5 16:29 AndroidManifest.xml
-rw-r--r--    1 sven  staff   401B Dec  5 16:29 apktool.yml
drwxr-xr-x    6 sven  staff   204B Dec  5 16:29 assets
drwxr-xr-x    3 sven  staff   102B Dec  5 16:29 lib
drwxr-xr-x    4 sven  staff   136B Dec  5 16:29 original
drwxr-xr-x  131 sven  staff   4.3K Dec  5 16:29 res
drwxr-xr-x    9 sven  staff   306B Dec  5 16:29 smali

解压缩的文件是:

  • AndroidManifest.xml:解密的 Android Manifest 文件,可以在文本编辑器中打开和编辑该文件。
  • apktool.yml:包含有关 apktool 输出信息的文件
  • original:包含 MANIFEST.MF 文件的文件夹,MANIFEST.MF 文件包含有关 JAR 文件中存在的文件的信息
  • res:包含应用程序资源的目录
  • smali:包含反汇编的 Dalvik 字节码的目录

还可以使用 apktool 将已解密的资源重新打包回二进制 APK/JAR。见本章后面的“探索应用程序包”一节和”Android 上的篡改和逆向工程“一章中“重打包”一节,了解更多信息和实例。

Apkx

`Apkx`是流行的免费 DEX 转换器和 Java 反编译器的 Python 包装器。它自动提取、转换和反编译 apk,安装方式如下:

$ git clone https://github.com/b-mueller/apkx
$ cd apkx
$ sudo ./install.sh

这会将`apkx`复制到`/usr/local/bin`。请参阅“逆向工程和篡改”一章的“反编译Java代码”一节,了解更多有关用法的信息。

Burp Suite

Burp Suite 是一个用于移动和 web 应用程序安全性测试的集成平台。它的工具可以无缝地协同工作,以支持整个测试过程,从最初的攻击面映射和分析到发现和利用安全漏洞。Burp Proxy 作为 Burp Suite 的 web 代理服务器,它被定位为浏览器和 web 服务器之间的中间人。Burp Suite 允许您拦截、检查和修改传入和传出的原始 HTTP 流量。

设置 Burp 代理您的流量非常简单。我们假设您有一个 android 设备和工作站连接到 Wi-Fi 网络,该网络允许客户端到客户端通信。

PortSwigger 提供了关于如何设置Android 设备来使用 Burp 如何将 Burp 的 CA 证书安装到 Android 设备的教程

Drozer

Drozer 是一个 Android 安全评估框架,如果第三方应用程序与其他应用程序的 IPC 端点和底层操作系统进行了交互,你可以搜索应用程序和设备中的安全漏洞。

使用 drozer 的优势在于它能够自动执行多个任务,并且可以通过模块进行扩展。这些模块非常有帮助并且涵盖了不同的类别,其中包括扫描器类别,该类别使你可以使用简单的命令扫描已知的缺陷,例如模块`scanner.provider.injection`可以检测系统中安装的所有应用程序的内容提供者中的`SQL`注入 。 如果不使用`drozer`,则简单的任务(例如列出应用程序的权限)需要几个步骤,包括反编译 APK 和手动分析结果。

安装 Drozer

你可以参考 drozer GitHub 页面(对于 Linux 和 Windows,macOS 请参考这篇博客文章)和 drozer 的网站了解必备条件和安装说明。

drozer 在 Unix、Linux 和 Windows 上的安装说明在 drozer Github 页面中有解释。对于macOS,这篇博客演示了所有的安装说明。

使用 Drozer

在开始使用 drozer 之前,还需要在 Android设备上运行 drozer 代理。从 GitHub 发布页面下载最新的 drozer 代理,并用 `adb install drozer.apk` 安装它。

一旦安装完成,你可以通过运行 `adb forward tcp:31415 tcp:31415` 和 `drozer console connect` 启动一个会话到模拟器或 USB 连接的设备。这被称为直接模式,你可以在用户指南的“开始一个会话”一节中看到完整的说明。另一种选择是在基础设施模式下运行Drozer,在这种模式下,你运行一个 Drozer 服务器,它可以处理多个控制台和代理,并在它们之间路由会话。你可以在用户指南的“基础设施模式”一节中找到如何在此模式下设置 drozer 的详细信息。

现在您可以开始分析应用程序了。一个好的开端是列举一个应用程序的攻击面,可以通过以下命令很容易地完成:

$ dz> run app.package.attacksurface <package>

同样,如果没有 drozer,这将需要几个步骤。`app.package.attacksurface`模块列出了活动,广播接收者,内容提供者和导出的服务,因此它们是公共的并且可以通过其他应用程序进行访问。 一旦确定了攻击面,就可以通过 drozer 与 IPC 端点进行交互,而无需编写单独的独立应用程序,某些任务会需要它,比如与内容提供者通信。

例如,如果应用程序的导出活动泄漏了敏感信息,我们可以使用 Drozer 模块`app.activity.start`来调用它:

$ dz> run app.activity.start --component <package> <component name>

前面的命令将启动活动,希望泄漏一些敏感信息。Drozer 有针对每一种 IPC 机制的模块,如果你想尝试带有故意易受攻击的应用程序的模块,请下载 InsecureBankv2,该应用程序演示了与 IPC 端点相关的常见问题。请密切注意扫描器类别中的模块,因为它们对于自动检测系统包中的漏洞非常有用,特别是如果你使用的是手机公司提供的 ROM。在过去甚至使用 drozer 识别出过 Google 系统软件包中的 SQL 注入漏洞。

其他的 Drozer 命令

这里有一个非详尽的命令列表,你可以用来开始探索 Android:

# List all the installed packages
$ dz> run app.package.list
 
# Find the package name of a specific app
$ dz> run app.package.list –f (string to be searched)
 
# See basic information
$ dz> run app.package.info –a (package name)
 
# Identify the exported application components
$ dz> run app.package.attacksurface (package name)
 
# Identify the list of exported Activities
$ dz> run app.activity.info -a (package name)
 
# Launch the exported Activities
$ dz> run app.activity.start --component (package name) (component name)
 
# Identify the list of exported Broadcast receivers
$ dz> run app.broadcast.info -a (package name)
 
# Send a message to a Broadcast receiver
$ dz> run app.broadcast.send --action (broadcast receiver name) -- extra (number of arguments)
 
# Detect SQL injections in content providers
$ dz> run scanner.provider.injection -a (package name)

其他的 Drozer 资源

你可能会发现其他有用信息的资源有:

Frida

Frida 是一个免费和开源的动态代码工具包,它允许你在本地应用程序中执行 JavaScript 脚本,在通用测试指南的“篡改与逆向工程”一章中已经介绍了。

Frida 通过 Java API 支持与 Android Java 运行时的交互。你能够在hook 和调用进程的 Java 和原生函数,以及它的原生库。你的 JavaScript 脚本对内存有完全的访问权限,例如读取或写入任何结构化数据。

下面是 Frida api 提供的一些任务,在 Android 上是相关的或独家的:

  • 实例化 Java 对象并调用静态和非静态类方法(Java API )。

  • 替换Java方法实现(Java API )。

  • 通过扫描 Java 堆枚举特定类的活动实例(Java API )。

  • 扫描进程内存中出现的字符串(Memory API )。

  • 拦截原生函数调用以在函数入口和出口运行自己的代码(Interceptor API)。

记住,在Android上,你还可以从安装 Frida 时提供的内置工具中获益,包括 Frida CLI(frida)、frida-psfrida-ls-devices 和 frida-trace

Frida 经常被拿来和 Xposed 进行比较,但是这种比较并不公平,因为这两个框架的设计目标是不同的。作为一个应用程序安全测试人员,了解这一点很重要,这样你就可以知道在什么情况下使用哪个框架:

  • Frida 是独立的,你所需要做的就是从目标 Android 设备中的已知位置运行 frida-server 二进制文件(请参阅下面的“安装Frida”)。这意味着,与 Xposed 相比,它没有深入安装在目标 OS 中。

  • 逆向一个应用程序是一个反复的过程。由于上一点的影响,在测试时,你获得了更短的反馈循环,因不需要(软)重启来应用钩子或简单地更新钩子。同时在实现更持久的钩子时,你可能更喜欢使用 Xposed。

  • 你可以在进程运行期间的任何时候动态注入和更新 Frida JavaScript 代码(类似于 iOS 上的 Cycript)。这样,可以通过让 Frida 来 spwan 你的应用程序执行所谓的早期检测,或者你可能更喜欢附加到一个进入特定状态的正在运行的应用程序。

  • Frida 能够处理 Java 以及原生代码(JNI),允许你修改它们。不幸的是,这正是 Xposed 的局限性,缺乏原生代码支持。

值得注意的是,到 2019 年初,Xposed 还不能在 Android 9(API 等级28)上运行。

安装 Frida

要在本地安装 Frida,只需运行:

$ pip install frida-tools

参考安装页面了解更多细节。

下一步就是在你的 Android 设备上设置 Frida:

  • 如果您的设备没有 root,你也可以使用Frida,请参考“逆向工程和篡改”章节的“非 root 设备的动态分析”。
  • 如果你有一个已 root 的设备,只需遵循官方说明或下面的提示。

除非另有说明,我们假定在这里是 root 设备。从 Frida 发布页下载 frida-serve 二进制文件。确保为你的 Android 设备或模拟器的架构下载了正确的 frida-server 二进制文件:x86,x86_64,arm 或 arm64。确保服务器版本(至少是主版本号)与本地 Frida 安装版本匹配。PyPI通常安装最新版本的 Frida。如果不确定安装的版本,可以使用 Frida 命令行工具检查:

$ frida --version

或者你可以运行以下命令来自动检测 Frida 版本并下载正确的 frida-server 二进制文件:

$ wget https://github.com/frida/frida/releases/download/$(frida --version)/frida-server-$(frida --version)-android-arm.xz

复制 frida-server 到设备上并运行它:

$ adb push frida-server /data/local/tmp/
$ adb shell "chmod 755 /data/local/tmp/frida-server"
$ adb shell "su -c /data/local/tmp/frida-server &"

在 Android 上使用 Frida

随着 frida-server 的运行,你现在应该可以使用以下命令获得正在运行的进程的列表(用 -U 选项指示 Frida 使用一个已连接的 USB 设备或模拟器):

$ frida-ps -U
  PID  Name
-----  --------------------------------------------------------------
  276  adbd
  956  android.process.media
  198  bridgemgrd
30692  com.android.chrome
30774  com.android.chrome:privileged_process0
30747  com.android.chrome:sandboxed
30834  com.android.chrome:sandboxed
 3059  com.android.nfc
 1526  com.android.phone
17104  com.android.settings
 1302  com.android.systemui
(...)

或使用 -Uai 参数组合限制列表,以获取连接的 USB 设备(-U)上当前安装的(-i)所有应用程序(-a):

$ frida-ps -Uai
  PID  Name                                      Identifier
-----  ----------------------------------------  ------------------------------
  766  Android System                            android
30692  Chrome                                    com.android.chrome
 3520  Contacts Storage                          com.android.providers.contacts
    -  Uncrackable1                              sg.vantagepoint.uncrackable1
    -  drozer Agent                              com.mwr.dz

这将显示所有应用程序的名称和标识符,如果目前正在运行,还将显示它们的 PID。在列表中搜索你的应用程序,并注意 PID 或其名称/标识符,从现在起你将使用其中一个来引用你的应用程序。建议使用这些应用的标识符,因为在每次运行应用程序时 PID 都会改变。例如,以 com.android.chrome 为例,你现在可以在所有 Frida 工具上使用此字符串,如在 Frida CLI,frida-trace 或 Python 脚本上。

使用 frida-trace 跟踪原生库

要跟踪特定的(底层)库调用,你可以使用 frida-trace 命令行工具:

$ frida-trace -U com.android.chrome -i "open"

这会在 __handlers __ / libc.so/open.js 中生成一些 JavaScript 代码,Frida 将其注入到进程中。该脚本将跟踪对 libc.so 中的 open 函数的所有调用。你可以使用 Frida JavaScript API 根据需要修改生成的脚本。

不幸的是,尚不支持跟踪 Java 类的高级方法(但将来可能会)。

Frida CLI 和 Java API

使用 Frida CLI 工具(Frida)与 Frida 交互工作。它挂接到一个进程,并为你提供 Frida API 的命令行界面。

$ frida -U com.android.chrome

通过 -l 选项,你可以使用 Frida CLI 加载脚本,例如,加载 myscript.js

$ frida -U -l myscript.js com.android.chrome

Frida 还提供了一个 Java API,这对处理 Android 应用程序特别有帮助,它允许你直接使用 Java 类和对象。下面是一个脚本,用于重写 Activity 类的 onResume 函数:

Java.perform(function () {
    var Activity = Java.use("android.app.Activity");
    Activity.onResume.implementation = function () {
        console.log("[*] onResume() got called!");
        this.onResume();
    };
});

上面的脚本调用 Java.perform 以确保您的代码在 Java VM 的环境中执行。它通过 Java.use 实例化一个 android.app.Activity 类的包装器,并重写 onResume 函数。新的 onResume 函数实现打印通知到控制台,并在活动每次处于恢复状态时通过调用 this.onResume 来调用原来的 onResume 方法。

Frida 还允许你搜索并使用堆上的实例化对象。以下脚本搜索 android.view.View 对象的实例,并调用其 toString 方法,结果打印到控制台:

setImmediate(function() {
    console.log("[*] Starting script");
    Java.perform(function () {
        Java.choose("android.view.View", {
             "onMatch":function(instance){
                  console.log("[*] Instance found: " + instance.toString());
             },
             "onComplete":function() {
                  console.log("[*] Finished heap search")
             }
        });
    });
});

输出会像这样:

[*] Starting script
[*] Instance found: android.view.View{7ccea78 G.ED..... ......ID 0,0-0,0 #7f0c01fc app:id/action_bar_black_background}
[*] Instance found: android.view.View{2809551 V.ED..... ........ 0,1731-0,1731 #7f0c01ff app:id/menu_anchor_stub}
[*] Instance found: android.view.View{be471b6 G.ED..... ......I. 0,0-0,0 #7f0c01f5 app:id/location_bar_verbose_status_separator}
[*] Instance found: android.view.View{3ae0eb7 V.ED..... ........ 0,0-1080,63 #102002f android:id/statusBarBackground}
[*] Finished heap search

你还可以使用 Java 的反射功能。要列出 android.view.View 类的公共方法,你可以在 Frida 中为此类创建一个包装器,并从该包装器的 class 属性调用 getMethods

Java.perform(function () {
    var view = Java.use("android.view.View");
    var methods = view.class.getMethods();
    for(var i = 0; i < methods.length; i++) {
        console.log(methods[i].toString());
    }
});

这将在终端打印很长的方法列表:

public boolean android.view.View.canResolveLayoutDirection()
public boolean android.view.View.canResolveTextAlignment()
public boolean android.view.View.canResolveTextDirection()
public boolean android.view.View.canScrollHorizontally(int)
public boolean android.view.View.canScrollVertically(int)
public final void android.view.View.cancelDragAndDrop()
public void android.view.View.cancelLongPress()
public final void android.view.View.cancelPendingInputEvents()
...

Frida Binding

为了扩展脚本编写体验,Frida 提供了与Python,C,NodeJS 和 Swift 等编程语言的绑定。

以 Python 为例,首先要注意的是不需要更多的安装步骤。使用 import frida 启动 Python 脚本,就可以开始了。请参阅下面的脚本,该脚本仅运行先前的 JavaScript 代码段:

# frida_python.py
import frida
 
session = frida.get_usb_device().attach('com.android.chrome')
 
source = """
Java.perform(function () {
    var view = Java.use("android.view.View");
    var methods = view.class.getMethods();
    for(var i = 0; i < methods.length; i++) {
        console.log(methods[i].toString());
    }
});
"""
 
script = session.create_script(source)
script.load()
 
session.detach()

在这种情况下,运行 Python 脚本(python3 frida_python.py)与上一个示例具有相同的结果:它会打印 android.view.View 类的所有方法到终端。但是,你可能希望使用 Python 中的数据。使用 send 代替 console.log 会将数据以 JSON 格式从 JavaScript 发送到 Python 上。请阅读以下示例中的注释:

# python3 frida_python_send.py
import frida
 
session = frida.get_usb_device().attach('com.android.chrome')
 
# 1. we want to store method names inside a list
android_view_methods = []
 
source = """
Java.perform(function () {
    var view = Java.use("android.view.View");
    var methods = view.class.getMethods();
    for(var i = 0; i < methods.length; i++) {
        send(methods[i].toString());
    }
});
"""
 
script = session.create_script(source)
 
# 2. this is a callback function, only method names containing "Text" will be appended to the list
def on_message(message, data):
    if "Text" in message['payload']:
        android_view_methods.append(message['payload'])
 
# 3. we tell the script to run our callback each time a message is received
script.on('message', on_message)
 
script.load()
 
# 4. we do something with the collected data, in this case we just print it
for method in android_view_methods:
    print(method)
 
session.detach()

这有效地过滤了方法,只打印包含字符串“Text”的方法:

$ python3 frida_python_send.py
public boolean android.view.View.canResolveTextAlignment()
public boolean android.view.View.canResolveTextDirection()
public void android.view.View.setTextAlignment(int)
public void android.view.View.setTextDirection(int)
public void android.view.View.setTooltipText(java.lang.CharSequence)
...

最后,由你决定在什么地方处理数据,有时候,用 JavaScript 来做会比较方便,而在其他情况下,Python 将是最好的选择。当然,你也可以使用 script.post 将消息从 Python 发送到 JavaScript 上。有关发送接收消息的更多信息,请参考 Frida 文档。

House

House 是一个用于 Android 应用运行时的移动应用分析工具包,由 NCC 小组开发和维护,用 Python 编写。

它利用 root 设备上运行的 Frida server 或重打包到 Android 应用程序中的 Frida gadget。House 的目的是通过方便的 web GUI 提供一种简单的方法来原型化 Frida 脚本。

House 的安装说明和操作指南可以在 Readme of the Github repo 中找到。

Magisk

Magisk(“Magic Mask”)是一种 root 你的 Android 设备的方法,它的特殊性在于对系统进行修改的方式。当其他的 root 工具改变系统分区上的实际数据时,Magisk 不会(它被称为“systemless“)。这样就可以隐藏对 root 敏感的应用程序(例如银行或游戏)的修改,并且在 root 的情况下允许使用官方的 Android OTA 升级

你可以通过阅读 GitHub 上的官方文档来熟悉 Magisk。如果您没有安装 Magisk,可以在文档中找到安装说明。如果你使用正式的 Android 版本并计划升级,Magisk 在 GitHub 上提供了一个教程

MobSF

MobSF 是一个自动化的、一体化的移动应用程序审计框架,它也支持 Android APK 文件。启动 MobSF 最简单的方法是通过 Docker。

$ docker pull opensecurity/mobile-security-framework-mobsf
$ docker run -it -p 8000:8000 opensecurity/mobile-security-framework-mobsf:latest

或通过运行以下命令在主机上本地安装并启动它:

# Setup
git clone https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
cd Mobile-Security-Framework-MobSF
./setup.sh # For Linux and Mac
setup.bat # For Windows
 
# Installation process
./run.sh # For Linux and Mac
run.bat # For Windows

一旦你启动并运行了 MobSF,你就可以通过在浏览器中打开 http://127.0.0.1:80。只要将你想要分析的APK拖放到上传区域,MobSF就会开始工作。

在 MobSF 完成了它的分析之后,你将收到一页关于所有被执行的测试的概述。页面被分割成多个部分,提供了一些关于应用程序攻击表面的初步提示。

显示如下内容:

  • 关于应用程序及其二进制文件的基本信息。

  • 一些选项:
    查看 AndroidManifest.xml 文件。
    查看应用程序的 IPC 组件。

  • 签名者证书。

  • 应用程序的权限。
  • 显示已知缺陷的安全分析,例如是否启用了应用程序备份。
  • 应用程序二进制文件使用的库列表和已解压缩的APK中所有文件的列表。
  • 恶意软件分析,检查恶意网址。

更多细节请参考 MobSF 文档

Objection

Objection 是一个“由 Frida 提供的运行时移动探索工具包”,它的主要目标是允许通过直观的界面在未 root 设备上进行安全性测试。

通过为你提供通过将 Frida gadget 注入应用程序重新打包的工具,Objection 实现了这一目标。通过这种方式,你可以将重新打包的应用程序配置到未 root 设备上让不会与应用程序交互,如前一节所述。

但是,Objection 还提供了一个 REPL,允许你与应用程序交互,使你能够执行应用程序可以执行的任何操作。在项目的主页上可以找到完整的 Objection 功能列表,这里有一些有趣的:

  • 重新打包应用程序来包含 Frida gadget
  • 为常用方法禁用 SSL pinning
  • 访问应用程序存储以下载或上载文件
  • 执行自定义 Frida 脚本
  • 列出活动、服务和广播接收器
  • 开始活动

在未 root 设备上执行高级动态分析的能力是使 Objection 非常有用的特性之一。一个应用程序可能包含高级 RASP 控制,可以检测你的 root 方法,注入 frida-gadget 可能是绕过这些控制的最简单的方法。此外,包含的 Frida 脚本使快速分析应用程序或绕过基本的安全控制变得非常容易。

最后,如果你确实可以访问一个 root 设备,Objection 可以直接连接到运行中的 Frida server 来提供所有功能,而不需要重新打包应用程序。

安装 Objection

正如 Objection's Wiki 中描述的那样,可以通过 pip 直接安装。

$ pip3 install objection

如果你的设备已经越狱了,你现在就可以和设备上运行的任何应用程序进行交互了,你可以跳到下面的“使用 Objection”部分。

但是,如果希望在未 root 设备上进行测试,首先需要在应用程序中包含 Frida gadget。 Objection Wiki 详细描述了需要的步骤,但是在做了正确的准备之后,你将能够通过调用 Objection 命令来给 APK 打补丁:

$ objection patchapk --source app-release.apk

然后,需要使用 adb 安装修改过的应用程序,正如“基本测试操作——安装应用程序”中解释的那样。

使用 Objection

开始使用 Objection 依赖于你是否给 APK 打了补丁,或是你是否使用运行了 Frida-server 的 root 设备。运行一个打过补丁的 APK,Objection 将自动找到附加的设备和搜索一个正在监听的 Frida gadget。但是,在使用 frida-server 时,你需要显式地告诉 frida-server 要分析哪个应用程序。

# Connecting to a patched APK
objection explore
 
# Find the correct name using frida-ps
$ frida-ps -Ua | grep -i telegram
30268  Telegram                               org.telegram.messenger
 
# Connecting to the Telegram app through Frida-server
$ objection --gadget="org.telegram.messenger" explore

一旦进入了 Objection REPL,你就可以执行任何可用的命令,以下是一些最有用的方法的概述:

# Show the different storage locations belonging to the app
$ env
 
# Disable popular ssl pinning methods
$ android sslpinning disable
 
# List items in the keystore
$ android keystore list
 
# Try to circumvent root detection
$ android root disable

更多关于使用 Objection REPL 的信息可以在 Objection Wiki 上找到。

radare2

radare2(r2)是一个流行的开源逆向工程框架,用于反汇编、调试、打补丁和分析二进制文件,该框架可编写脚本,支持多种架构和文件格式,包括 Android 和 iOS 应用程序。对 Android 支持 Dalvik DEX(odex, multidex),ELF(可执行文件,.so, ART)和 Java(JNI 和 Java 类)。它还包含了几个有用的脚本,可以在移动应用程序分析期间帮助您,它提供了底层的反汇编和安全的静态分析,在传统工具失败时非常有用。

radare2 实现了一个丰富的命令行界面(CLI),你可以在上面执行上述任务。但是,如果你不是很习惯使用 CLI 进行逆向工程,你可以考虑使用 Web UI(通过 -H 参数)或者更方便的 Qt 和 C++ GUI 版本 Cutter。请记住关于 CLI,更具体地说是它的可视化模式和脚本功能(r2pipe),是 radare2 强大功能的核心,绝对值得学习如何使用它。

安装 radare2

请参考 radare2 的官方安装说明。我们强烈建议始终从 GitHub 版本安装 radare2,而不是通过 APT 等常见的包管理器。Radare2正处于非常活跃的开发阶段,这意味着第三方存储库经常会过时。

使用 radare2

radare2 框架包含一组小型实用程序,可以在 r2 shell 中使用,也可以作为独立的 CLI 工具使用。这些工具包括 rabin2rasm2rahash2radiff2rafind2ragg2rarun2rax2,当然还有 r2,这是主要的一个。

例如,你可以使用 rafind2 直接从一个加密的Android 清单文件(AndroidManifest.xml)中读取字符串:

# Permissions
$ rafind2 -ZS permission AndroidManifest.xml
# Activities
$ rafind2 -ZS activity AndroidManifest.xml
# Content providers
$ rafind2 -ZS provider AndroidManifest.xml
# Services
$ rafind2 -ZS service AndroidManifest.xml
# Receivers
$ rafind2 -ZS receiver AndroidManifest.xml

或者使用 rabin2 来获得关于二进制文件的信息:

$ rabin2 -I UnCrackable-Level1/classes.dex
arch     dalvik
baddr    0x0
binsz    5528
bintype  class
bits     32
canary   false
retguard false
class    035
crypto   false
endian   little
havecode true
laddr    0x0
lang     dalvik
linenum  false
lsyms    false
machine  Dalvik VM
maxopsz  16
minopsz  1
nx       false
os       linux
pcalign  0
pic      false
relocs   false
sanitiz  false
static   true
stripped false
subsys   java
va       true
sha1  12-5508c  b7fafe72cb521450c4470043caa332da61d1bec7
adler32  12-5528c  00000000

输入 rabin2 -h 查看所有的选项:

$ rabin2 -h
Usage: rabin2 [-AcdeEghHiIjlLMqrRsSUvVxzZ] [-@ at] [-a arch] [-b bits] [-B addr]
              [-C F:C:D] [-f str] [-m addr] [-n str] [-N m:M] [-P[-P] pdb]
              [-o str] [-O str] [-k query] [-D lang symname] file
 -@ [addr]       show section, symbol or import at addr
 -A              list sub-binaries and their arch-bits pairs
 -a [arch]       set arch (x86, arm, .. or <arch>_<bits>)
 -b [bits]       set bits (32, 64 ...)
 -B [addr]       override base address (pie bins)
 -c              list classes
 -cc             list classes in header format
 -H              header fields
 -i              imports (symbols imported from libraries)
 -I              binary info
 -j              output in json
 ...

使用 r2 实用程序访问 r2 shell,你可以像加载任何其他二进制一样加载 DEX 二进制文件:

$ r2 classes.dex

输入 r2 -h 以查看所有可用选项。一个非常常用的参数是 -A,它在加载目标二进制文件后触发分析。但是,应该对小的二进制文件谨慎使用,因为它非常耗费时间和资源。你可以在“Android上的篡改和逆向工程”一章中了解更多。

一旦进入了 r2 shell,你还可以访问其他 radare2 实用程序提供的函数。例如,运行 i 将打印二进制文件的信息,就像 rabin2 -I 所做的那样。

要打印所有字符串,在 r2 shell 中使用 rabin2 -Z 或命令 iz(或更简单的 izq)。

[0x000009c8]> izq
0xc50 39 39 /dev/com.koushikdutta.superuser.daemon/
0xc79 25 25 /system/app/Superuser.apk
...
0xd23 44 44 5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=
0xd51 32 32 8d127684cbc37c17616d806cf50473cc
0xd76 6 6 <init>
0xd83 10 10 AES error:
0xd8f 20 20 AES/ECB/PKCS7Padding
0xda5 18 18 App is debuggable!
0xdc0 9 9 CodeCheck
0x11ac 7 7 Nope...
0x11bf 14 14 Root detected!

在大多数情况下,你可以在命令中附加特殊选项,例如 q 可以使命令不太冗长,或者 j 可以用 JSON 格式提供输出(使用 〜{} 表示JSON字符串)。

[0x000009c8]> izj~{}
[
  {
    "vaddr": 3152,
    "paddr": 3152,
    "ordinal": 1,
    "size": 39,
    "length": 39,
    "section": "file",
    "type": "ascii",
    "string": "L2Rldi9jb20ua291c2hpa2R1dHRhLnN1cGVydXNlci5kYWVtb24v"
  },
  {
    "vaddr": 3193,
    "paddr": 3193,
    "ordinal": 2,
    "size": 25,
    "length": 25,
    "section": "file",
    "type": "ascii",
    "string": "L3N5c3RlbS9hcHAvU3VwZXJ1c2VyLmFwaw=="
  },

你可以使用 r2 命令 ic 打印类名及其方法。(类信息)

[0x000009c8]> ic
...
0x0000073c [0x00000958 - 0x00000abc]    356 class 5 Lsg/vantagepoint/uncrackable1/MainActivity
:: Landroid/app/Activity;
0x00000958 method 0 pC   Lsg/vantagepoint/uncrackable1/MainActivity.method.<init>()V
0x00000970 method 1 P    Lsg/vantagepoint/uncrackable1/MainActivity.method.a(Ljava/lang/String;)V
0x000009c8 method 2 r    Lsg/vantagepoint/uncrackable1/MainActivity.method.onCreate (Landroid/os/Bundle;)V
0x00000a38 method 3 p    Lsg/vantagepoint/uncrackable1/MainActivity.method.verify (Landroid/view/View;)V
0x0000075c [0x00000acc - 0x00000bb2]    230 class 6 Lsg/vantagepoint/uncrackable1/a :: Ljava/lang/Object;
0x00000acc method 0 sp   Lsg/vantagepoint/uncrackable1/a.method.a(Ljava/lang/String;)Z
0x00000b5c method 1 sp   Lsg/vantagepoint/uncrackable1/a.method.b(Ljava/lang/String;)[B

你可以使用 r2 命令 ii 打印导入的方法。(导入信息)

[0x000009c8]> ii
[Imports]
Num  Vaddr       Bind      Type Name
...
  29 0x000005cc    NONE    FUNC Ljava/lang/StringBuilder.method.append(Ljava/lang/String;) Ljava/lang/StringBuilder;
  30 0x000005d4    NONE    FUNC Ljava/lang/StringBuilder.method.toString()Ljava/lang/String;
  31 0x000005dc    NONE    FUNC Ljava/lang/System.method.exit(I)V
  32 0x000005e4    NONE    FUNC Ljava/lang/System.method.getenv(Ljava/lang/String;)Ljava/lang/String;
  33 0x000005ec    NONE    FUNC Ljavax/crypto/Cipher.method.doFinal([B)[B
  34 0x000005f4    NONE    FUNC Ljavax/crypto/Cipher.method.getInstance(Ljava/lang/String;) Ljavax/crypto/Cipher;
  35 0x000005fc    NONE    FUNC Ljavax/crypto/Cipher.method.init(ILjava/security/Key;)V
  36 0x00000604    NONE    FUNC Ljavax/crypto/spec/SecretKeySpec.method.<init>([BLjava/lang/String;)V

检查二进制文件时,一种常见的方法是搜索、导航到它并使之可视化,以便解释代码。使用 radare2 查找内容的方法之一是使用特定命令过滤输出,即使用 ~ 加上关键字(~+ 表示大小写不敏感)对它们进行 grep。例如,我们可能知道应用程序正在验证一些东西,我们可以检查 radare2 所有的 flag,看看我们在哪里找到与“验证”相关的东西。

当加载一个文件时,radare2 会标记它能够找到的所有东西,这些标记的名称或引用称为 flag,你可以通过命令 f 来访问它们。

在这个案例中,我们使用关键字”verify“来 grep flag。

[0x000009c8]> f~+verify
0x00000a38 132 sym.Lsg_vantagepoint_uncrackable1_MainActivity.method. \
verify_Landroid_view_View__V
0x00000a38 132 method.public.Lsg_vantagepoint_uncrackable1_MainActivity. \
Lsg_vantagepoint_uncrackable1
        _MainActivity.method.verify_Landroid_view_View__V
0x00001400 6 str.verify

看起来我们在 0x00000a38 处找到了一个方法(标记了两次),在 0x00001400 处找到了一个字符串。让我们通过使用它的标志来导航(寻找)到那个方法:

[0x000009c8]> s sym.Lsg_vantagepoint_uncrackable1_MainActivity.method. \
verify_Landroid_view_View__V

当然,您还可以使用 r2 的反汇编功能,并用 pd 命令(或者 pdf,如果你知道你已经位于一个函数中)打印反汇编结果。

[0x00000a38]> pd

r2 命令通常接受选项(见 pd?),例如,你可以通过在命令 pd N 后面附加数字(“N”)来限制显示的操作码。

你可能希望通过输入 V 进入所谓的可视模式,而不是仅仅将反汇编输出到控制台。

默认情况下,你将看到十六进制视图。通过键入 p,你可以切换到不同的视图,如反汇编视图:

Radare2 提供了一种图形模式,它对于跟踪代码流非常有用,你可以在可视模式下输入V

这只是一些 radare2 命令的一部分,用来开始从 Android 二进制文件中获取一些基本信息。Radare2 非常强大,在 Radare2 命令文档中可以找到许多命令。Radare2 将在整个指南中用于不同的目的,如逆向代码、调试或执行二进制分析。我们还将结合使用其他框架,特别是Frida(更多信息,请参阅 r2frida 一节)。

有关 radare2 在Android上的详细使用,特别是在分析原生库时,请参考“Android 上的篡改和逆向工程”一章。你可能还想读一下 radare2 的官方书籍

r2frida

r2frida 是一个允许 radare2 连接 Frida 的项目,有效地将 radare2 强大的逆向工程能力与 Frida 的动态分析工具包结合在一起。R2frida 允许你:

  • 通过 USB 或 TCP 将 radare2 附加到任何本地进程或远程 frida-server。
  • 从目标进程读/写内存。
  • 将映射、符号、导入、类和方法等Frida信息加载到 radare2 中。
  • 从 Frida 调用 r2 命令,因为它将 r2pipe 接口公开到了 Frida Javascript API 中。

安装 r2frida

请参考 r2frida 的官方安装说明

使用 r2frida

随着 frida-server 运行,你现在应该能够使用pid、spawn path、主机和端口或设备 id 连接到它。例如,附加到 PID 1234:

$ r2 frida://1234

有关如何连接 frida-server 的更多示例,请参阅 r2frida 的 README 页面中的使用部分

连接之后,你应该会看到带有设备id的r2提示符。r2frida 命令必须以 \ 或 =!开始。例如,你可以使用命令 \i 检索目标信息:

[0x00000000]> \i
arch                x86
bits                64
os                  linux
pid                 2218
uid                 1000
objc                false
runtime             V8
java                false
cylang              false
pageSize            4096
pointerSize         8
codeSigningPolicy   optional
isDebuggerAttached  false

要在内存中搜索特定关键字,你可以使用搜索命令 \/

[0x00000000]> \/ unacceptable
Searching 12 bytes: 75 6e 61 63 63 65 70 74 61 62 6c 65
Searching 12 bytes in [0x0000561f05ebf000-0x0000561f05eca000]
...
Searching 12 bytes in [0xffffffffff600000-0xffffffffff601000]
hits: 23
0x561f072d89ee hit12_0 unacceptable policyunsupported md algorithmvar bad valuec
0x561f0732a91a hit12_1 unacceptableSearching 12 bytes: 75 6e 61 63 63 65 70 74 61

要以 JSON 格式输出搜索结果,只需在前面的搜索命令中添加 j(就像在 r2 shell 中所做的那样)。这可以在大多数命令中使用:

[0x00000000]> \/j unacceptable
Searching 12 bytes: 75 6e 61 63 63 65 70 74 61 62 6c 65
Searching 12 bytes in [0x0000561f05ebf000-0x0000561f05eca000]
...
Searching 12 bytes in [0xffffffffff600000-0xffffffffff601000]
hits: 23
{"address":"0x561f072c4223","size":12,"flag":"hit14_1","content":"unacceptable \
policyunsupported md algorithmvar bad valuec0"},{"address":"0x561f072c4275", \
"size":12,"flag":"hit14_2","content":"unacceptableSearching 12 bytes: 75 6e 61 \
63 63 65 70 74 61"},{"address":"0x561f072c42c8","size":12,"flag":"hit14_3", \
"content":"unacceptableSearching 12 bytes: 75 6e 61 63 63 65 70 74 61 "},
...

要列出已加载的库,请使用命令 \il 并使用 radare2 的内部 grep 命令  过滤结果。 例如,以下命令将列出与关键字 keystoressl 和 crypto 匹配的已加载库:

[0x00000000]> \il~keystore,ssl,crypto
0x00007f3357b8e000 libssl.so.1.1
0x00007f3357716000 libcrypto.so.1.1

类似地,通过特定的关键字列出导出表并过滤结果:

[0x00000000]> \iE libssl.so.1.1~CIPHER
0x7f3357bb7ef0 f SSL_CIPHER_get_bits
0x7f3357bb8260 f SSL_CIPHER_find
0x7f3357bb82c0 f SSL_CIPHER_get_digest_nid
0x7f3357bb8380 f SSL_CIPHER_is_aead
0x7f3357bb8270 f SSL_CIPHER_get_cipher_nid
0x7f3357bb7ed0 f SSL_CIPHER_get_name
0x7f3357bb8340 f SSL_CIPHER_get_auth_nid
0x7f3357bb7930 f SSL_CIPHER_description
0x7f3357bb8300 f SSL_CIPHER_get_kx_nid
0x7f3357bb7ea0 f SSL_CIPHER_get_version
0x7f3357bb7f10 f SSL_CIPHER_get_id

使用命令 db 列出或设置断点。这在分析/修改内存时是有用的:

[0x00000000]> \db

最后请记住,您也可以使用 \ 运行 Frida JavaScript 代码,加上脚本名称即可:

[0x00000000]> \. agent.js

你可以在他们的 Wiki 项目中找到更多关于如何使用 r2frida 的示例。

基本测试操作

访问设备 Shell

在测试应用程序时,最常见的事情之一就是访问设备 shell。在这一节中,我们将看到如何在有或没有 USB 线情况下从你的主机上远程访问 Android Shell,以及在本地设备访问。

远程 Shell

为了从你的主机连接到 Android 设备的 shell,adb 通常是你选择的工具(除非你喜欢使用远程 SSH 访问,例如通过 Termux)。

对于本节,我们假设你已经正确地启用了开发者模式和 USB 调试,正如“在真实设备上进行测试”中所解释的那样。一旦你通过 USB 连接了 Android 设备,你可以通过运行以下命令访问远程设备的 shell:

$ adb shell

按 Control + D 或输入 exit 退出

如果你的设备是 root 的或你正在使用模拟器,一旦你处在远程 shell 中,你可以通过运行 su 获得 root 访问权限:

$ adb shell
bullhead:/ $ su
bullhead:/ # id
uid=0(root) gid=0(root) groups=0(root) context=u:r:su:s0

只有当你使用模拟器时,才可以使用命令 adb root 重新启动 adb,这样下次进入 adb shell 时,你就已经拥有 root 权限了。这也允许在工作站和 Android 文件系统之间双向传输数据,甚至可以访问只有 root 用户可以访问的位置(通过 adb push/pull)。有关数据传输的更多信息,请参阅下面“主机-设备数据传输”一节。

连接多个设备

如果你有不止一个设备,记住在你的所有 adb 命令上包括 -s 参数,后跟设备序列号(例如 adb -s emulator-5554 shell 或 adb -s 00b604081540b7c6 shell)。你可以使用以下命令得到所有连接的设备的列表和他们的序列号:

$ adb devices
List of devices attached
00c907098530a82c    device
emulator-5554    device

通过 Wi-Fi 连接一个设备

你也可以不使用 USB 线访问你的 Android 设备。为此,你必须将你的主机和安卓设备连接到同一个 Wi-Fi 网络,然后按照下面的步骤进行:

  • 用 USB 线将设备连接到主机,并设置目标设备在端口5555上监听 TCP/IP 连接:adb tcpip 5555

  • 断开与目标设备的USB连接线,运行 adb connect <device_ip_address>。运行 adb devices 检查该设备现在是否可用。

  • 用 adb shell 打开 shell。

但是请注意,这样做会让你的设备对处于同一网络并知道你的设备 IP 地址的任何人开放。你可能更喜欢使用 USB 连接。

例如,在一个 Nexus 设备上,你可以在设
Settings -> System -> About phone -> Status -> IP address 找到 IP 地址,或者进入 Wi-Fi 菜单,在你连接的网络上点击一下。

在 Android 开发者文档中可以看到完整的说明和注意事项。

通过 SSH 连接设备

如果愿意,还可以启用 SSH 访问。一个方便的选项是使用 Termux,你可以轻松地配置它来提供 SSH 访问(使用密码或公钥身份验证),并使用命令 sshd 启动它(默认在端口8022上启动)。为了通过 SSH 连接到 Termux,只需运行命令 ssh -p 8022 <ip_address>(其中 ip_address 是实际的远程设备IP)。这个选项还有一些额外的好处,它允许在端口8022上通过 SFTP 访问文件系统。

设备上的 Shell 应用

与远程 shell 相比,通常使用设备上的 shell(终端模拟器))可能非常单调乏味,但对于调试,例如网络问题或检查某些配置来说,它很方便。

Termux 是一个用于 Android 的终端模拟器,它提供了一个 Linux 环境,可以直接使用或不使用 root,并且不需要设置。安装额外的包是一项琐碎的任务,但是它有自己的 APT 包管理器(与其他终端模拟器应用程序相比的不同之处)。你可以使用命令 pkg search <pkg_name> 来搜索特定的包,并使用 pkg install <pkg_name> 来安装包。你可以直接从 Google Play 安装 Termux。

主机设备数据传输

使用 adb

你可以使用 adb pull <remote> <local> 和 adb push <local> <remote> 命令将文件复制到设备或从设备中复制。它们的用法非常简单,例如,下面的操作将把 foo.txt 从当前目录(本地)复制到 sdcard 文件夹(远程):

$ adb push foo.txt /sdcard/foo.txt

这种方法通常在你知道你想要复制什么和复制到哪里从哪里复制时使用,也支持批量文件传输,例如你可以从 Android 设备复制整个目录到你的工作站。

$ adb pull /sdcard
/sdcard/: 1190 files pulled. 14.1 MB/s (304526427 bytes in 20.566s)

使用 Android Studio 设备文件浏览器

Android Studio 有一个内置的设备文件资源管理器,你可以通过 View -> Tool Windows -> Device File Explorer 打开它。

如果你使用的是一个 root 设备,那么现在可以开始浏览整个文件系统了。然而,当使用未 root 设备访问应用程序沙箱时,除非应用程序是可调试的,否则不会工作,即使那样,你也会被“监禁”在应用程序沙箱中。

使用 objection

当你在一个特定的应用程序上工作并且想要复制你可能在它的沙箱中遇到的文件时,这个选项非常有用(注意你只能访问目标应用程序能够访问的文件)。这种方法不需要将应用设置为可调试的,在使用 Android Studio 的设备文件浏览器时需要这样做。

首先,如“推荐工具-Objection”中所述,以 Objection 的方式连接到应用程序。然后,像往常一样在终端上使用 ls 和 cd 浏览可用文件:

$ frida-ps -U | grep -i owasp
21228  sg.vp.owasp_mobile.omtg_android
 
$ objection -g sg.vp.owasp_mobile.omtg_android explore
 
...g.vp.owasp_mobile.omtg_android on (google: 8.1.0) [usb] # cd ..
/data/user/0/sg.vp.owasp_mobile.omtg_android
 
...g.vp.owasp_mobile.omtg_android on (google: 8.1.0)  [usb] # ls
Type       ...  Name
---------  ...  -------------------
Directory  ...  cache
Directory  ...  code_cache
Directory  ...  lib
Directory  ...  shared_prefs
Directory  ...  files
Directory  ...  app_ACRA-approved
Directory  ...  app_ACRA-unapproved
Directory  ...  databases
 
Readable: True  Writable: True

一旦你有一个文件你想下载,你可以运行 file download <some_file>。这将下载该文件到你的工作目录,同样的方式,你可以使用 file upload 上传文件。

...[usb] # ls
Type    ...  Name
------  ...  -----------------------------------------------
File    ...  sg.vp.owasp_mobile.omtg_android_preferences.xml
 
Readable: True  Writable: True
...[usb] # file download sg.vp.owasp_mobile.omtg_android_preferences.xml
Downloading ...
Streaming file from device...
Writing bytes to destination...
Successfully downloaded ... to sg.vp.owasp_mobile.omtg_android_preferences.xml

缺点是,在撰写本文时,objection 还不支持批量文件传输,因此你只能复制单个文件。不过,在某些情况下,如果你已经在使用 objection 探索应用程序,并找到了一些有趣的文件,这一点还是很有用的。不需要记下文件的完整路径并使用 adb pull <path_to_some_file>,你可以直接 file download <some_file> 下载文件。

使用 Termux

如果你有一个 root 设备,并且安装了 Termux 以及在其上正确配置了 SSH 访问权限,那么在端口8022上应该已经运行了一个 SFTP(SSH 文件传输协议)服务器,你可从终端访问它:

$ sftp -P 8022 root@localhost
...
sftp> cd /data/data
sftp> ls -1
...
sg.vantagepoint.helloworldjni
sg.vantagepoint.uncrackable1
sg.vp.owasp_mobile.omtg_android

或者简单地通过使用支持 SFTP 的客户端(如FileZilla):

查看 Termux Wiki 以了解更多有关远程文件访问方法的信息。

获取和解压应用程序
从设备中提取APK文件有几种方法。根据应用程序是公共的还是私有的,你需要决定哪种方法是最简单的。

获取和解压应用程序

从设备中提取 APK 文件有几种方法。你需要根据应用程序是公共的还是私有的,决定哪种方法是最简单的。

其他应用程序商店

最简单的选择之一是从 Google Play Store 的公共应用程序镜像的网站下载 APK。但是,请记住,这些站点不是官方站点,并且不能保证该应用程序没有重新打包或包含恶意程序。一些著名网站托管 APK,并以不修改应用程序而闻名,甚至列出了应用程序的 SHA-1 和 SHA-256 校验和:

注意,你不能控制这些网站,你不能保证他们在未来做什么,在没有其他选择时再使用它们。

使用 gplaycli

gplaycli 是一个基于 Python 的 CLI 工具,用于从 Google Play Store 搜索、安装和更新 Android 应用程序。按照安装步骤操作就可以运行它了,gplaycli提供了几个选项,请参考其帮助(-h)以获得更多信息。

如果你不确定一个应用程序的包名(或 AppID),你可以执行一个关键字搜索 APK (-s):

$ gplaycli -s "google keep"
 
Title                          Creator     Size      Last Update  AppID                                    Version
 
Google Keep - notes and lists  Google LLC  15.78MB   4 Sep 2019   com.google.android.keep                  193510330
Maps - Navigate & Explore      Google LLC  35.25MB   16 May 2019  com.google.android.apps.maps             1016200134
Google                         Google LLC  82.57MB   30 Aug 2019  com.google.android.googlequicksearchbox  301008048

注意在使用 gplaycli 时的应用区域(
Google Play)限制,为了访问限制在你的国家的应用程序,你可以使用其他应用程序商店,比如“其他应用程序商店”中描述的那些。

接下来,你可以通过指定 APK 的 AppID 来下载(-d)选择的APK(添加 -p 显示进度条,添加 -v 显示信息):

$ gplaycli -p -v -d com.google.android.keep
[INFO] GPlayCli version 3.26 [Python3.7.4]
[INFO] Configuration file is ~/.config/gplaycli/gplaycli.conf
[INFO] Device is bacon
[INFO] Using cached token.
[INFO] Using auto retrieved token to connect to API
[INFO] 1 / 1 com.google.android.keep
[################################] 15.78MB/15.78MB - 00:00:02 6.57MB/s/s
[INFO] Download complete

com.google.android.keep.apk 文件将下载到你当前的目录中。正如你所想象的,这种方法是一种非常方便的下载 APK 的的方法,特别是在自动化方面。

你可以使用自己的 Google Play 凭证或令牌,默认情况下,gplaycli 将使用内部提供的令牌

从设备中提取应用程序包

推荐的方法是从设备获取应用程序包包,因为我们可以保证应用程序包没有被第三方修改过。要从一个 root 或非 root 设备获取应用程序,可以使用以下方法:

使用 adb pull 取回 APK,如果你不知道包名,第一步是列出设备上安装的所有应用程序:

$ adb shell pm list packages

一旦找到了应用程序的包名,就需要通过它在系统中存储的完整路径来下载它。

$ adb shell pm path <package name>

有了 APK 的完整路径后,现在可以简单地使用 adb pull 来提取它。

$ adb pull <apk path>

APK将下载到你的工作目录中。

另外,还有一些像 APK Extractor 这样的应用程序不需要 root,甚至可以通过你喜欢的方法共享所提取的 APK。如果你不喜欢通过网络连接设备或设置 adb 来传输文件,那么这将非常有用。

安装应用程序

使用 adb install 在模拟器或连接的设备上安装 APK。

adb install path_to_apk

注意,如果你有原始的源代码并且使用 Android Studio,则不需要这样做,因为 Android Studio 会为你完成应用的打包和安装过程。

信息收集

分析应用程序的一个基本步骤是收集信息,这可以通过检查工作站中的应用程序包或远程访问设备上的应用程序数据来完成。在后面的章节中,你会发现更高级的技术,但现在,我们将集中在基础上:获得所有已安装应用的列表,探索应用程序包,访问设备上的应用程序数据目录。这应该会给你一些关于这个应用程序的背景信息,甚至不需要对它进行逆向工程或执行更高级的分析。我们将回答以下问题:

  • 包中包含哪些文件?
  • 应用程序使用哪些原生库?
  • 应用程序定义哪些应用程序组件?有哪些服务或内容提供者?
  • 应用程序是可调试的吗?
  • 应用程序是否包含网络安全策略?
  • 安装时应用程序是否创建新文件?

列出已安装应用程序

当目标是安装在设备上的应用程序时,你首先要弄清楚你想要分析的应用程序的正确包名。你可以通过 pm(Android软件包管理器)或使用 frida-ps 来检索已安装的应用程序:

$ adb shell pm list packages
package:sg.vantagepoint.helloworldjni
package:eu.chainfire.supersu
package:org.teamsik.apps.hackingchallenge.easy
package:org.teamsik.apps.hackingchallenge.hard
package:sg.vp.owasp_mobile.omtg_android

你可以添加参数只显示第三方应用程序(-3)和它们的 APK 文件的位置(-f),随后使用 adb pull 可进行下载:

$ adb shell pm list packages -3 -f
package:/data/app/sg.vantagepoint.helloworldjni-1/base.apk=sg.vantagepoint.helloworldjni
package:/data/app/eu.chainfire.supersu-1/base.apk=eu.chainfire.supersu
package:/data/app/org.teamsik.apps.hackingchallenge.easy-1/base.apk=org.teamsik.apps.hackingchallenge.easy
package:/data/app/org.teamsik.apps.hackingchallenge.hard-1/base.apk=org.teamsik.apps.hackingchallenge.hard
package:/data/app/sg.vp.owasp_mobile.omtg_android-kR0ovWl9eoU_yh0jPJ9caQ==/base.apk=sg.vp.owasp_mobile.omtg_android

这与运行 adb shell pm path <app_package_id> 类似。

$ adb shell pm path sg.vp.owasp_mobile.omtg_android
package:/data/app/sg.vp.owasp_mobile.omtg_android-kR0ovWl9eoU_yh0jPJ9caQ==/base.apk

使用`frida-ps -Uai`获取已连接 USB 设备(`-U`)上当前安装(`-i`)的所有应用程序(`a`):

$ frida-ps -Uai
  PID  Name                                      Identifier
-----  ----------------------------------------  ---------------------------------------
  766  Android System                            android
21228  Attack me if u can                        sg.vp.owasp_mobile.omtg_android
 4281  Termux                                    com.termux
    -  Uncrackable1                              sg.vantagepoint.uncrackable1
    -  drozer Agent                              com.mwr.dz

注意,这也显示了当前正在运行的应用程序的PID。记下标识符和 PID(如果有的话),以后会用到它们。

探索应用程序包

一旦收集了目标应用程序的包名,你将希望开始收集有关它的信息。首先检索 APK,如“基本测试操作——获取和提取应用程序”中所解释的那样。

APK 文件实际上是 ZIP 文件,可以使用一个标准的 unarchiver 进行解压:

$ unzip base.apk
$ ls -lah
-rw-r--r--   1 sven  staff    11K Dec  5 14:45 AndroidManifest.xml
drwxr-xr-x   5 sven  staff   170B Dec  5 16:18 META-INF
drwxr-xr-x   6 sven  staff   204B Dec  5 16:17 assets
-rw-r--r--   1 sven  staff   3.5M Dec  5 14:41 classes.dex
drwxr-xr-x   3 sven  staff   102B Dec  5 16:18 lib
drwxr-xr-x  27 sven  staff   918B Dec  5 16:17 res
-rw-r--r--   1 sven  staff   241K Dec  5 14:45 resources.arsc

下面是被解压的文件:

  • AndroidManifest.xml:包含应用程序包名,目标和最低 API 等级,应用程序配置,应用程序组件,权限等的定义。

  • META-INF:包含应用程序的元数据
    MANIFEST.MF:存储应用程序资源的哈希
    CERT.RSA:应用程序的证书
    CERT.SF:MANIFEST.MF 文件中的资源列表和相应行的 SHA-1 摘要

  • assets:包含应用程序资源(Android 应用程序中使用的文件,例如 XML 文件,JavaScript 文件和图片)的目录,AssetManager 可以检索该目录

  • classes.dex:以 DEX 文件格式编译的类,Dalvik 虚拟机/ Android Runtime 可以运行。 DEX 是 Dalvik 虚拟机的 Java 字节码, 针对小型设备进行了优化

  • lib:包含组成 APK 的第三方库的目录。

  • res:包含尚未编译为 resources.arsc 的资源目录
  • resources.arsc:包含预编译资源的文件,例如用于布局的XML文件

由于使用标准 unzip 工具解压会留下一些不可读的文件,如 AndroidManifest.xml,你最好使用 apktool 解压APK,正如“推荐工具- apktool”中所述,解压结果如下:

$ ls -alh
total 32
drwxr-xr-x    9 sven  staff   306B Dec  5 16:29 .
drwxr-xr-x    5 sven  staff   170B Dec  5 16:29 ..
-rw-r--r--    1 sven  staff    10K Dec  5 16:29 AndroidManifest.xml
-rw-r--r--    1 sven  staff   401B Dec  5 16:29 apktool.yml
drwxr-xr-x    6 sven  staff   204B Dec  5 16:29 assets
drwxr-xr-x    3 sven  staff   102B Dec  5 16:29 lib
drwxr-xr-x    4 sven  staff   136B Dec  5 16:29 original
drwxr-xr-x  131 sven  staff   4.3K Dec  5 16:29 res
drwxr-xr-x    9 sven  staff   306B Dec  5 16:29 smali

 

Android 清单文件

Android 清单文件是信息的主要来源,它包含了很多有趣的信息,比如包名、权限、应用程序组件等等。

这里是一些信息和相应的关键字的非详尽列表,你可以很容易地通过检查文件或使用 grep -i <keyword> AndroidManifest.xml在 AndroidManifest 搜索。

  • App权限:permission(见“Android 平台 API”)

  • Backup llowance:android:allowBackup(见“android上的数据存储”)

  • 应用程序组件:activityservice, providerreceiver(见“Android 平台 API”和“Android 上的数据存储”)

  • 可调试标志:debuggable(参见“Android应用程序的代码质量和编译设置”)

请参阅前面提到的章节来了解更多关于如何测试这些要点的信息。

应用程序二进制文件

如上文“探索应用程序包”所示,应用程序的二进制文件(classes.dex)可以在应用程序包的根目录中找到。它是一个所谓的 DEX(Dalvik 可执行文件)文件,包含编译后的Java代码。由于它的特性,在一些转换之后,你将能够使用反编译器来生成 Java 代码。我们还看到了运行 apktool 后生成的 smali 文件夹。它以一种叫做 smali 的中间语言包含了反汇编的 Dalvik 字节码,这是 Dalvik 可执行文件的一种人类可读的表示。

有关如何 DEX 文件逆向工程的更多信息,请参考“Android上的篡改和逆向工程”一章中的“检查反编译 Java 代码”一节。

原生库

你可以查看 APK 中的 lib 文件夹:

$ ls -1 lib/armeabi/
libdatabase_sqlcipher.so
libnative.so
libsqlcipher_android.so
libstlport_shared.so

或者使用 objection:

...g.vp.owasp_mobile.omtg_android on (google: 8.1.0) [usb] # ls lib
Type    ...  Name
------  ...  ------------------------
File    ...  libnative.so
File    ...  libdatabase_sqlcipher.so
File    ...  libstlport_shared.so
File    ...  libsqlcipher_android.so

到目前为止,这是你可以获得的关于原生库的所有信息,除非你开始对它们进行逆向工程,即使用不同于逆向应用程序二进制文件的方法来完成,因为代码不能反编译只能反汇编。有关如何对这些库进行逆向工程的更多信息,请参考“Android上的篡改和逆向工程”一章中的“检查原生反汇编代码”一节。

其他的应用程序资源

通常看看 APK 根目录中能否找到其他的资源和文件是很值得的,因为有时它们会包含额外的好东西,如密钥存储库、加密的数据库、证书等。

访问应用程序数据目录

一旦你安装了应用程序,还有更多的信息需要探索,像 objection 就会派上用场了。

当使用 objection 时,你可以检索不同类型的信息,其中 env 将显示应用程序的所有目录信息。

$ objection -g sg.vp.owasp_mobile.omtg_android explore
 
...g.vp.owasp_mobile.omtg_android on (google: 8.1.0) [usb] # env
 
Name                    Path
----------------------  ---------------------------------------------------------------------------
cacheDirectory          /data/user/0/sg.vp.owasp_mobile.omtg_android/cache
codeCacheDirectory      /data/user/0/sg.vp.owasp_mobile.omtg_android/code_cache
externalCacheDirectory  /storage/emulated/0/Android/data/sg.vp.owasp_mobile.omtg_android/cache
filesDirectory          /data/user/0/sg.vp.owasp_mobile.omtg_android/files
obbDir                  /storage/emulated/0/Android/obb/sg.vp.owasp_mobile.omtg_android
packageCodePath         /data/app/sg.vp.owasp_mobile.omtg_android-kR0ovWl9eoU_yh0jPJ9caQ==/base.apk

在这些信息我们可以找到:

  • 内部数据目录(又称沙盒目录),在 /data/data/[package-name]或 /data/user/0/[package-name]

  • 外部数据目录在 /storage/emulated/0/Android/data/[package-name] 或 /sdcard/Android/data/[package-name]

  • 应用程序包的路径在 /data/app/

内部数据目录用于存储运行时创建的数据,基本结构如下:

...g.vp.owasp_mobile.omtg_android on (google: 8.1.0)  [usb] # ls
Type       ...  Name
---------  ...  -------------------
Directory  ...  cache
Directory  ...  code_cache
Directory  ...  lib
Directory  ...  shared_prefs
Directory  ...  files
Directory  ...  databases
 
Readable: True  Writable: True

每个文件夹都有自己的目的:

  • cache:此位置用于数据缓存,例如,在此目录中找到 WebView 缓存。

  • code_cache:这是文件系统的应用程序特定缓存目录的位置,设计用于存储缓存代码。对于运行 Android 5.0(API 等级21)或更高版本的设备,当应用或整个平台升级时,系统将删除存储在该位置的所有文件。

  • lib:此文件夹存储用 C/C++ 写的原生库,这些库可能有几个文件扩展名之一,包括 .so 和 .dll(x86支持)。此文件夹包含应用程序具有原生库平台的子目录,包括
    a. armeabi:所有基于 ARM 处理器编译的代码
    b. armeabi-v7a:仅适用于所有基于版本7及更高版本的 ARM 处理器编译的代码
    c. arm64-v8a:所有仅基于版本8及更高版本的 ARM 64位处理器编译的代码
    d. x86:仅适用基于 x86 处理器编译的代码
    e. x86_64:仅适用基于 x86_64 处理器编译的代码
    f. mips:基于 MIPS 处理器编译的代码

  • shared_prefs:此文件夹包含一个XML文件,该文件存储通过 SharedPreferences API 保存的值。

  • file:此文件夹存储应用创建的常规文件。

  • 数据库:此文件夹存储应用程序在运行时生成的 SQLite 数据库文件,例如用户数据文件。

然而,应用程序可能不仅在这些文件夹中存储更多的数据,还会在父文件夹(/data/data/[package-name])中存储更多数据。

有关安全存储敏感数据的更多信息和最佳实践,请参阅“测试数据存储”一章。

监控系统日志

在Android上,你可以通过使用 Logcat 很容易地查看系统消息的日志,Logcat有两种执行方式:

  • Logcat 是 Android Studio 中 Dalvik 调试监视器服务器(DDMS)的一部分。 如果应用程序以调试模式运行,则日志输出将显示在 Android Monitor 的 Logcat 选项卡上。 你可以通过在 Logcat 中定义模式来过滤应用程序的日志输出。

  • 你可以使用 adb 执行 Logcat 来持久化存储日志输出:

$ adb logcat > logcat.log

使用以下命令,可以在作用域内对应用程序的日志输出进行 grep,只需插入包名。当然,你的应用程序需要运行,以便 ps 能够获得它的 PID。

$ adb logcat | grep "$(adb shell ps | grep <package-name> | awk '{print $2}')"

设置网络测试环境

基本的网络监控/嗅探

通过 tcpdump、netcat (nc) 和 Wireshark 可以实时远程嗅探所有 Android 流量。首先,确保你的手机上有最新版本的 Android tcpdump,以下是安装步骤

$ adb root
$ adb remount
$ adb push /wherever/you/put/tcpdump /system/xbin/tcpdump

如果执行`adb root` 返回错误`adbd cannot run as root in production builds`,如下安装 tcpdump:

$ adb push /wherever/you/put/tcpdump /data/local/tmp/tcpdump
$ adb shell
$ su
$ mount -o rw,remount /system;
$ cp /data/local/tmp/tcpdump /system/xbin/
$ cd /system/xbin
$ chmod 755 tcpdump

你可能会遇到错误 mount: '/system' not in /proc/mounts

在这种情况下,你可以使用 $ mount -o rw,remount / 来替代 $ mount -o rw,remount /system; 一行。

记住:要使用 tcpdump,你需要手机有 root 权限。

执行 tcpdump,看看是否有效。一旦传入了一些包,可以按 CTRL+c 停止 tcpdump。

$ tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on wlan0, link-type EN10MB (Ethernet), capture size 262144 bytes
04:54:06.590751 00:9e:1e:10:7f:69 (oui Unknown) > Broadcast, RRCP-0x23 reply
04:54:09.659658 00:9e:1e:10:7f:69 (oui Unknown) > Broadcast, RRCP-0x23 reply
04:54:10.579795 00:9e:1e:10:7f:69 (oui Unknown) > Broadcast, RRCP-0x23 reply
^C
3 packets captured
3 packets received by filter
0 packets dropped by kernel

要远程嗅探 Android 手机的网络流量,首先执行 tcpdump 并通过管道将其输出到 netcat(nc):

$ tcpdump -i wlan0 -s0 -w - | nc -l -p 11111

上面的 tcpdump 命令涉及到

  • 监听wlan0接口,
  • 以字节为单位定义捕获的大小(快照长度)以获取所有内容(-s0)和
  • 写入文件(-w)。我们传递 -而不是文件名,这将使 tcpdump 写入标准输出中。

通过使用管道 (|),我们将 tcpdump 的所有输出发送到 netcat,netcat 会在端口11111上打开一个监听器。你通常需要监视 wlan0 接口。如果你需要另一个接口,请使用命令 $ ip addr 列出可用的选项。

要访问端口 11111,你需要通过 adb 将该端口转发到你的设备上。

$ adb forward tcp:11111 tcp:11111

下面的命令通过 netcat 将你连接到转发的端口,并通过管道连接到 Wireshark。

$ nc localhost 11111 | wireshark -k -S -i -

Wireshark 应该立即启动(-k)。它会连接到转发端口,通过 netcat 从标准输入(-i -)中获得所有数据,你应该能看到来自 wlan0 接口的所有手机流量。

你可以使用 Wireshark 以可读的格式显示捕获的流量,弄清楚使用了哪些协议以及它们是否未加密。捕获所有流量(TCP 和 UDP)非常重要,因此你应该执行测试应用程序的所有功能并对其进行分析。


Wireshark 和 tcpdump

Firebase/Google 云消息传递(FCM/GCM)

Firebase 云消息传递(FCM)是 Google 云消息传递(GCM)的后继产品,是 Google 提供的一项免费服务,可让你在应用程序服务器和客户端应用程序之间发送消息。服务器和客户端应用程序通过 FCM/GCM 连接服务器进行通信,可处理下游和上游消息。

下游消息(推送通知)从应用服务器发送到客户端应用程序;上游消息从客户端应用程序发送到服务器。

FCM 适用于 Android、iOS 和 Chrome。FCM目前提供了两种连接服务器协议:HTTP 和 XMPP。如官方文档中所述,这些协议的实现方式不同,下面的示例演示如何拦截这两个协议。

测试步骤准备

您需要在你的手机上配置 iptables 或使用 bettercap 来拦截流量。

FCM 可以使用 XMPP 或 HTTP 与谷歌后端通信。

HTTP

FCM 使用端口5228、5229和5230进行 HTTP 通信,通常只使用端口5228。

  • 为 FCM 使用的端口配置本地端口转发,下面的例子适用于 macOS:
$ echo "
rdr pass inet proto tcp from any to any port 5235-> 127.0.0.1 port 8080
rdr pass inet proto tcp from any to any port 5236 -> 127.0.0.1 port 8080
" | sudo pfctl -ef -
  • 拦截代理必须监听上面端口转发规则中指定的端口(端口8080)。

XMPP

对于 XMPP 通信,FCM 使用端口5235(Production)和5236(Testing)。

  • 为 FCM 使用的端口配置本地端口转发,下面的例子适用于 macOS:
$ echo "
rdr pass inet proto tcp from any to any port 5235-> 127.0.0.1 port 8080
rdr pass inet proto tcp from any to any port 5236 -> 127.0.0.1 port 8080
" | sudo pfctl -ef -

拦截请求

拦截代理必须监听上面端口转发规则中指定的端口(端口8080)。

启动应用程序并触发一个使用 FCM 的函数,你应该能在监听代理中看到 HTTP 消息。

推送通知的端到端加密

作为额外的安全层,推送通知可以使用 Capillary 加密。Capillary 是一个简化从基于 Java 的应用程序服务器向 Android 客户端发送端到端(E2E)加密推送消息的库。

设置拦截代理

有几个工具支持对依赖 HTTP(S) 协议的应用程序进行网络分析。最重要的工具是所谓的拦截代理,OWASP ZAP 和 Burp Suite Professional 是最有名的。一个拦截代理给测试人员一个中间人的位置。这个位置对于读取和修改所有应用程序请求和端点响应非常有用,用于测试授权、会话、管理等。

为一个虚拟设备设置拦截代理

在一个 Android 虚拟设备(AVD)上设置一个 Web 代里

下面的过程,为 Android Studio 3.x 附带的 Android 模拟器设置 HTTP 代理:

  1. 设置代理监听本地主机,例如端口8080。
  2. 在模拟器设置中配置HTTP代理:
    a. 点击模拟器菜单栏中的三个点
    b. 打开设置菜单
    c. 单击 Proxy 选项
    d. 在主机名字段中输入“127.0.0.1”,在端口号字段中输入代理端口(例如,“8080”)
    e. 点击应用

现在应该是通过主机上的代理路来由 HTTP 和 HTTPS 请求。如果没有,试着关闭或打开飞机模式。

在启动 AVD 时,还可以在命令行上使用模拟器命令配置 AVD 的代理。下面的示例启动 AVD Nexus_5X_API_23,并将代理设置为 127.0.0.1 和端口8080。

$ emulator @Nexus_5X_API_23 -http-proxy 127.0.0.1:8080

在虚拟设备上安装一个 CA 证书

安装 CA 证书的一种简单方法是将证书推送到设备上,并通过安全设置将其添加到证书存储中。例如,你可以按以下方式安装 PortSwigger (Burp)CA 证书:

  1. 启动 Burp 并使用主机上的 web 浏览器导航到 burp/,然后单击“CA Certificate”按钮下载 cacert.der
  2. 改变文件后缀 .der 为 .cer
  3. 将文件推送到模拟器
    $ adb push cacert.cer /sdcard/
  4. 点击 Settings -> Security -> Install from SD Card
  5. 滑倒底部点击 cacert.cer

然后系统会提示你确认证书的安装(如果你还没有安装过,系统还会要求设置设备 PIN)。

对于 Android 7.0 (API 等级24)及以上版本,遵循“绕过网络安全配置”一节中描述的相同过程。

为一个真实设备设置拦截代理

首先必须评估可用的网络设置选项。用于测试的移动设备和运行监听代理的机器必须连接到同一个 Wi-Fi 网络,使用(现有的)接入点或创建一个 ad-hoc 无线网络

配置好网络并在测试机器和移动设备之间建立连接之后,还需要执行几个步骤。

  • 代理必须配置为指向拦截代理

  • 拦截代理的 CA 证书必须添加到 Android 设备证书存储中的可信证书中。用于存储 CA 证书的菜单的位置可能取决于 Android 版本和Android OEM 对设置菜单的修改。

  • 如果叶子证书的有效期延长了一定时间(对于Chrome,则为39个月),一些应用程序可能会显示 NET::ERR_CERT_VALIDITY_TOO_LONG 错误。如果使用默认的 Burp CA 证书,就会发生这种情况,因为 Burp Suite 颁发的叶证书与其 CA 证书具有相同的有效性。你可以通过创建自己的 CA 证书并将其导入到 Burp Suite 来绕过这个问题,这在 nviso.be 上的一篇博客文章中有解释。

完成这些步骤并启动应用程序后,请求应该会显示在拦截代理中。

在 secure.force.com 上你可以找到在 Android 设备上设置 OWASP ZAP 的视频。

其他一些区别:从 Android 8.0(API 等级26)开始,当 HTTPS 流量通过另一个连接时,应用程序的网络行为会发生变化。而从 Android 9(AP 等级28)开始,在握手过程中出现错误时,SSLSocket 和 SSLEngine 在错误处理方面的行为将略有不同。

如前所述,从 Android 7.0(API 等级24)开始,Android OS 默认不再信任用户 CA 证书,除非在应用程序中指定,在下一节中,我们将解释绕过 Android 安全控制的两种方法。

绕过网络安全配置

从 Android 7.0(API 等级24)开始,网络安全配置允许应用自定义它们的网络安全设置,通过定义应用程序信任哪些 CA 证书。

为了实现应用程序的网络安全配置,需要创建一个名为 network_security_config.xml 的新 xml 资源文件。这在谷歌 Android 代码库中有详细的解释。

创建之后,应用程序还必须在清单文件中包含一个条目,以指向新的网络安全配置文件。

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
    <application android:networkSecurityConfig="@xml/network_security_config"
                    ... >
        ...
    </application>
</manifest>

网络安全配置使用一个 XML 文件,应用程序在其中指定哪些 CA 证书将被信任。有许多绕过网络安全配置的方法,下面将对此进行描述,参阅在 Android P 上的网络安全配置安全分析指南了解更多信息。

将用户证书添加到网络安全配置中

网络安全配置有不同的配置,可以通过 src 属性添加非系统证书的颁发机构:

<certificates src=["system" | "user" | "raw resource"]
              overridePins=["true" | "false"] />

每个证书可以是下列中的一种:

  • 一个“raw resource”ID,指向包含 X.509 证书的文件

  • 预安装的系统 CA 证书的“system”

  • 用户添加的CA证书的“user”

应用程序信任的 CA 证书可以是系统信任的CA,也可以是用户信任的 CA。通常你已经在 Android 中添加了拦截代理的证书作为附加 CA。因此我们将重点关注“user”设置,该设置允许你通过以下网络安全配置强制 Android 应用程序信任此证书:

<network-security-config>
   <base-config>
      <trust-anchors>
          <certificates src="system" />
          <certificates src="user" />
      </trust-anchors>
   </base-config>
</network-security-config>

要实现这个新设置,你必须遵循以下步骤:

  • 使用反编译工具如 apktool 反编译应用程序:
    $ apktool d <filename>.apk
  • 通过创建包含 <certificates src="user">的网络安全配置,使应用程序信任用户证书

  • 在反编译应用程序时,进入 apktool 创建的目录,并使用 apktool 重编译应用程序,新的 apk 将在 dist 目录中

$ apktool b
  • 你需要重新打包应用程序,正如“逆向工程和篡改”一章的“重打包”部分所解释的那样。有关重新打包过程的更多细节,你也可以参考 Android 开发者文档,它从整体上解释了这个过程。

请注意,即使这种方法非常简单,它的主要缺点是你必须对想要评估的每个应用程序应用此操作,这是额外的测试开销。

请记住,如果您正在测试的应用程序有额外的强化措施,比如验证应用程序签名,那么你可能无法再启动该应用程序。作为重打包的一部分,你将使用自己的密钥签名应用程序,因此签名变化将导致这样的检查立即终止程序。你需要识别和禁用这些检查通过在重新打包的应用程序修改他们或使用 Frida 动态分析。

使用 Android-CertKiller 的 python 脚本可以自动执行上述步骤。这个脚本可以从安装的 Android 应用程序中提取APK,对其进行反编译,使其可调试,添加允许用户证书的新的网络安全配置,编译并签名新的 APK,并通过 SSL Bypass 安装新的 APK。

python main.py -w
 
***************************************
Android CertKiller (v0.1)
***************************************
 
CertKiller Wizard Mode
---------------------------------
List of devices attached
4200dc72f27bc44d    device
 
---------------------------------
 
Enter Application Package Name: nsc.android.mstg.owasp.org.android_nsc
 
Package: /data/app/nsc.android.mstg.owasp.org.android_nsc-1/base.apk
 
I. Initiating APK extraction from device
   complete
------------------------------
I. Decompiling
   complete
------------------------------
I. Applying SSL bypass
   complete
------------------------------
I. Building New APK
   complete
------------------------------
I. Signing APK
   complete
------------------------------
 
Would you like to install the APK on your device(y/N): y
------------------------------------
 Installing Unpinned APK
------------------------------
Finished

使用 Magisk 在系统信任的 CA 中添加代理证书

为了避免为每个应用程序配置网络安全配置,我们必须强制设备接受代理的证书作为系统信任的证书之一。

有一个 Magisk 模块,它将自动将所有用户安装的 CA 证书添加到系统信任的 CA 列表中。

在 Github 发布页面下载最新版本的模块,将下载的文件推送到设备上,点击 + 按钮导入到 Magisk 管理器的“Module”视图中。最后,重新启动 Magisk Manager 以使更改生效。

从现在开始,通过这个 Magisk 模块,用户在“设置”,“安全性和位置”,“加密和凭据”,“从存储安装”(位置可能有所不同)安装的任何 CA 证书都会自动由此推送到系统的信任存储中。 重新启动并验证 CA 证书是否在“设置”,“安全和位置”,“加密和凭据”,“受信任的凭据”中列出(位置可能有所不同)。

手动在系统信任的 CA 中添加代理证书

或者,您也可以手动遵循以下步骤,以达到相同的结果:

  • 使 / 系统分区可写,这可能只在 root 设备上进行。运行 'mount' 命令以确保 / 系统分区是可写的:mount -o rw,remount /system。如果该命令失败,尝试运行以下命令 mount -o rw,remount -t ext4 /system

  • 准备代理的 CA 证书以匹配系统证书格式,以 der 格式导出代理证书(这是 Burp Suite 的默认格式),然后运行以下命令:

$ openssl x509 -inform DER -in cacert.der -out cacert.pem
$ openssl x509 -inform PEM -subject_hash_old -in cacert.pem | head -1
mv cacert.pem <hash>.0
  • 最后,复制 <hash>.0 文件进入 /system/etc/security/cacerts 目录,然后运行以下命令:
chmod 644 <hash>.0

通过遵循上述步骤,你可以让任何应用程序信任代理的证书,这允许拦截其流量,当然,除非应用程序使用 SSL pinning。

潜在障碍

应用程序通常实现一些安全控制,这些控制使得对应用程序执行安全检查变得更加困难,例如 root 检测和证书固定。理想情况下,你将获得启用和禁用这些控制的两个应用程序版本,这允许你分析控制的正确实现,然后可以继续使用不太安全的版本进行进一步测试。

当然,这并不总是可能的,你可能需要对启用了所有安全控制的应用程序执行黑盒评估。下面的部分将向您展示如何绕过针对不同应用程序的证书固定。

无线网络中的客户端隔离

一旦你设置了一个拦截代理并且有了一个中间人的位置,你可能仍然不能看到任何东西。这可能是由于应用程序中的限制(参见下一节),但也可能是由于你所连接的 Wi-Fi 中所谓的客户端隔离。

无线客户端隔离是一种安全特性,它阻止无线客户端彼此通信。此功能对游客和 BYOD SSID 很有用,它增加了安全级别,以限制连接到无线网络的设备之间的攻击和威胁。

如果我们需要测试的Wi-Fi具有客户端隔离怎么办?

你可以在 Android 设备上配置代理指向 127.0.0.1:8080,通过 USB 连接手机到笔记本电脑,使用 adb 做一个反向端口转发:

$ adb reverse tcp:8080 tcp:8080

一旦你完成了这个操作,Android 手机上所有的代理流量将会在127.0.0.1上转到8080端口,它将会通过 adb 重定向到笔记本电脑上的127.0.0.1:8080,你将会在 Burp 中看到流量。使用此技巧,还可以在具有客户端隔离的 Wi-Fi 中测试和拦截流量。

Non-Proxy Aware Apps

一旦你设置了一个拦截代理并且有了一个中间人位置,你可能仍然不能看到任何东西,这主要是由于以下原因:

  • 这款应用使用了一个类似 Xamarin 的框架,它没有使用Android操作系统的代理设置。

  • 你正在测试的应用程序检测了设置的代理,不允许进行任何通信。

在这两种场景中,你都需要额外的步骤最后才能看到流量,在下面的部分中,我们将描述两种不同的解决方案,bettercap 和 iptables。

还可以使用在你控制下的接入点来重定向通信,但这将需要额外的硬件,我们现在主要关注软件层面的解决方案。

对于这两个解决方案,你需要在 Burp 激活“支持不可见代理”,在代 Proxy Tab/Options/Edit 界面。

iptables

可以在 Android 设备上使用 iptables 将所有通信重定向到监听代理,下面的命令将把端口80重定向到在端口8080上你运行的代理:

$ iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination <Your-Proxy-IP>:8080

验证 iptables 设置并检查 IP 和端口。

$ iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
 
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
 
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DNAT       tcp  --  anywhere             anywhere             tcp dpt:5288 to:<Your-Proxy-IP>:8080
 
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
 
Chain natctrl_nat_POSTROUTING (0 references)
target     prot opt source               destination
 
Chain oem_nat_pre (0 references)
target     prot opt source               destination

如果你想重置 iptables 配置,可以刷新规则:

$ iptables -t nat -F

bettercap

阅读“测试网络通信”一章和“模拟中间人攻击”测试用例,可以了解运行 bettercap 的进一步准备和说明。

运行代理的机器和 Android 设备必须连接到同一个无线网络。使用以下命令启动 bettercap,将下面的 IP 地址(X.X.X.X)替换为 Android 设备的 IP 地址。

$ sudo bettercap -eval "set arp.spoof.targets X.X.X.X; arp.spoof on; set arp.spoof.internal true; set arp.spoof.fullduplex true;"
bettercap v2.22 (built for darwin amd64 with go1.12.1) [type 'help' for a list of commands]
 
[19:21:39] [sys.log] [inf] arp.spoof enabling forwarding
[19:21:39] [sys.log] [inf] arp.spoof arp spoofer started, probing 1 targets.

代理检测

一些移动应用程序试图检测是否设置了代理。如果是设置了,他们会认为这是恶意的,不能正常工作。

为了绕过这种保护机制,您可以设置 bettercap 或配置不需要在 Android 手机上设置代理的 iptables。我们之前没有提到的第三种选择是使用 Frida,在 Android 上可以通过查询 ProxyInfo 类并检查 getHost() 和 getPort() 方法来检测是否设置了系统代理。可能还有其他各种方法来完成相同的任务,以及你需要反编译 APK,以识别实际的类和方法名。

下面您可以找到一个 Frida 脚本的模板源代码,它将帮助你重写用于验证是否设置了代理的方法,并始终返回 false(在本例中称为 isProxySet)。即使现在配置了代理,应用程序现在也会认为没有设置,因为函数返回 false。

setTimeout(function(){
    Java.perform(function (){
        console.log("[*] Script loaded")
 
        var Proxy = Java.use("<package-name>.<class-name>")
 
        Proxy.isProxySet.overload().implementation = function() {
            console.log("[*] isProxySet function invoked")
            return false
        }
    });
});

证书绑定

一些应用程序会实现 SSL Pinning,这将阻止应用程序将你的拦截证书作为有效证书接受,这意味着将不能监视应用程序和服务器之间的通信。

有关静态和动态禁用 SSL Pinning 的信息,请参考“测试网络通信”一章中的“绕过 SSL Pinning”。

引用

Tools

Android 反逆向防御

Root 检测测试(MSTG-RESILIENCE-1)

概述

在反逆向方面,root 检测的目标是让应用程序在一个 root 设备上运行更加困难,这反过来就阻碍了一些逆向工程师喜欢使用的工具和技术。和大多数其他防御措施一样,root 检测本身并不是很有效,但是在整个应用程序中分散实现多个 root 检测可以提高整个防篡改方案的有效性。

对于 Android,我们对“root 检测”的定义更广泛一些,包括自定义 ROM 检测,即确定该设备是现有的 Android 版本还是自定义版本。

常见的 Root 检测方法

在下一节中,我们将列出一些常见的 root 检测方法,你会在 OWASP 移动测试指南的 crackme 示例中发现其中一些方法的实现。

Root 检测也可以通过像 RootBeer 这样的库实现。

SafetyNet

SafetyNet 是一个 Android API,它提供一组服务并根据软件和硬件信息创建设备配置文件。然后将该配置文件与已通过 Android 兼容性测试的可接受设备模型列表进行比较。Google 建议将该功能用作“作为防滥用系统一部分的一个附加深度防御信号”。

SafetyNet 的确切工作方式没有得到很好的记录,并且可能随时改变。当你调用这个 API 时,SafetyNet 会下载一个二进制包,包含谷歌提供的设备验证代码,然后通过反射动态执行该代码。John Kozyrakis 在《SafetyNet:谷歌的Android篡改检测》中指出,SafetyNet 也尝试检测设备是否已 root,但具体是如何确定的还不清楚。

要使用 API,应用程序可以调用 SafetyNetApi.attest 方法(返回带有认证结果的 JWS 消息),然后检查以下字段:

  • ctsProfileMatch:如果为“ true”,则设备配置文件与Google列出的设备之一匹配。

  • basicIntegrity:如果为“ true”,则可能未对运行该应用程序的设备进行篡改。

  • nonces:使响应与请求相匹配。

  • timestampMs:检查自你发出请求并获得响应以来经过了多长时间,延迟的响应可能表明活动可疑。

  • apkPackageNameapkCertificateDigestSha256apkDigestSha256:提供有关 APK 的信息,该信息用于验证调用应用程序的身份,如果 API 无法可靠地确定 APK 信息,则缺少这些参数。

以下是示例认证结果:

{
  "nonce": "R2Rra24fVm5xa2Mg",
  "timestampMs": 9860437986543,
  "apkPackageName": "com.package.name.of.requesting.app",
  "apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the
                                  certificate used to sign requesting app"],
  "apkDigestSha256": "base64 encoded, SHA-256 hash of the app's APK",
  "ctsProfileMatch": true,
  "basicIntegrity": true,
}

ctsProfileMatch Vs basicIntegrity

SafetyNet 认证 API 最初提供了一个名为 basicIntegrity 的单一值,以帮助开发人员确定设备的完整性。随着API的发展,谷歌引入了一种新的、更严格的检查方法,其结果以一个名为 ctsProfileMatch 的值显示,该值允许开发人员更细致地评估运行其应用程序的设备。

从广义上讲,basicIntegrity 给你一个关于设备及其 API 的一般完整性的信号。许多 root 设备 basicIntegrity 会失败,模拟器、虚拟设备和有篡改迹象的设备(如 API hook)也是如此。

另一方面,ctsProfileMatch 会给你一个关于设备兼容性的更严格的信号,只有通过谷歌认证的未修改设备才能通过 ctsProfileMatchctsProfileMatch 会失败的设备包括:

  • basicIntegrity 失败的设备
  • 具有解锁的引导加载程序的设备
  • 具有自定义系统镜像(自定义 ROM)的设备
  • 制造商未申请或未通过 Google 认证的设备
  • 设备的系统映像直接从 Android 开源程序源文件编译
  • 系统映像作为测试版或开发者预览程序(包括Android测试版程序)的一部分分发的设备

使用 SafetyNetApi.attest 的建议

  • 使用加密安全的随机函数在服务器上创建一个大的(16字节或更长的)随机数,以使恶意用户无法重用成功的认证结果来替代不成功的结果

  • 仅当 ctsProfileMatch 值为真时,才信任 APK 信息(apkPackageName, apkCertificateDigestSha256 和 apkDigestSha256)。

  • 应该使用安全连接将整个 JWS 响应发送到服务器以进行验证。不建议在应用程序中直接执行验证,因为在这种情况下不能保证验证逻辑本身没有被修改。

  • verify 方法只验证 JWS 消息是否由 SafetyNet 签名,它不能验证判决的有效载荷是否符合你的预期。尽管这个服务看起来很有用,但它只是为测试目的而设计的,并且它有非常严格的使用配额,每个项目每天只有10000个请求,不会根据请求增加。因此,您应该参考 SafetyNet 验证示例,并在服务器上以不依赖于 Google 服务器的方式实现数字签名验证逻辑。

  • 当发出认证请求时,SafetyNet 认证 API 为你提供一个设备状态的快照。一个成功的认证并不一定意味着该设备在过去就已经通过了认证,或者在将来就会通过认证,建议计划一种策略以进行满足用例所需的最少数量的认证。

  • 以防止无意中达到你的 SafetyNetApi.attest配额并获得 attestation 错误,你应该编译一个系统来监视你对 API 的使用,并在达到配额之前发出警告,这样你可以提高配额。你还应该准备好处理由于超出配额而导致的认证失败,并避免在这种情况下阻塞所有用户。如果你接近于达到配额,或者预期短期的峰值可能会导致你超过配额,那么可以提交此表单,请求短期或长期增加 API 密钥的配额,这个过程以及额外的配额都是免费的。

请遵循此清单,以确保你已完成将 SafetyNetApi.attest API 集成到应用程序中所需的每个步骤。

程序检测

文件存在检查

也许最广泛使用的程序检测方法是检查通常会在 root 设备上发现的文件,例如常见的 root 应用程序的包文件及其相关文件和目录,包括以下内容:

/system/app/Superuser.apk
/system/etc/init.d/99SuperSUDaemon
/dev/com.koushikdutta.superuser.daemon/
/system/xbin/daemonsu

检测代码还经常查找在设备 root 后安装的二进制文件,这些搜索包括检查 busybox 和试图打开在不同的位置 su 二进制文件:

/sbin/su 
/system/bin/su 
/system/bin/failsafe/su 
/system/xbin/su 
/system/xbin/busybox 
/system/sd/xbin/su 
/data/local/su 
/data/local/xbin/su 
/data/local/bin/su

检查 su 是否在 PATH 中也可以起作用:

public static boolean checkRoot(){
    for(String pathDir : System.getenv("PATH").split(":")){
        if(new File(pathDir, "su").exists()) {
            return true;
        }
    }
    return false;
}

可以很容易地在 Java 和原生代码中实现文件检查。下面的 JNI 示例(改编自 rootinspector)使用 stat 系统调用检索关于文件的信息,如果该文件存在,则返回“1”。

jboolean Java_com_example_statfile(JNIEnv * env, jobject this, jstring filepath) {
  jboolean fileExists = 0;
  jboolean isCopy;
  const char * path = (*env)->GetStringUTFChars(env, filepath, &isCopy);
  struct stat fileattrib;
  if (stat(path, &fileattrib) < 0) {
    __android_log_print(ANDROID_LOG_DEBUG, DEBUG_TAG, "NATIVE: stat error: [%s]", strerror(errno));
  } else
  {
    __android_log_print(ANDROID_LOG_DEBUG, DEBUG_TAG, "NATIVE: stat success, access perms: [%d]", fileattrib.st_mode);
    return 1;
  }
 
  return 0;
}
执行 su 和其他命令

确定 su 是否存在的另一种方法是尝试通过 Runtime.getRuntime.exec 方法执行它。如果 su 不在路径上,就会抛出 IOException。同样的方法可以用来检查在 root 设备上经常发现的其他程序,比如 busybox 和通常指向它的符号链接。

检查运行的进程

supersu 是目前最流行的 root 工具,它运行一个名为 daemonsu 的身份验证守护进程,因此这个进程的出现是 root 设备的另一个标志。可以使用 ActivityManager.getRunningAppProcesses 和 manager.getRunningServices API ,ps 命令和浏览 /proc 目录枚举正在运行的进程。下面是一个在 rootinspector 中实现的例子:

public boolean checkRunningProcesses() {
 
  boolean returnValue = false;
 
  // Get currently running application processes
  List<RunningServiceInfo> list = manager.getRunningServices(300);
 
  if(list != null){
    String tempName;
    for(int i=0;i<list.size();++i){
      tempName = list.get(i).process;
 
      if(tempName.contains("supersu") || tempName.contains("superuser")){
        returnValue = true;
      }
    }
  }
  return returnValue;
}
检查安装的应用程序包

你可以使用 Android 软件包管理器来获取已安装软件包的列表,以下包的名称属于流行的 root 工具:

com.thirdparty.superuser
eu.chainfire.supersu
com.noshufou.android.su
com.koushikdutta.superuser
com.zachspong.temprootremovejb
com.ramdroid.appquarantine
com.topjohnwu.magisk

检查可写的分区和系统目录

系统目录上不寻常的权限表明可能是自定义的或 root 的设备。系统和数据目录通常是挂载只读的,但当设备是 root 时,有时会发现它们挂载读写的。寻找这些挂载“rw”标志的文件系统,或者尝试在数据目录中创建文件。

检查定制的 Android 版本

检查测试版本和自定义 ROM 的标志也很有帮助。一种方法是检查 BUILD 标签中的 test-key,它通常表示自定义的 Android 镜像,检查 BUILD 标签如下

private boolean isTestKeyBuild()
{
String str = Build.TAGS;
if ((str != null) && (str.contains("test-keys")));
for (int i = 1; ; i = 0)
  return i;
}

缺少谷歌 Over-The-Air(OTA)证书是自定义 ROM 的另一个标志:在现有的 Android 版本中,OTA 更新 Google 的公开证书。

绕过 Root 检测

使用 jdb、DDMS、strace 或内核模块运行执行跟踪,以了解应用程序正在做什么。你通常会看到与操作系统的各种可疑交互,比如打开 su 以读取和获取进程列表,这些交互肯定是 root 检测的信号。识别并禁用 root 检测机制,如果你正在执行黑盒弹性评估,那么禁用 root 检测机制是第一步。

为了绕过这些检查,你可以使用几种技术,其中大部分在“逆向工程和篡改”一章中介绍:

  • 重命名二进制文件。例如,在某些情况下,简单地重命名 su 二进制文件就足以阻止 root 检测(但是不要破坏你的环境!)
  • 卸载 /proc 以防止读取进程列表,有时,/proc 的不可用足以绕过这些检查。
  • 使用 Frida 或 Xposed 在 Java 和原生层上 hook API,这将隐藏文件和进程,隐藏文件的内容,并返回应用程序请求的各种伪造值。
  • 使用内核模块 hook 底层 API。
  • 给程序打补丁,删除检查。

有效性评估

检查 root 检测机制,包括以下标准:

  • 多种检测方法分散在整个应用程序中(而不是把所有的东西都放在一个方法中)。

  • root 检测机制在多个 API 层上操作(Java API、原生库函数、汇编程序/系统调用)。

  • 这些机制在某种程度上是原创的(不是从 StackOverflow 或其他源复制和粘贴的)。

为 root 检测机制开发绕过方法,并回答以下问题:

  • 使用标准工具(如 RootCloak )可以轻松绕过这些机制吗?

  • 静态/动态分析对于处理 root 检测是必要的吗?

  • 你需要编写自定义代码吗?

  • 成功绕过这些机制需要多长时间?

  • 你认为绕过这些机制的难度如何?

如果根检测缺失或太容易被绕过,请按照上面列出的有效性标准提出建议。这些建议可能包括更多的检测机制,以及更好地将现有机制与其他防御机制集成。

反调试检测测试(MSTG-RESILIENCE-2)

概述

调试是分析应用程序运行时行为的一种非常有效的方法。它允许逆向工程师单步执行代码、在任意点停止应用程序执行、检查变量的状态、读取和修改内存等等。

反调试特性可以是预防式的,也可以是反应式的。顾名思义,预防式反调试首先阻止调试器进行附加,反应式反调试包括检测调试器并以某种方式对它们做出反应(例如,终止应用程序或触发隐藏行为)。“越多越好”的规则适用于:为了最大化效率,防御者结合了多种预防和检测方法,在不同的 API 层上运行,并且分布在整个应用程序中。

正如在“逆向工程和篡改”一章中提到的,我们在 Android 上必须处理两种调试协议:我们可以使用 JDWP 在 Java 层进行调试,也可以使用基于 ptrace 的调试器在原生层进行调试。一个好的反调试方案应该能防御这两种类型的调试。

JDWP 反调试

在“逆向工程和篡改”一章中,我们讨论了 JDWP,这是调试器和 Java 虚拟机之间的通信协议。我们展示了,通过给应用程序的清单文件打补丁和更改 ro. debugging 属性来启用所有应用程序的调试是很容易的。让我们看看开发人员检测和禁用 JDWP 调试器所做的一些事情。

检测 ApplicationInfo 中的可调试标志

我们已经遇到过 android:debuggable 属性。Android 清单文件中的这个标志决定是否为应用程序启动 JDWP 线程。它的值可以通过应用程序的 ApplicationInfo 对象用程序代码来确定。如果设置了标志,则说明清单文件已被篡改并允许调试。

public static boolean isDebuggable(Context context){
 
    return ((context.getApplicationContext().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0);
 
}

isDebuggerConnected

尽管这对于逆向工程师而言可能是显而易见的,你可以使用 android.os.Debug 类中的 isDebuggerConnected 确定是否连接了调试器。

public static boolean detectDebugger() {
    return Debug.isDebuggerConnected();
}

通过访问 DvmGlobals 的全局结构,可以通过原生代码调用相同的 API。

JNIEXPORT jboolean JNICALL Java_com_test_debugging_DebuggerConnectedJNI(JNIenv * env, jobject obj) {
    if (gDvm.debuggerConnected || gDvm.debuggerActive)
        return JNI_TRUE;
    return JNI_FALSE;
}

时间检测

Debug.threadCpuTimeNanos 表示当前线程已执行代码的时间,由于调试会减慢进程的执行速度,因此你可以使用执行时间的差异来猜测是否已连接调试器

static boolean detect_threadCpuTimeNanos(){
  long start = Debug.threadCpuTimeNanos();
 
  for(int i=0; i<1000000; ++i)
    continue;
 
  long stop = Debug.threadCpuTimeNanos();
 
  if(stop - start < 10000000) {
    return false;
  }
  else {
    return true;
  }
}

处理与 JDWP 相关的数据结构

在 Dalvik 中,可以通过 DvmGlobals 结构访问全局虚拟机状态,全局变量 gDvm 有一个指向这个结构的指针。DvmGlobals 包含各种变量和指针,这些变量和指针对于 JDWP 调试非常重要,可以被篡改。

struct DvmGlobals {
    /*
     * Some options that could be worth tampering with :)
     */
 
    bool        jdwpAllowed;        // debugging allowed for this process?
    bool        jdwpConfigured;     // has debugging info been provided?
    JdwpTransportType jdwpTransport;
    bool        jdwpServer;
    char*       jdwpHost;
    int         jdwpPort;
    bool        jdwpSuspend;
 
    Thread*     threadList;
 
    bool        nativeDebuggerActive;
    bool        debuggerConnected;      /* debugger or DDMS is connected */
    bool        debuggerActive;         /* debugger is making requests */
    JdwpState*  jdwpState;
 
};

例如,将 gDvm.methDalvikDdmcServer_dispatch 函数指针设置为 NULL 会使 JDWP 线程崩溃

JNIEXPORT jboolean JNICALL Java_poc_c_crashOnInit ( JNIEnv* env , jobject ) {
  gDvm.methDalvikDdmcServer_dispatch = NULL;
}

即使 gDvm 变量不可用,也可以通过使用 ART 中的类似技术禁用调试。ART运行时将与 jdwp 相关的类的一些虚函数表导出为全局符号(在 C++ 中,虚函数表是包含指向类方法的指针的表)。这包括类 JdwpSocketState 和 JdwpAdbState 的虚函数表,它们分别通过网络套接字和 ADB 处理 JDWP 连接。你可以通过重写关联虚函数表中的方法指针来操纵调试运行时的行为

重写方法指针的一种方法是用 JdwpAdbState:: Shutdown 的地址重写函数 jdwpAdbState:: processIncoming 的地址,这将导致调试器立即断开连接。

#include <jni.h>
#include <string>
#include <android/log.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include <jdwp/jdwp.h>
 
#define log(FMT, ...) __android_log_print(ANDROID_LOG_VERBOSE, "JDWPFun", FMT, ##__VA_ARGS__)
 
// Vtable structure. Just to make messing around with it more intuitive
 
struct VT_JdwpAdbState {
    unsigned long x;
    unsigned long y;
    void * JdwpSocketState_destructor;
    void * _JdwpSocketState_destructor;
    void * Accept;
    void * showmanyc;
    void * ShutDown;
    void * ProcessIncoming;
};
 
extern "C"
 
JNIEXPORT void JNICALL Java_sg_vantagepoint_jdwptest_MainActivity_JDWPfun(
        JNIEnv *env,
        jobject /* this */) {
 
    void* lib = dlopen("libart.so", RTLD_NOW);
 
    if (lib == NULL) {
        log("Error loading libart.so");
        dlerror();
    }else{
 
        struct VT_JdwpAdbState *vtable = ( struct VT_JdwpAdbState *)dlsym(lib, "_ZTVN3art4JDWP12JdwpAdbStateE");
 
        if (vtable == 0) {
            log("Couldn't resolve symbol '_ZTVN3art4JDWP12JdwpAdbStateE'.\n");
        }else {
 
            log("Vtable for JdwpAdbState at: %08x\n", vtable);
 
            // Let the fun begin!
 
            unsigned long pagesize = sysconf(_SC_PAGE_SIZE);
            unsigned long page = (unsigned long)vtable & ~(pagesize-1);
 
            mprotect((void *)page, pagesize, PROT_READ | PROT_WRITE);
 
            vtable->ProcessIncoming = vtable->ShutDown;
 
            // Reset permissions & flush cache
 
            mprotect((void *)page, pagesize, PROT_READ);
 
        }
    }
}

传统的反调试

在 Linux 上,ptrace 系统调用用于观察和控制进程(tracee)的执行,并检查和更改该进程的内存和寄存器。ptrace 是在原生代码中实现系统调用跟踪和断点调试的主要方式。大多数 JDWP 反调试技巧(对于基于计时器的检查来说可能是安全的)不会捕获基于 ptrace 的经典调试器,因此,许多 Android 反调试技巧包括 ptrace,通常利用了一次只能连接到进程的一个调试器这一事实。

检查 TracerPid

当你调试应用程序并在原生代码上设置断点时,Android Studio 将把所需的文件复制到目标设备并启动 lldb-server,它将使用 ptrace 附加到进程。从现在开始,如果你检查被调试进程的状态文件/proc//status 或 /proc/self/status),你将看到“TracerPid”字段的值与0不同,这是调试的标志。

记住,这只适用于原生代码。如果你正在调试只使用 Java/ kotlin 的应用程序,那么“TracerPid”字段的值应该是0。

这种技术通常应用于 C 写的 JNI 原生库中,如 Google 的 gperftools(Google 性能工具)堆检查器实现的 IsDebuggerAttached 所示。但是,如果你希望将此检查作为 Java/Kotlin 代码的一部分,你可以参考 Tim Strazzere 的反模拟器项目中的 hasTracerPid 方法的 Java 实现。

当你尝试自己实现这种方法时,可以使用 ADB 手动检查 TracerPid 的值。下面的清单使用谷歌的 NDK 示例应用 hello-jni (com.example.hellojni) 在附加 Android Studio 的调试器后执行检查:

$ adb shell ps -A | grep com.example.hellojni
u0_a271      11657   573 4302108  50600 ptrace_stop         0 t com.example.hellojni
$ adb shell cat /proc/11657/status | grep -e "^TracerPid:" | sed "s/^TracerPid:\t//"
TracerPid:      11839
$ adb shell ps -A | grep 11839
u0_a271      11839 11837   14024   4548 poll_schedule_timeout 0 S lldb-server

你可以看到 com.example.hellojni(PID = 11657)的状态文件如何包含11839的 TracerPID,我们可以将其标识为 lldb-server 进程。

使用 Fork 和 petrace

你可以通过类似于以下简单示例代码,将子进程附加到父进程作为调试器来防止进程调试:

void fork_and_attach()
{
  int pid = fork();
 
  if (pid == 0)
    {
      int ppid = getppid();
 
      if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0)
        {
          waitpid(ppid, NULL, 0);
 
          /* Continue the parent process */
          ptrace(PTRACE_CONT, NULL, NULL);
        }
    }
}

附加了子进程后,进一步附加到父进程的尝试将会失败。我们可以通过将代码编译成 JNI 函数并将其打包到我们在设备上运行的应用程序中来验证这一点。

root@android:/ # ps | grep -i anti
u0_a151   18190 201   1535844 54908 ffffffff b6e0f124 S sg.vantagepoint.antidebug
u0_a151   18224 18190 1495180 35824 c019a3ac b6e0ee5c S sg.vantagepoint.antidebug

使用 gdbserver 试图附加到父进程会失败并有一个错误:

root@android:/ # ps | grep -i anti
u0_a151   18190 201   1535844 54908 ffffffff b6e0f124 S sg.vantagepoint.antidebug
u0_a151   18224 18190 1495180 35824 c019a3ac b6e0ee5c S sg.vantagepoint.antidebug

但是,你可以通过杀死子进程并释放父进程而不被 trace 来轻松地绕过这个失败。因此,你通常会发现更复杂的方案,包括多个进程和线程以及某种形式的监视来阻止篡改。常用的方法包括

  • fork 多个进程,相互跟踪
  • 跟踪运行过程以确保子进程活着
  • 监视 /proc 文件系统中的值,例如 /proc/pid/status 中的 TracerPID。

让我们看看对上述方法的一个简单改进。在初始 fork 之后,我们在父进程中启动一个额外的线程,该线程持续监视子进程的状态。根据应用程序是在 debug 模式还是 release 模式(由清单文件中的 android:debuggable 标志表示),子进程应该做以下事情之一:

  • 在 release 模式中:对 ptrace 的调用失败,子进程立即崩溃并出现段错误(退出代码11)。

  • 在 debug 模式下:对 ptrace 的调用有效,并且子进程应该一直运行,因此,对 waitpid(child_pid) 的调用应该永远不会返回。 如果返回了,则说明有些可疑,将杀死整个流程组。

下面是使用 JNI 函数实现的完整的改进代码:

#include <jni.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <pthread.h>
 
static int child_pid;
 
void *monitor_pid() {
 
    int status;
 
    waitpid(child_pid, &status, 0);
 
    /* Child status should never change. */
 
    _exit(0); // Commit seppuku
 
}
 
void anti_debug() {
 
    child_pid = fork();
 
    if (child_pid == 0)
    {
        int ppid = getppid();
        int status;
 
        if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0)
        {
            waitpid(ppid, &status, 0);
 
            ptrace(PTRACE_CONT, ppid, NULL, NULL);
 
            while (waitpid(ppid, &status, 0)) {
 
                if (WIFSTOPPED(status)) {
                    ptrace(PTRACE_CONT, ppid, NULL, NULL);
                } else {
                    // Process has exited
                    _exit(0);
                }
            }
        }
 
    } else {
        pthread_t t;
 
        /* Start the monitoring thread */
        pthread_create(&t, NULL, monitor_pid, (void *)NULL);
    }
}
 
JNIEXPORT void JNICALL
Java_sg_vantagepoint_antidebug_MainActivity_antidebug(JNIEnv *env, jobject instance) {
 
    anti_debug();
}

同样,我们把它打包到一个 Android 应用程序中,看看它是否能工作。与前面一样,当我们运行应用程序的调试版本时,会出现两个进程。

root@android:/ # ps | grep -I anti-debug
u0_a152   20267 201   1552508 56796 ffffffff b6e0f124 S sg.vantagepoint.anti-debug
u0_a152   20301 20267 1495192 33980 c019a3ac b6e0ee5c S sg.vantagepoint.anti-debug

但是,如果我们在此时终止子进程,父进程也会退出:

root@android:/ # kill -9 20301
130|root@hammerhead:/ # cd /data/local/tmp
root@android:/ # ./gdbserver --attach localhost:12345 20267
gdbserver: unable to open /proc file '/proc/20267/status'
Cannot attach to lwp 20267: No such file or directory (2)
Exiting

要绕过这个检测,我们必须稍微修改应用程序的行为(最简单的方法是用 NOP 修改对 _exit 的调用和在 libc.so 中 hook 函数 _exit)。在这一点上,我们已经进入了众所周知的“军备竞赛”:实现更复杂的防御形式和绕过它们。

绕过调试器检测

没有通用的方法可以绕过反调试:最好的方法取决于用来防止或检测调试的机制和整体保护方案中其他的防御措施。例如,如果没有完整性检查,或者你已经停用了它们,修改应用程序可能是最简单的方法。在其他情况下,一个 hook 框架或内核模块可能是更可取的。以下方法描述了绕过调试器检测的不同方法:

  • 修改反调试功能:通过简单地用 NOP 指令覆盖它,来禁用不希望的行为。注意,如果反调试机制设计良好,可能需要更复杂的修改。

  • 使用 Frida 或 Xposed 在 Java 和原生层 hook API:操作函数的返回值,如 isDebuggable 和 isDebuggerConnected 函数,来隐藏调试器。

  • 改变环境:Android是一个开放的环境。如果其他方法都不起作用,你可以修改操作系统来推翻开发人员在设计反调试技巧时所做的假设。

绕过案例:UnCrackable App for Android Level 2

在处理混淆的应用程序时,你经常会发现开发人员有意隐藏原生库中的数据和功能。 在“ UnCrackable App for Android”的 Level 2 中,可以找到一个示例。

乍一看,代码看起来像之前的挑战,名为 CodeCheck 的类负责验证用户输入的代码,真正检查似乎在 bar 方法中进行,该方法被声明为原生方法。

package sg.vantagepoint.uncrackable2;
 
public class CodeCheck {
    public CodeCheck() {
        super();
    }
 
    public boolean a(String arg2) {
        return this.bar(arg2.getBytes());
    }
 
    private native boolean bar(byte[] arg1) {
    }
}
 
    static {
        System.loadLibrary("foo");
    }

请查看 GitHub 中针对 Android Crackme Level 2 提出的不同解决方案

有效性评估

检查反调试机制,包括以下标准:

  • 附加 jdb 和基于 ptrace 的调试器失败或导致应用终止或不起作用。

  • 多种检测方法分散在应用程序的源代码中(而不是所有的检测方法或功能都集中在一个方法或功能中)。

  • 反调试防御在多个 API 层(Java、原生库函数、汇编程序/系统调用)上操作。

  • 这些机制在某种程度上是原创的(而不是从 StackOverflow 或其他源复制和粘贴的)。

努力绕过反调试防御,并回答以下问题:

  • 这些机制可以被简单地绕过吗(例如,通过 hook 单个 API 函数)?

  • 通过静态和动态分析识别反调试代码有多难?

  • 你是否需要编写自定义代码来禁用防御?你需要多少时间?

  • 你对绕过这些机制的难度的主观评价是什么?

如果反调试机制缺失或太容易被绕过,请按照上述有效性标准提出建议,这些建议可能包括添加更多的检测机制,以及更好地将现有机制与其他防御机制集成。

文件完整性检测测试(MSTG-RESILIENCE-3)

概述

关于文件完整性有两个主题:

  1. 代码完整性检查:在“Android上的篡改和逆向工程”一章中,我们讨论了 Android 的 APK 代码签名验证。我们还发现,逆向工程师可以通过重新打包和重新签名应用程序,轻松绕过这个检查。为了让这个绕过过程更加复杂,可以通过对应用程序字节码、原生库和重要数据文件进行 CRC 校验来增强保护方案。这些检查可以在 Java 层和原生层上实现,思路是在适当的地方进行额外的控制,以让应用程序只能在未修改的状态下正确运行,即使代码签名是有效的。

  2. 文件存储完整性检查:应用程序存储在 SD 卡或公共存储中的文件的完整性以及存储在 SharedPreferences 中的键值对的完整性应该受到保护。

示例实现-存储

当为存储本身提供完整性时,你可以通过给定的键值对(对于 Android SharedPreferences)或者通过文件系统提供的完整文件创建 HMAC。

在使用 HMAC 时,可以使用 bouncy castle 实现或用 AndroidKeyStore 对给定内容进行 HMAC

使用 BouncyCastle 生成 HMAC 时,请完成以下步骤:

  1. 确保 BouncyCastle 或 SpongyCastle 注册为安全提供商。
  2. 使用密钥(可以存储在密钥存储库中)初始化HMAC。
  3. 获取需要 HMAC 的内容的字节数组。
  4. 在 HMAC 上使用字节码调用 doFinal
  5. 将 HMAC 附加到步骤3中获得的字节数组中。
  6. 存储步骤5的结果。

使用 BouncyCastle 验证 HMAC 时,请完成以下步骤:

  1. 确保 BouncyCastle 或 SpongyCastle 注册为安全提供商。
  2. 将消息和 HMAC 字节提取为单独的数组。
  3. 重复生成 HMAC 过程的步骤1-4。
  4. 将提取的 HMAC 字节与步骤3的结果进行比较。

在基于 Android 密钥存储库生成 HMAC 时,最好只在 Android 6.0 (API 等级23)或更高的版本中这样做。

以下是一个方便的 HMAC 实现,没有使用 `AndroidKeyStore`:

public enum HMACWrapper {
    HMAC_512("HMac-SHA512"), //please note that this is the spec for the BC provider
    HMAC_256("HMac-SHA256");
 
    private final String algorithm;
 
    private HMACWrapper(final String algorithm) {
        this.algorithm = algorithm;
    }
 
    public Mac createHMAC(final SecretKey key) {
        try {
            Mac e = Mac.getInstance(this.algorithm, "BC");
            SecretKeySpec secret = new SecretKeySpec(key.getKey().getEncoded(), this.algorithm);
            e.init(secret);
            return e;
        } catch (NoSuchProviderException | InvalidKeyException | NoSuchAlgorithmException e) {
            //handle them
        }
    }
 
    public byte[] hmac(byte[] message, SecretKey key) {
        Mac mac = this.createHMAC(key);
        return mac.doFinal(message);
    }
 
    public boolean verify(byte[] messageWithHMAC, SecretKey key) {
        Mac mac = this.createHMAC(key);
        byte[] checksum = extractChecksum(messageWithHMAC, mac.getMacLength());
        byte[] message = extractMessage(messageWithHMAC, mac.getMacLength());
        byte[] calculatedChecksum = this.hmac(message, key);
        int diff = checksum.length ^ calculatedChecksum.length;
 
        for (int i = 0; i < checksum.length && i < calculatedChecksum.length; ++i) {
            diff |= checksum[i] ^ calculatedChecksum[i];
        }
 
        return diff == 0;
    }
 
    public byte[] extractMessage(byte[] messageWithHMAC) {
        Mac hmac = this.createHMAC(SecretKey.newKey());
        return extractMessage(messageWithHMAC, hmac.getMacLength());
    }
 
    private static byte[] extractMessage(byte[] body, int checksumLength) {
        if (body.length >= checksumLength) {
            byte[] message = new byte[body.length - checksumLength];
            System.arraycopy(body, 0, message, 0, message.length);
            return message;
        } else {
            return new byte[0];
        }
    }
 
    private static byte[] extractChecksum(byte[] body, int checksumLength) {
        if (body.length >= checksumLength) {
            byte[] checksum = new byte[checksumLength];
            System.arraycopy(body, body.length - checksumLength, checksum, 0, checksumLength);
            return checksum;
        } else {
            return new byte[0];
        }
    }
 
    static {
        Security.addProvider(new BouncyCastleProvider());
    }
}

提供完整性的另一种方法是对获得的字节数组进行签名,并将签名添加到原始字节数组中。

绕过文件完整性检查

绕过应用程序源代码的完整性检查

  1. 修改反调试功能,通过使用 NOP 指令覆盖相关的字节码或原生代码来禁用不希望的行为。

  2. 使用 Frida 或 Xposed 在 Java 和原生层上 hook 文件系统 API。返回原始文件的句柄,而不是修改后的文件。

  3. 使用内核模块拦截与文件相关的系统调用,当进程尝试打开修改后的文件时,返回文件未修改版本的文件描述符。

关于修改、代码注入和内核模块的例子,请参考“Android上的篡改和逆向工程”一章。

绕过存储完整性检查

  1. 如“设备绑定测试”一节所述,从设备检索数据。
  2. 修改检索到的数据,然后将其放回存储。

有效性评估

评估应用程序源代码的完整性检查

以未修改的状态运行应用程序,并确保一切正常。对应用程序包中的 class .dex 和任意 .so 库进行简单的修改。如“基本安全测试”章节所述,重新打包并重新签名应用程序,然后运行应。应用程序应该检测到修改并以某种方式做出响应,至少应该警告用户或终止。努力绕过防御,并回答以下问题:

  • 这些机制可以被简单地绕过吗(例如,通过 hook 单个 API 函数)?
  • 通过静态和动态分析识别反调试代码有多难?
  • 你是否需要编写自定义代码来禁用防御?你需要多少时间?
  • 你认为绕过这些机制的难度如何?

评估存储完整性检查

应用与应用程序源代码完整性检查类似的方法,回答以下问题:

  • 可以简单地绕过这些机制吗(例如,通过更改文件或键值的内容)?

  • 获得 HMAC 密钥或非对称私钥有多难?

  • 你是否需要编写自定义代码来禁用防御?你需要多少时间?

  • 你认为绕过这些机制的难度如何?

逆向工程工具检测测试(MSTG-RESILIENCE-4)

概述

逆向工程师常用的工具、框架和应用程序由可能代表着对应用的一次逆向尝试。这些工具中的一些只能在 root 设备上运行,而其他一些则可以使应用程序进入调试模式或依靠在手机上启动一个后台服务。因此,应用程序可以通过不同的方式来检测和应对逆向工程攻击,例如终止自身运行。

检测方法

通过查找相关的应用程序包、文件、进程或其他特定的修改工具,可以检测到以未修改的形式安装的流行的逆向工程工具。在下面的示例中,我们将讨论检测 Frida 工具框架的不同方法,该框架在本指南中得到了广泛使用。其他工具,如 Substrate 和 Xposed,也可以被类似地检测出来。注意,DBI/注入/hook 工具通常可以通过运行时完整性检查(下面将讨论这些检查)隐式检测到。

例如,在 root 设备上的默认配置中,Frida作为 frida-server 在设备上运行。当你显式地附加到一个目标应用程序时(例如通过 Frida -trace 或 Frida REPL),Frida 会将 frida 代理注入到应用程序的内存中。因此,你可能会在附加到应用程序之后(而不是之前)发现它。如果你检查 /proc//maps,会发现 frida-agent 作为frida-agent-64.so 存在:

bullhead:/ # cat /proc/18370/maps | grep -i frida
71b6bd6000-71b7d62000 r-xp  /data/local/tmp/re.frida.server/frida-agent-64.so
71b7d7f000-71b7e06000 r--p  /data/local/tmp/re.frida.server/frida-agent-64.so
71b7e06000-71b7e28000 rw-p  /data/local/tmp/re.frida.server/frida-agent-64.so

另一种方法(也适用于非 root 设备)包括将 frida-gadget 嵌入到 APK 中,并强制应用程序将其作为原生库之一加载。如果在启动应用程序后检查应用程序内存映射(不需要显式地附加到它),你会发现嵌入的 frida-gadget 作为 libfrida-gadget.so 存在。

bullhead:/ # cat /proc/18370/maps | grep -i frida
 
71b865a000-71b97f1000 r-xp  /data/app/sg.vp.owasp_mobile.omtg_android-.../lib/arm64/libfrida-gadget.so
71b9802000-71b988a000 r--p  /data/app/sg.vp.owasp_mobile.omtg_android-.../lib/arm64/libfrida-gadget.so
71b988a000-71b98ac000 rw-p  /data/app/sg.vp.owasp_mobile.omtg_android-.../lib/arm64/libfrida-gadget.so

查看 Frida 留下的这两个痕迹,你可能已经认为检测它们是一项微不足道的任务。实际上,绕过这样的检测是很简单的事情,但事情可能会变得更加复杂。下表简要介绍了一些典型的 Frida 检测方法,并简要讨论了它们的有效性。

以下的一些检测方法在 Berdhard Mueller的文章《多种特征检测 Frida》”中被提出,请参阅它以获得更多细节和示例代码片段。

方法 描述 讨论
检查应用程序签名 为了将 frida-gadget 嵌入到 APK 中,需要重新打包并签名。你可以在应用程序启动时检查 APK 的签名(例如GET_SIGNING_CERTIFICATES,从 API 等级28开始),并将其与你在 APK 中固定的签名进行比较。 可以很简单地绕过,比如修改 APK 或者 hook 系统调用
在环境中检测相关工件 工件可以是包文件、二进制文件、库、进程和临时文件,对于Frida,这可能是运行在目标的 root 系统中的 frida-server(负责通过 TCP 公开 Frida 的守护进程)。检查正在运行的服务(getRunningServices)和搜索名为“frida-server”的进程(ps),你还可以浏览已加载的库列表并检查可疑的库(例如,那些名字中包含“frida”的库)。 从 Android 7.0(API 等级24),检查运行的服务和进程不会显示像 frida-server 这样的守护进程,因为不是由应用本身启动的。即使有可能,只要重命名相应的Frida 工具(Frida -server/ Frida -gadget/ Frida -agent),绕过检测也很容易。
检查打开的 TCP 端口 默认情况下,frida-server 进程绑定到 TCP 端口27042,检查此端口是否打开是检测守护进程的另一种方法。 这个方法在默认模式下会检测到 frida-server,但是监听端口可以通过命令行参数更改,所以绕过很简单了。
检查响应 D-Bus Auth 的端口 frida-server 使用 D-Bus 协议进行通信,因此可以期望它响应 D-Bus AUTH,向每一个开放的端口发送 D-Bus AUTH 消息并检查回复,frida-server 会把自己显示出来。 这是一种相当健壮的检测 frida-server 的方法,但是 Frida 提供了不需要 frida-server 的其他操作模式。
扫描进程内存查找相关工件 扫描内存以查找 Frida 库中的工件,例如所有版本的 frida-gadget 和 frida-agent 中的字符串“LIBFRIDA”。例如,使用 Runtime.getRuntime().exec 遍历 /proc/self/maps 或 /proc/<pid>/maps(取决于 Android 版本)中列出的内存映射来搜索字符串。 这种方法更有效一些,而且仅用 Frida 是很难绕过的,特别是在添加了一些混淆和多个工件被扫描的情况下。然而,所选择的工件可能会在 Frida 二进制文件中被修改,在 Berdhard Mueller 的 GitHub 上可以找到源代码。

请记住,这个表格远非详尽无遗。我们可以开始讨论命名管道(frida-server 用于外部通信)、检测跳板(在函数开头处插入的间接跳转向量),这将有助于检测 Substrate 或 Frida 的拦截器,但是也可能无效,例如对 Frida 的 Stalker 以及许多其他或多或少有效的检测方法。它们中的每一个都将取决于你是否正在使用 root 设备、特定版本的 root 方法或工具本身的版本。最后,这是猫捉老鼠游戏的一部分,保护在不受信任的环境(运行在用户设备中的应用程序)上处理的数据。

需要注意的是,这些控制只会增加逆向工程过程的复杂性。如果使用,最好的方法是巧妙地组合这些控制,而不是单独使用它们。然而,它们都不能保证 100% 的有效性,因为逆向工程师总是能够完全访问设备,因此总是会赢!您还必须考虑到,将一些控制集成到应用程序中可能会增加应用程序的复杂性,甚至会影响其性能。

有效性评估

在测试设备中安装各种逆向工程工具和框架然后启动应用程序。至少包括:Frida, Xposed,
Substrate for Android, Drozer, RootCloak, Android SSL Trust Killer。

应用程序应该以某种方式响应这些工具的存在。例如:

  • 提醒用户并要求承担责任。
  • 优雅地停止应用程序执行。
  • 安全清除存储在设备上的任何敏感数据。
  • 向后端服务器报告,例如进行欺诈的检测。

接下来,绕过逆向工程工具的检测,回答以下问题:

  • 这些机制可以被简单地绕过吗(例如,通过 hook 单个 API 函数)?
  • 通过静态和动态分析识别反逆向工程代码有多难?
  • 您是否需要编写自定义代码来禁用防御?你需要多少时间?
  • 你认为绕过这些机制的难度如何?

以下步骤将指导你绕过逆向工程工具检测:

  1. 修改反逆向工程功能,通过使用 NOP 指令覆盖相关的字节码或原生代码来禁用不希望的行为。

  2. 使用 Frida 或 Xposed 在 Java 和原生层上 hook 文件系统 API,返回原始文件的句柄,而不是修改后的文件。

  3. 使用内核模块拦截与文件相关的系统调用,当进程尝试打开修改后的文件时,返回文件未修改版本的文件描述符。

关于修改、代码注入和内核模块的例子,请参考“Android上的篡改和逆向工程”一章。

模拟器检测测试(MSTG-RESILIENCE-5)

概述

在防逆向方面,模拟器检测的目的是增加在模拟器设备上运行应用程序的难度,这阻碍了逆向工程师使用某些喜欢的工具和技术。这种难度的增加强制逆向工程师去对抗模拟器检测或使用真实的物理设备,从而限制了大规模设备分析所需的访问。

模拟器检测示例

有几个指标可以表明设备是模拟器。尽管所有这些 API 调用都可以 hook,但这些指标提供了第一道防线。

第一组指标在`build.prop`文件中。

API Method          Value           Meaning
Build.ABI           armeabi         possibly emulator
BUILD.ABI2          unknown         possibly emulator
Build.BOARD         unknown         emulator
Build.Brand         generic         emulator
Build.DEVICE        generic         emulator
Build.FINGERPRINT   generic         emulator
Build.Hardware      goldfish        emulator
Build.Host          android-test    possibly emulator
Build.ID            FRF91           emulator
Build.MANUFACTURER  unknown         emulator
Build.MODEL         sdk             emulator
Build.PRODUCT       sdk             emulator
Build.RADIO         unknown         possibly emulator
Build.SERIAL        null            emulator
Build.USER          android-build   emulator

你可以在 root 的 Android 设备上编辑文件`build.prop`,也可以在从源代码编译 AOSP 时对其进行修改,两种技术都可以让你绕过上面的静态字符串检查。

下一组静态指标利用`Telephony manage`。所有 Android 模拟器都有固定的值,这个 API 可以查询。

API                                                     Value                   Meaning
TelephonyManager.getDeviceId()                          0's                     emulator
TelephonyManager.getLine1 Number()                      155552155               emulator
TelephonyManager.getNetworkCountryIso()                 us                      possibly emulator
TelephonyManager.getNetworkType()                       3                       possibly emulator
TelephonyManager.getNetworkOperator().substring(0,3)    310                     possibly emulator
TelephonyManager.getNetworkOperator().substring(3)      260                     possibly emulator
TelephonyManager.getPhoneType()                         1                       possibly emulator
TelephonyManager.getSimCountryIso()                     us                      possibly emulator
TelephonyManager.getSimSerial Number()                  89014103211118510720    emulator
TelephonyManager.getSubscriberId()                      310260000000000         emulator
TelephonyManager.getVoiceMailNumber()                   15552175049             emulator

请记住 hook 框架,如 Xposed 和 Frida,都能 hook 这个 API 提供虚假的数据。


绕过模拟器检测

  1. 修改模拟器检测功能,通过使用 NOP 指令覆盖相关的字节码或原生代码来禁用不希望的行为。
  2. 使用 Frida 或 Xposed API 在 Java 层和原生层 hook 文件 API,返回看起来正常的值(最好取自真实设备),而不是模拟器的值。例如,你可以重写`TelephonyManager.getDeviceID`方法返回一个 IMEI 值。
  3. 关于修改、代码注入和内核模块的例子,请参考“Android上的篡改和逆向工程”一章。

有效性评估

在模拟器中安装并运行应用程序。应用程序应该检测到它正在模拟器中执行,并终止或拒绝执行本应受到保护的功能。

努力绕过防御,并回答以下问题:

  • 通过静态和动态分析识别模拟器检测代码有多难?
  • 检测机制可以被简单地绕过吗(例如,通过 hook 单一的 API 函数)?
  • 你是否需要编写自定义代码来禁用反模拟器功能?你需要多少时间?
  • 你认为绕过这些机制的难度如何?

运行时完整性检测测试(MSTG-RESILIENCE-6)

概述

此类别中的控制会验证应用程序内存空间的完整性,以保护应用程序不受在运行时的内存补丁的影响。这些补丁包括对二进制代码、字节码、函数指针表和重要数据结构不希望的修改,以及加载到进程内存中的恶意代码。完整性可通过以下方式验证:

  1. 将内存或内存校验与正确值进行比较
  2. 在内存中搜索不希望被修改的签名。

这与“检测逆向工程工具和框架”有一些重叠,事实上,我们在那一章中展示了基于签名的方法,当时我们展示了如何搜索进程内存寻找 Frida 相关的字符串的,下面是各种完整性监控的更多示例。

运行时完整性检测示例

Java 运行时检测篡改

下面的检测代码来自 dead && end 的博客

try {
  throw new Exception();
}
catch(Exception e) {
  int zygoteInitCallCount = 0;
  for(StackTraceElement stackTraceElement : e.getStackTrace()) {
    if(stackTraceElement.getClassName().equals("com.android.internal.os.ZygoteInit")) {
      zygoteInitCallCount++;
      if(zygoteInitCallCount == 2) {
        Log.wtf("HookDetection", "Substrate is active on the device.");
      }
    }
    if(stackTraceElement.getClassName().equals("com.saurik.substrate.MS$2") &&
        stackTraceElement.getMethodName().equals("invoked")) {
      Log.wtf("HookDetection", "A method on the stack trace has been hooked using Substrate.");
    }
    if(stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") &&
        stackTraceElement.getMethodName().equals("main")) {
      Log.wtf("HookDetection", "Xposed is active on the device.");
    }
    if(stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") &&
        stackTraceElement.getMethodName().equals("handleHookedMethod")) {
      Log.wtf("HookDetection", "A method on the stack trace has been hooked using Xposed.");
    }
 
  }
}

检测 Native Hook

通过使用 ELF 二进制文件,本地函数 hook 可以通过重写内存中的函数指针(例如,Global Offset Table 或 PLT hook)或修改函数代码本身的一部分(内联 hook)来安装。检查各自内存区域的完整性是检测这种 hook 的一种方法。

Global Offset Table(GOT)用于解析库函数。在运行时,动态链接器会使用全局符号的绝对地址对该表进行修改。GOT hook 会重写存储的函数地址,并将合法的函数调用重定向到逆向者的代码。可以通过枚举进程内存映射并验证每个 GOT 条目指向合法加载的库来检测这种类型的钩子。

与之相反,GNU `ld`只在第一次需要符号地址时才解析符号地址(惰性绑定),Android 链接器解析所有外部函数,并在加载库之后立即写入相应的 GOT 条目(立即绑定)。因此,您可以期望所有 GOT 条目在运行时都指向它们各自库的代码部分中的有效内存位置。GOT hook 检测方法通常会遍历GOT并进行验证。

内联 hook 的工作方式是在函数代码的开头或结尾覆盖一些指令。在运行时,这个所谓的跳板将重定向执行注入的代码。你可以通过检查库函数的开头和结尾是否有可疑指令来检测内联 hook,例如可疑的跳转,会跳到库外位置。

绕过和有效性评估

确保禁用了所有基于文件的逆向工程工具的检测。然后,使用 Xposed、Frida 和 Substrate 注入代码,并尝试安装原生 hook 和 Java 方法hook。应用程序应该能够检测到内存中的“恶意”代码,并做出相应的反应。

使用以下技术绕过检查:

  1. 修改完整性检查,通过使用 NOP 指令覆盖相应的字节码或原生代码来禁用不希望的行为。

  2. 使用 Frida 或 Xposed 来 hook 用于检测的 API 然后返回虚假值。

关于修改、代码注入和内核模块的例子,请参考“Android上的篡改和逆向工程”一章。

混淆测试(MSTG-RESILIENCE-9)

概述

混淆是将代码和数据转换为更难理解的过程,它是每个软件保护方案中不可分割的一部分。需要理解的重要一点是,混淆不是可以简单打开或关闭的东西,程序代码可能以多种方式、不同程度变得完全或部分不可理解。

在“Android 应用程序的代码质量和构建设置”一章的测试案例“Make Sure That Free Security Features Are Activated (MSTG-CODE-9)”中,我们描述了一些基本的混淆技术,这些技术一般通过 R8 和 Pro-Guard 在 Android 上使用。

有效性评估

尝试反编译字节码,反汇编包含的库文件,并进行静态分析,至少应用程序的核心功能(例如要混淆的功能)不应该被容易识别,验证以下内容:

  • 有意义的标识符,如类名、方法名和变量名
  • 字符串资源和二进制文件中的字符串被加密,
  • 被加密、打包或以其他方式隐藏的与受保护功能相关的代码和数据

要进行更详细的评估,你需要详细了解相关威胁和所使用的混淆方法。

设备绑定测试(MSTG-RESILIENCE-10)

概述

设备绑定的目标是阻止攻击者试图将应用及其状态从设备 A 复制到设备 B ,并继续在设备 B 上执行该应用程序。确定设备 A 是可信任的之后,它可能比设备 B 具有更多的特权。将应用程序从设备 A 复制到设备 B 时,这些差异特权不应更改。

在描述可用标识符之前,让我们快速讨论如何将它们用于绑定,这里有三种方法运行设备绑定:

  1. 使用设备标识符增强用于身份验证的凭据,如果应用程序需要频繁地重新对其自身或用户进行身份验证,这是有意义的。

  2. 使用与设备牢固绑定的密钥材料对存储在设备中的数据进行加密可以加强设备绑定,Android 密钥库提供了不可导出的私钥,我们可以将其用于此目的。当恶意行为者从设备提取此类数据时,由于无法访问密钥,因此无法解密数据,要实现此目标,请执行以下步骤:
    a. 使用`KeyGenParameterSpec`API 在`Android`密钥存储库中生成密钥对。

    //Source: <https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.html>
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
         KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
    keyPairGenerator.initialize(
         new KeyGenParameterSpec.Builder(
                 "key1",
                 KeyProperties.PURPOSE_DECRYPT)
                 .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
                 .build());
    KeyPair keyPair = keyPairGenerator.generateKeyPair();
    Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
    cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
    ···
    // The key pair can also be obtained from the Android Keystore any time as follows:
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
    keyStore.load(null);
    PrivateKey privateKey = (PrivateKey) keyStore.getKey("key1", null);
    PublicKey publicKey = keyStore.getCertificate("key1").getPublicKey();

    b. 为 AES-GCM 生成一个密钥:

    //Source: <https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.html>
    KeyGenerator keyGenerator = KeyGenerator.getInstance(
         KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    keyGenerator.init(
         new KeyGenParameterSpec.Builder("key2",
                 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                 .build());
    SecretKey key = keyGenerator.generateKey();
    // The key can also be obtained from the Android Keystore any time as follows:
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
    keyStore.load(null);
    key = (SecretKey) keyStore.getKey("key2", null);

    c. 通过 AES-GCM 加密,使用密钥对认证数据和应用程序存储的其他敏感数据进行加密,并使用实例 ID 等设备特定参数作为关联数据:

    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    final byte[] nonce = new byte[GCM_NONCE_LENGTH];
    random.nextBytes(nonce);
    GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce);
    cipher.init(Cipher.ENCRYPT_MODE, key, spec);
    byte[] aad = "<deviceidentifierhere>".getBytes();;
    cipher.updateAAD(aad);
    cipher.init(Cipher.ENCRYPT_MODE, key);
    //use the cipher to encrypt the authentication data see 0x50e for more details.

    d. 使用存储在`Android`密钥存储库中的公钥加密密钥,并将加密的密钥存储在应用程序的私有存储中。

e. 当需要访问令牌等身份验证数据或其他敏感数据时,使用存储在`Android`密钥存储库中的私钥对密钥进行解密,然后使用解密后的私钥对密文进行解密。

  1. 使用基于令牌的设备身份验证(实例`ID`)确保使用了相同的应用程序实例。

静态分析

过去,Android 开发者经常依赖于
`Settings.Secure.ANDROID_ID` (SSAID)和MAC 地址。随着`Android 8.0`(`API`等级`26`)的发布,这种情况有所改变。由于 MAC 地址在没有连接到接入点时通常是随机的,因此`SSAID`不再是一个绑定设备的`ID`,而是一个绑定到用户、设备和请求 SSAID 的应用程序的签名密钥的值。此外,在谷歌的`SDK`文档中有关于标识符的新建议。基本上,谷歌建议:

  • 涉及广告时,请使用`Advertising ID`(`AdvertisingIdClient.Info`),以便用户可以选择拒绝。
  • 使用`Instance ID` (`FirebaseInstanceId`)进行设备标识。
  • 仅将 SSAID 用于欺诈检测以及由同一开发人员签名的应用之间的共享状态。

请注意,`Instance ID`和`Advertising ID`在设备升级和设备重置之间并不稳定,但是 Instance ID 至少允许识别设备上当前安装的软件。

动态分析

有几种方法可以测试应用程序绑定。

使用模拟器动态分析

  1. 在模拟器上运行应用程序。
  2. 确保你可以在应用程序实例中提高信任(例如在应用程序中进行身份验证)。
  3. 按照以下步骤从模拟器中检索数据:
    a. 通过 ADB shell 以 SSH 方式进入模拟器。
    b. 运行`run-as <your app-id>`。 你的`app-id`是`Android Manifest.xml`中描述的包。
    c. `chmod 777`缓存和 shared-preferences 中的内容。
    d. 从 app-id 中退出当前用户。
    e. 将`/data/data/<your appid>/cache`的内容和`shared-preferences`复制到 SD 卡。
    f. 使用 ADB 或 DDMS 提取内容。
  4. 在另一个模拟器上安装应用程序。
  5. 在应用程序的数据文件夹中,覆盖步骤3中的数据。
    a. 将第3步中的数据复制到第二个模拟器的SD卡中。
    b. 通过 ADB shell 以 SSH 方式进入模拟器。
    c. 运行`run-as <your app-id>`。 你的 app-id 是 Android Manifest.xml 中描述的包。
    d. `chmod 777`缓存和`shared-preferences`中的内容。
    e. 从 app-id 中退出当前用户。
    f. 将`/data/data/<your appid>/cache`的内容和`shared-preferences`复制到 SD 卡。
  6. 你可以继续通过身份验证吗? 如果是这样,则绑定可能无法正常工作。

    使用两个不同的 root 设备

  7. 在你的 root 设备上运行应用程序。

  8. 确保你可以在应用程序实例中提高信任(例如在应用程序中进行身份验证)。
  9. 从第一个带根的设备检索数据。
  10. 在第二个 root 设备上安装应用程序。
  11. 在应用程序的数据文件夹中,用步骤3中的数据覆盖。
  12. 你可以继续通过身份验证吗? 如果是这样,则绑定可能无法正常工作。

Google Instance ID

谷歌 Instance ID 使用令牌对正在运行的应用程序实例进行身份验证,当应用程序被重置、卸载等时候,实例 ID 被重置,这意味着你将拥有该应用程序的新“实例”。 通过以下步骤获取 Instance ID:

  1. 在`Google Developer Console`中为给定应用程序配置`Instance ID`,这包括管理`PROJECT_ID`。
  2. 设置`Google Play`服务,在文件`build.gradle`中,添加

    apply plugin: 'com.android.application'
      ...
     
      dependencies {
          compile 'com.google.android.gms:play-services-gcm:10.2.4'
      }
  3. 获得一个`Instance ID`

    String iid = Instance ID.getInstance(context).getId();
    //now submit this iid to your server.
  4. 生成令牌。

    String authorizedEntity = PROJECT_ID; // Project id from Google Developer Console
    String scope = "GCM"; // e.g. communicating using GCM, but you can use any
                      // URL-safe characters up to a maximum of 1000, or
                      // you can also leave it blank.
    String token = Instance ID.getInstance(context).getToken(authorizedEntity,scope);
    //now submit this token to the server.
  5. 确保在无效的设备信息,安全性问题等情况下,可以处理来自 Instance ID 的回调,这需要扩展`Instance IDListenerService`并在那里处理回调:

    public class MyInstance IDService extends Instance IDListenerService {
    public void onTokenRefresh() {
      refreshAllTokens();
    }
     
    private void refreshAllTokens() {
      // assuming you have defined TokenList as
      // some generalized store for your tokens for the different scopes.
      // Please note that for application validation having just one token with one scopes can be enough.
      ArrayList<TokenList> tokenList = TokensList.get();
      Instance ID iid = Instance ID.getInstance(this);
      for(tokenItem : tokenList) {
      tokenItem.token =
        iid.getToken(tokenItem.authorizedEntity,tokenItem.scope,tokenItem.options);
      // send this tokenItem.token to your server
      }
    }
    };
  6. 在你的 Android清单中注册服务:
    <service android:name=".MyInstance IDService" android:exported="false">
    <intent-filter>
          <action android:name="com.google.android.gms.iid.Instance ID" />
    </intent-filter>
    </service>

    当你向服务器提交 Instance ID(iid)和令牌时,你可以通过 Instance ID 云服务来使用服务器验证令牌和 iid。当 iid 或令牌无效时,你可以触发一个保护程序(例如,通知服务器可能的复制或安全问题,或者从应用程序中删除数据并要求重新注册)。

请注意,Firebase 也支持 Instance ID

IMEI & Serial

谷歌建议不要使用这些标识符,除非应用程序处于高风险中。

在 Android 8.0(API 等级26)之前的 Android 设备,可以请求如下序列号:

String serial = android.os.Build.SERIAL;

对于运行 Android 版本 O 及更高版本的设备,可以通过下面方式请求设备的序列号:

  1. 在你的 Android 清单中设置权限:
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  2. 在运行时向用户请求权限:查看https://developer.android.com/training/permissions/requesting.html 了解更多细节。
  3. 获得序列号:

    String serial = android.os.Build.getSerial();

    检索 IMEI:

  4. 在你的 Android 清单中设置需要的权限:

    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
  5. 如果您使用的是 Android 6(API 等级23)或更高版本,请在运行时向用户请求权限:有关更多详细信息,请参阅https://developer.android.com/training/permissions/requesting.html
  6. 获得 IMEI:
    TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    String IMEI = tm.getDeviceId();

SSAID

谷歌建议不要使用这些标识符,除非应用程序处于高风险中。你可以如下检索`SSAID`:

String SSAID = Settings.Secure.ANDROID_ID;

自`Android 8.0`(`API`等级`26`)以来,`SSAID`和`MAC`地址的行为已更改。此外,`Google SDK`文档中还提供了有关标识符的新的建议。由于这种新行为,我们建议开发人员不要仅依赖`SSAID`。标识符变得不稳定起来,例如恢复出厂设置后或升级到`Android 8.0`(`API`等级`26`)后重新安装应用程序时,`SSAID`可能会更改。有些设备具有相同的`ANDROID_ID`或具有的`ANDROID_ID`可以被覆盖。因此,最好使用从`AndroidKeyStore`中随机生成的密钥,用`AES_GCM`加密`ANDROID_ID`。然后应将加密的`ANDROID_ID`存储在`SharedPreferences`中(私密的)。一旦应用程序签名更改,应用程序就可以检查`delta`并注册新的`ANDROID_ID`。如果在没有新的应用程序签名密钥的情况下进行了更改,则应表明存在其他问题。

有效性评估

当源代码可用时,你可以寻找一些关键术语:

  • 不再有效的唯一标识符:
    a. 没有`Build.getSerial`的`Build.SERIAL`
    b. 适用于`HTC`设备的`htc.camera.sensor.front_SN`
    c. `persist.service.bdroid.bdadd`
    d. 来自`WifiManager`的`Settings.Secure.bluetooth_address`或`WifiInfo.getMacAddress`,除非清单中启用了系统权限`LOCAL_MAC_ADDRESS`。
  • `ANDROID_ID`仅用作标识符,随着时间的流逝,这将影响旧设备的绑定质量。
  • 缺少实例`ID`、`Build.SERIAL`和`IMEI`。
    TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    String IMEI = tm.getDeviceId();
  • 使用`KeyPairGeneratorSpec`或`KeyGenParameterSpec`API 在`AndroidKeyStore`中创建私钥。

为了确保可以使用标识符,请检查`AndroidManifest.xml`中`IMEI`和`Build.Serial`的用法。 该文件应包含权限`<uses-permission android:name =“ android.permission.READ_PHONE_STATE” />`。

当`Android 8.0`(`API`等级`26`)的应用程序请求`Build.Serial`时,其结果将为“UNKNOWN”

有几种动态测试设备绑定的方法,见前面“使用模拟器动态分析”和“使用不同的 root 设备”。

引用

OWASP MASVS

  • MSTG-RESILIENCE-1:“应用程序通过提醒用户或终止应用程序来检测到已生根或越狱的设备并对其做出响应。”
  • MSTG-RESILIENCE-2:“应用程序阻止调试或检测并响应所附加的调试器,必须覆盖所有可用的调试协议。”
  • MSTG-RESILIENCE-3:“应用程序会检测并响应其自身沙箱中的可执行文件和关键数据的篡改。”
  • MSTG-RESILIENCE-4:“应用程序可以检测并响应设备上使用广泛的逆向工程工具和框架。”
  • MSTG-RESILIENCE-5:“应用程序检测并响应在模拟器中运行的情况。”
  • MSTG-RESILIENCE-6:“应用程序检测并响应对其自身存储空间中的代码和数据的篡改。”
  • MSTG-RESILIENCE-9:“混淆应用于编程防御,这反过来会阻碍通过动态分析去混淆。”
  • MSTG-RESILIENCE-10:“应用程序通过设备的多种独特属性获取设备指纹实现了设备绑定功能。”

SafetyNet Attestation

Tools

参考链接


[翻译]OWASP 安卓测试指南(v1.2 - 14 May 2020)节选

发布者

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注