Android 是谷歌基于 Linux 开发的开源平台,它充当移动操作系统(OS)。如今,该平台是各种现代技术的基础,如手机、平板电脑、可穿戴技术、电视和其他"智能"设备。典型的 Android 版本具有一系列预装("内置")的应用程序,并支持通过 Google Play 商店和其他市场安装第三方应用。
在最低层,Android 是基于 Linux 内核的变种。 在内核之上,硬件抽象层(HAL)定义了用于与内置硬件组件进行交互的标准接口。几种HAL 的实现打包在了共享库模块中,Android 系统需要时就会调用。这是允许应用程序与设备的硬件进行交互的基础,例如,它允许一个内置的电话应用程序使用设备的麦克风和扬声器。
Android 应用程序通常使用 Java 编写并编译为 与传统的 Java 字节码有些不同的 Dalvik 字节码。首先将 Java 代码编译为 .class 文件,然后使用 dx 工具将 JVM 字节码转换为 Dalvik 的 .dex 格式,这样就创建了 Dalvik 字节码。
当前版本的安卓在 Android 运行时(ART)上执行此字节码。ART 是安卓原始运行时 Dalvik 虚拟机的后继者。 Dalvik 和 ART 之间的关键区别在于字节码的执行方式。
在Dalvik中,字节码在执行时转换为机器码,这一过程称为即时(JIT)编译。JIT编译会对性能产生不利的影响:每次执行应用程序时都必须进行编译。为了提高性能,ART 引入了提前(AOT)编译。顾名思义,应用会在首次执行之前进行预编译。预编译的机器代码会用于所有后续的执行。AOT 将性能提高了两倍,同时降低了功耗。
Android 应用程序不能直接访问硬件资源,并且每个应用程序都运行在自己的沙盒中。这允许对资源和应用程序进行精确的控制:例如,崩溃的应用程序不会影响设备上运行的其他应用程序。同时,Android 运行时会控制分配给应用程序的最大系统资源数量,防止任何一个应用程序垄断过多的资源。
安卓从 Android 2.3.4(API 等级10)开始支持设备加密,此后发生了一些大的变化。Google 强制所有的设备运行 Android 6.0(API 等级23)或支持存储加密的更高版本,有些低端设备被豁免,因为这会严重影响性能。在下面各节中,你可以找到有关设备加密及其算法的信息。
Android 5.0(API 等级 21)及更高版本支持全盘加密。这种加密使用用户设备密码保护的单个密钥来加密和解密 userdata 分区。现在这种加密已被弃用,并认为应尽可能使用基于文件的加密。全盘加密具有一些缺点,例如,如果用户未输入解锁密码,则重启后将无法接听电话或没有操作警报。
自 Android 4.1(API 等级 15)以来,地址随机化(ASLR)就已成为 Android 的一部分,它是防止缓冲区溢出攻击的标准保护措施,这样可以确保将应用程序和操作系统都加载到随机的内存地址,从而很难获取特定的内存区域或库的正确地址。在 Android 8.0(API 等级26)中,也为内核实现了这种保护(KASLR)。仅当可以将应用程序加载到内存中的随机位置时才可以使用ASLR保护,这由应用程序的位置无关可执行文件(PIE)标志指示。从 Android 5.0(API 等级 21)开始,不再支持未启用 PIE 的原生库。最后,数据执行保护(DEP)可以防止代码在堆栈和堆上执行,这也可以用来防止缓冲区溢出漏洞。
每个应用程序都有一个 Android Manifest 文件,该文件以二进制 XML 格式嵌入内容。这个文件的标准名称是 AndroidManifest.xml,它位于应用程序的 Android Package Kit(APK)文件的根目录中。
下面是一个清单文件的示例,包括包名(惯例是反向的URL,但任何字符串都可以接受)。它还列出了应用程序版本,相关 SDK,需要的权限,给出了内容提供者,使用 intent 过滤器的广播接收器和应用程序及其活动的描述:
<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>
Activity 组成了应用程序的可见部分。每个页面都有一个活动,所以一个有三个不同页面的应用程序会实现三个不同的活动。活动通过继承 Activity 类来声明,包含所有用户界面元素:碎片、视图和布局。
和应用程序一样,活动也有自己的生命周期,并且需要监视系统的变化来处理它们。活动可以处于以下状态:活动、暂停、停止和不活动,这些状态由 Android 操作系统管理。对应的, 活动可以实现以下事件管理器:
Fragment 表示活动中的一个行为或用户界面的一部分。Android 在 Honeycomb 3.0 版本 (API 等级11)中引入了碎片。
碎片用于封装界面的各个部分,以促进可重用性和适应不同屏幕大小。碎片是自治的实体,它们包含了所有需要的组件(它们有自己的布局、按钮等等)。然而,它们必须与活动集成才能有效:碎片不能单独存在。它们有自己的生命周期,与实现它们的活动的生命周期相关联。
碎片不一定有用户界面,它们可以成为管理和应用程序用户界面相关的后台操作的一种方便而高效的方式。一个片段可以被声明为持久的,这样即使活动被破坏,系统仍然保持它的状态不变。
Intent 消息传递是建立在 Binder 之上的异步通信框架。此框架允许点对点和发布-订阅消息传递。Intent是一个消息传递对象,可以用来请求另一个应用组件的动作。Intent 以几种方式促进组件间的通信,下面是三个基本用例:
活动表示应用程序中的一个界面。你可以通过将 intent 传递给
一旦应用程序成功开发,下一步就是发布分享给其他人。然而,应用程序不能简单地添加到商店并共享,它们必须首先签名,该加密签名作为应用程序开发者放置的可验证标记。它识别应用程序的作者,并确保应用程序自最初发布以来没有被修改过。
在开发过程中,应用程序使用自动生成的证书进行签名,此证书本质上是不安全的并且仅用于调试。大多数商店不接受这种证书进行发布,因此,必须创建具有更安全的特性的证书。当一个应用程序安装在 Android 设备上时,包管理器会确保它是用包含在相应 APK 中的证书进行的签名。如果证书的公钥与用于设备上任何其他 APK 签名的密钥相匹配,那么新的 APK 可能与已存在的 APK 共享一个 UID。这促进了来自单个供应商的应用程序之间的交互,或者,指定的安全权限 对 Signature 保护级别来说是可能的,这将限制对使用相同密钥签名的应用程序的访问。
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/
)。
应用程序签名的原始版本将签名的 APK 实现为标准的签名 JAR,它必须包含
META-INF/MANIFEST.MF
中的所有条目,所有文件都必须使用一个公共证书签名,这个方案不保护 APK 的某些部分,比如 ZIP 元数据。该方案的缺点是 APK 验证者在应用签名之前需要对不可信的数据结构进行处理,并且验证者会丢弃数据结构不包含的数据。此外,APK 验证者必须解压所有压缩文件,这需要大量的时间和内存。
在 APK 签名方案中,对完整的 APK 进行哈希和签名,然后创建一个 APK 签名分块并插入到 APK 中。在验证期间,v2 方案检查整个 APK 文件的签名。这种形式的 APK 验证速度更快,并且提供了更全面的防篡改保护。你可以在下面看到 v2 方案的 APK 签名验证过程(
https://source.android.com/security/apksigning/v2#verification
)。
apk 签名验证过程 v2 方案
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' 中):
安全地存储你的密钥并确保它在整个生命周期中保持机密性是极其重要的。任何获得密钥访问权限的人都可以将你无法控制的内容(从而添加不安全的特性或使用基于签名的权限访问共享内容)更新到你的应用程序中。用户对应用程序及其开发者的信任完全基于这些证书,因此,证书保护和安全管理对于声誉和客户去留至关重要,密钥永远不能与其他个人共享。密钥存储在可以用密码保护的二进制文件中,这样的文件被称为密钥存储库。密钥存储库的密码应该是强壮的,并且只有密钥创建者知道。因此,密钥通常存储在开发人员对其访问受限的专用构建机器上。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 官方文档中找到,下面给出了一个例子来说明这一点。
在这个示例中,一个未签名的应用程序(
myUnsignedApp.apk
)将使用来自开发人员密钥存储库
myKeyStore.jks
(位于当前目录中)的私钥进行签名。该应用程序将成为名为
mySignedApp.apk
的已签名应用程序,并准备发布到商店。
在发布之前,应该始终使用
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 应用程序的结构和部署方式有了基本的了解。在本章中,我们将讨论如何建立一个安全测试环境,并描述您将使用的基本测试流程。本章是后面章节中讨论的更详细的测试方法的基础。
您可以在几乎所有运行 Windows、Linux 或 Mac OS 的机器上设置一个功能完整的测试环境。
至少,你需要 Android Studio(附带 Android SDK)平台工具、一个模拟器和一个应用程序来管理各种 SDK 版本和框架组件。Android Studio 还附带了一个用于创建模拟器映像的 Android 虚拟设备(AVD)管理器应用程序。确保你的系统上安装了最新的
SDK 工具
和
平台工具包
。
此外,如果你打算使用包含原生库的应用程序,你可能想要通过安装
Android NDK
来完成主机设置(这在“Android 上的篡改和逆向工程”一章中也会提到)。
通过 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:
注意:在
Linux
上,你需要选择一个SDK目录,
/opt
、
/srv
和
/usr/local
是常见的选择。
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 根目录,运行以下命令:
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 调试开关启用调试。
存在多种模拟器,它们各有优缺点:
免费模拟器:
Android 虚拟机设备(AVD)
- 官方的Android模拟器,与Android Studio一起发布。
Android X86
- 安卓代码库的 X86 接口
商业模拟器:
Genymotion
- 具有许多特性的成熟仿模拟器,可以作为本地和基于云的解决方案,提供非商业使用的免费版本。
Corellium
- 通过基于云或本地的解决方案提供自定义设备虚拟化。
虽然有其他一些免费的 Android 模拟器,但我们推荐使用 AVD,因为它提供了与其他模拟器相比更适合测试应用程序的增强功能。在本指南的其余部分中,我们将使用官方的 AVD 来执行测试。
AVD 支持一些硬件仿真,如
GPS
、
SMS
和
运动传感器
。
你可以使用 Android Studio 中的 AVD 管理器启动 Android 虚拟设备(AVD),也可以在命令行中使用
android
命令启动 AVD 管理器,在 Android SDK 的 tools 目录中可以找到:
Rooting(例如,修改 OS 以便你可以作为 root 用户运行命令)建议在真实设备上进行测试。这使你能够完全控制操作系统,并允许您绕过应用程序沙盒之类的限制。这些特权又允许你更容易地使用代码注入和函数 hook 等技术。
注意,root 是有风险的,在你继续之前,需要弄清三个主要后果。Root 可产生以下负面影响:
取消设备保修(在采取任何行动之前,一定要检查制造商的政策)
设备变砖,即设备无法操作和使用
创建额外的安全风险(因为通常会删除内置的漏洞缓解措施)
你不应该 root 一个存储着私人信息的个人设备,我们建议购买一个便宜的专用测试设备。许多老的设备,比如谷歌的 Nexus 系列,都可以运行最新的 Android 版本,用来测试非常好。
你需要明白,root 你的设备最终是你自己的决定,OWASP 不会对任何损坏负责。如果你不确定,在开始 root 过程之前应该寻求专家的建议。
实际上,任何 Android 手机都可以 root。Android OS(在内核级是 Linux OS 的演化)的商业版本针对移动世界进行了优化。这些版本中,一些特性被删除或禁用了,例如,非特权用户可以成为 root 用户(拥有提升的特权)。Root 手机意味着允许用户成为 root 用户,例如,添加一个名为
su
的标准 Linux 可执行文件,可用于更改为另一个用户帐户。
要 root 一个移动设备,首先解锁它的启动引导程序,解锁的过程取决于设备制造商。然而,出于实际的原因,root 某些移动设备要比其他设备更受欢迎,尤其是在安全测试方面:Google 开发的,Samsung、LG 和 Motorola 等公司制造的设备最受欢迎,尤其是因为它们被许多开发人员使用。当引导加载程序被解锁时,设备保证不会失效,谷歌提供了许多工具来支持 root。
XDA 论坛
上发布了精选的所有主流品牌设备的 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 检测方法的一个详细列表在“在 Android 上测试反逆向防御”一章中被提出。
对于典型的移动应用程序安全性编译版本,你通常希望测试一个禁用 root 检测的调试版本编译。如果这样的编译无法用于测试,你可以使用各种方法禁用 root 检测,这将在本书后面介绍。
本指南中使用了许多工具和框架来评估 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
是一个“无需更改 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'"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/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
(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
请注意,如果连接了多个设备,则必须使用
-s
参数定义目标设备的序列号(如上一代码片段所示)。
Angr 是一个用于分析二进制文件的 Python 框架。它对于静态和动态符号分析都是有用的。换句话说:给定一个二进制和一个请求状态,Angr将尝试到达那个状态,使用形式化的方法(一种用于静态代码分析的技术)来找到一条路径,以及强制执行。使用 angr 来获得请求的状态通常比手动调试和搜索通往所需状态的路径要快得多。Angr 使用VEX 中间语言进行操作,并带有 ELF/ARM 二进制文件加载程序,因此非常适合处理原生代码,例如原生 Android 二进制文件。
Angr 允许使用大量插件来进行反汇编、程序检测、符号执行、控制流分析、数据依赖分析、反编译等等。
自从版本8,Angr 是基于 Python3 并可以使用 pip 安装在*nix操作系统、macOS 和 Windows 上:
angr 的一些依赖项包含 Python 模块 Z3 和 PyVEX 的派生版本,这将覆盖原始版本。如果你将这些模块用于其他用途,则应该使用
Virtualenv
创建一个专用的虚拟环境。另外,你也可以使用提供的 docker 容器。有关更多细节,请参阅
安装指南
。
在
Angr 的 Gitbooks
页面上有全面的文档,包括安装指南、教程和使用示例,还提供了完整的
API 参考
。
你可以通过 Python REPL(如 iPython)使用angr,也可以编写方法脚本。尽管 angr 的学习曲线有些陡峭,但是尽管 angr 的学习曲线有些陡峭,但是当你想要通过强制的方式得到一个可执行文件的给定状态,我们还是建议你使用它。请见“逆向工程和篡改”一章的“符号执行”部分,可以作为一个很好的例子来说明它是如何工作的。
Apktool
用于解压 Android 应用程序包(APK)。简单地用标准
unzip
工具解压 APK 会留下一些不可读的文件。
AndroidManifest.xml
被加密成二进制 XML 格式,文本编辑器无法读取。此外,应用程序资源仍然被打包到一个归档文件中。
当使用默认命令行参数运行时,apktool 会自动将 Android 清单文件解密为为基于文本的 XML 格式并提取文件资源(它还会将 .DEX 文件反汇编为 smali 代码——这个特性我们将在本书后面介绍)。
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$
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
是流行的免费 DEX 转换器和 Java 反编译器的 Python 包装器。它自动提取、转换和反编译 apk,安装方式如下:
这会将
apkx
复制到
/usr/local/bin
。请参阅“逆向工程和篡改”一章的“反编译Java代码”一节,了解更多有关用法的信息。
Burp Suite 是一个用于移动和 web 应用程序安全性测试的集成平台。它的工具可以无缝地协同工作,以支持整个测试过程,从最初的攻击面映射和分析到发现和利用安全漏洞。Burp Proxy 作为 Burp Suite 的 web 代理服务器,它被定位为浏览器和 web 服务器之间的中间人。Burp Suite 允许您拦截、检查和修改传入和传出的原始 HTTP 流量。
设置 Burp 代理您的流量非常简单。我们假设您有一个 android 设备和工作站连接到 Wi-Fi 网络,该网络允许客户端到客户端通信。
PortSwigger 提供了关于如何
设置Android 设备来使用 Burp
和
如何将 Burp 的 CA 证书安装到 Android 设备的教程
。
Drozer
是一个 Android 安全评估框架,如果第三方应用程序与其他应用程序的 IPC 端点和底层操作系统进行了交互,你可以搜索应用程序和设备中的安全漏洞。
使用 drozer 的优势在于它能够自动执行多个任务,并且可以通过模块进行扩展。这些模块非常有帮助并且涵盖了不同的类别,其中包括扫描器类别,该类别使你可以使用简单的命令扫描已知的缺陷,例如模块
scanner.provider.injection
可以检测系统中安装的所有应用程序的内容提供者中的
SQL
注入 。 如果不使用
drozer
,则简单的任务(例如列出应用程序的权限)需要几个步骤,包括反编译 APK 和手动分析结果。
你可以参考
drozer GitHub 页面
(对于 Linux 和 Windows,macOS 请参考
这篇博客文章
)和
drozer 的网站
了解必备条件和安装说明。
drozer 在 Unix、Linux 和 Windows 上的安装说明在
drozer Github 页面
中有解释。对于macOS,
这篇博客
演示了所有的安装说明。
在开始使用 drozer 之前,还需要在 Android设备上运行 drozer 代理。从
GitHub 发布页面
下载最新的 drozer 代理,并用
adb install drozer.apk
安装它。
一旦安装完成,你可以通过运行
adb forward tcp:31415 tcp:31415
和
drozer console connect
启动一个会话到模拟器或 USB 连接的设备。这被称为直接模式,你可以在
用户指南的“开始一个会话”
一节中看到完整的说明。另一种选择是在基础设施模式下运行Drozer,在这种模式下,你运行一个 Drozer 服务器,它可以处理多个控制台和代理,并在它们之间路由会话。你可以在用户指南的
“基础设施模式”
一节中找到如何在此模式下设置 drozer 的详细信息。
现在您可以开始分析应用程序了。一个好的开端是列举一个应用程序的攻击面,可以通过以下命令很容易地完成:
同样,如果没有 drozer,这将需要几个步骤。
app.package.attacksurface
模块列出了活动,广播接收者,内容提供者和导出的服务,因此它们是公共的并且可以通过其他应用程序进行访问。 一旦确定了攻击面,就可以通过 drozer 与 IPC 端点进行交互,而无需编写单独的独立应用程序,某些任务会需要它,比如与内容提供者通信。
例如,如果应用程序的导出活动泄漏了敏感信息,我们可以使用 Drozer 模块
app.activity.start
来调用它:
前面的命令将启动活动,希望泄漏一些敏感信息。Drozer 有针对每一种 IPC 机制的模块,如果你想尝试带有故意易受攻击的应用程序的模块,请下载
InsecureBankv2
,该应用程序演示了与 IPC 端点相关的常见问题。请密切注意扫描器类别中的模块,因为它们对于自动检测系统包中的漏洞非常有用,特别是如果你使用的是手机公司提供的 ROM。在过去甚至使用 drozer 识别出过 Google 系统软件包中的 SQL 注入漏洞。
这里有一个非详尽的命令列表,你可以用来开始探索 Android:
# 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 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
)
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-ps
、
frida-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,只需运行:
如果您的设备没有 root,你也可以使用Frida,请参考“逆向工程和篡改”章节的“非 root 设备的动态分析”。
如果你有一个已 root 的设备,只需遵循
官方说明
或下面的提示。
除非另有说明,我们假定在这里是 root 设备。从
Frida 发布页
下载 frida-serve 二进制文件。确保为你的 Android 设备或模拟器的架构下载了正确的 frida-server 二进制文件:x86,x86_64,arm 或 arm64。确保服务器版本(至少是主版本号)与本地 Frida 安装版本匹配。PyPI通常安装最新版本的 Frida。如果不确定安装的版本,可以使用 Frida 命令行工具检查:
$ 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 &"
$ 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
(...)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$
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
(
.
.
.
)
$ 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
$
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
命令行工具:
这会在
__handlers __ / libc.so/open.js
中生成一些 JavaScript 代码,Frida 将其注入到进程中。该脚本将跟踪对
libc.so
中的
open
函数的所有调用。你可以使用 Frida
JavaScript API
根据需要修改生成的脚本。
不幸的是,尚不支持跟踪 Java 类的高级方法(但
将来
可能会)。
使用 Frida CLI 工具(
Frida
)与 Frida 交互工作。它挂接到一个进程,并为你提供 Frida API 的命令行界面。
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
方法,结果打印到控制台:
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")
1
2
3
4
5
6
7
8
9
10
11
12
13
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
[
*
]
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.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()
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
(
)
.
.
.
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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
(
)
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 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
(
)
$ 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)
$
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 是一个用于 Android 应用运行时的移动应用分析工具包,由 NCC 小组开发和维护,用 Python 编写。
它利用 root 设备上运行的 Frida server 或重打包到 Android 应用程序中的 Frida gadget。House 的目的是通过方便的 web GUI 提供一种简单的方法来原型化 Frida 脚本。
House 的安装说明和操作指南可以在
Readme of the Github repo
中找到。
Magisk(“Magic Mask”)是一种 root 你的 Android 设备的方法,它的特殊性在于对系统进行修改的方式。当其他的 root 工具改变系统分区上的实际数据时,Magisk 不会(它被称为“systemless“)。这样就可以隐藏对 root 敏感的应用程序(例如银行或游戏)的修改,并且在 root 的情况下允许使用官方的 Android OTA 升级
你可以通过阅读
GitHub 上的官方文档
来熟悉 Magisk。如果您没有安装 Magisk,可以在文档中找到安装说明。如果你使用正式的 Android 版本并计划升级,Magisk 在 GitHub 上提供了一个
教程
。
MobSF
是一个自动化的、一体化的移动应用程序审计框架,它也支持 Android APK 文件。启动 MobSF 最简单的方法是通过 Docker。
# 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
是一个“由 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's Wiki
中描述的那样,可以通过 pip 直接安装。
# 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
# 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 的信息可以在
Objection Wiki
上找到。
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 的官方安装说明
。我们强烈建议始终从 GitHub 版本安装 radare2,而不是通过 APT 等常见的包管理器。Radare2正处于非常活跃的开发阶段,这意味着第三方存储库经常会过时。
radare2 框架包含一组小型实用程序,可以在 r2 shell 中使用,也可以作为独立的 CLI 工具使用。这些工具包括
rabin2
,
rasm2
,
rahash2
,
radiff2
,
rafind2
,
ragg2
,
rarun2
,
rax2
,当然还有
r2
,这是主要的一个。
例如,你可以使用
rafind2
直接从一个加密的Android 清单文件(AndroidManifest.xml)中读取字符串:
$ 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
# 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$
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
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$
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 -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!
1
2
3
4
5
6
7
8
9
10
11
12
13
[
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
!
"section": "file",
"type": "ascii",
"string": "L2Rldi9jb20ua291c2hpa2R1dHRhLnN1cGVydXNlci5kYWVtb24v"
"vaddr": 3193,
"paddr": 3193,
"ordinal": 2,
"size": 25,
"length": 25,
"section": "file",
"type": "ascii",
"string": "L3N5c3RlbS9hcHAvU3VwZXJ1c2VyLmFwaw=="
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
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=="
}
,
[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
[
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
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
1
2
3
4
5
6
7
8
9
10
11
12
[
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
[
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
r2 命令通常接受选项(见
pd?
),例如,你可以通过在命令
pd N
后面附加数字(“N”)来限制显示的操作码。
你可能希望通过输入
V
进入所谓的可视模式,而不是仅仅将反汇编输出到控制台。
默认情况下,你将看到十六进制视图。通过键入
p
,你可以切换到不同的视图,如反汇编视图:
Radare2 提供了一种图形模式,它对于跟踪代码流非常有用,你可以在可视模式下输入
V
:
这只是一些 radare2 命令的一部分,用来开始从 Android 二进制文件中获取一些基本信息。Radare2 非常强大,在
Radare2 命令文档
中可以找到许多命令。Radare2 将在整个指南中用于不同的目的,如逆向代码、调试或执行二进制分析。我们还将结合使用其他框架,特别是Frida(更多信息,请参阅 r2frida 一节)。
有关 radare2 在Android上的详细使用,特别是在分析原生库时,请参考“Android 上的篡改和逆向工程”一章。你可能还想读一下
radare2 的官方书籍
。
r2frida 是一个允许 radare2 连接 Frida 的项目,有效地将 radare2 强大的逆向工程能力与 Frida 的动态分析工具包结合在一起。R2frida 允许你:
通过 USB 或 TCP 将 radare2 附加到任何本地进程或远程 frida-server。
从目标进程读/写内存。
将映射、符号、导入、类和方法等Frida信息加载到 radare2 中。
从 Frida 调用 r2 命令,因为它将 r2pipe 接口公开到了 Frida Javascript API 中。
请参考 r2frida 的
官方安装说明
。
随着 frida-server 运行,你现在应该能够使用pid、spawn path、主机和端口或设备 id 连接到它。例如,附加到 PID 1234:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
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
[
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
[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 "},
1
2
3
4
5
6
7
8
9
10
11
12
[
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 "
}
,
.
.
.
[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
1
2
3
4
5
6
7
8
9
10
11
12
[
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
在测试应用程序时,最常见的事情之一就是访问设备 shell。在这一节中,我们将看到如何在有或没有 USB 线情况下从你的主机上远程访问 Android Shell,以及在本地设备访问。
为了从你的主机连接到 Android 设备的 shell,
adb
通常是你选择的工具(除非你喜欢使用远程 SSH 访问,例如通过
Termux
)。
对于本节,我们假设你已经正确地启用了开发者模式和 USB 调试,正如“在真实设备上进行测试”中所解释的那样。一旦你通过 USB 连接了 Android 设备,你可以通过运行以下命令访问远程设备的 shell:
只有当你使用模拟器时,才可以使用命令
adb root
重新启动 adb,这样下次进入
adb shell
时,你就已经拥有 root 权限了。这也允许在工作站和 Android 文件系统之间双向传输数据,甚至可以访问只有 root 用户可以访问的位置(通过
adb push/pull
)。有关数据传输的更多信息,请参阅下面“主机-设备数据传输”一节。
如果你有不止一个设备,记住在你的所有
adb
命令上包括
-s
参数,后跟设备序列号(例如
adb -s emulator-5554 shell
或
adb -s 00b604081540b7c6 shell
)。你可以使用以下命令得到所有连接的设备的列表和他们的序列号:
例如,在一个 Nexus 设备上,你可以在设
Settings -> System -> About phone -> Status -> IP address 找到 IP 地址,或者进入 Wi-Fi 菜单,在你连接的网络上点击一下。
在
Android 开发者文档
中可以看到完整的说明和注意事项。
如果愿意,还可以启用 SSH 访问。一个方便的选项是使用 Termux,你可以轻松地
配置它来提供 SSH 访问
(使用密码或公钥身份验证),并使用命令
sshd
启动它(默认在端口8022上启动)。为了通过 SSH 连接到 Termux,只需运行命令
ssh -p 8022 <ip_address>
(其中
ip_address
是实际的远程设备IP)。这个选项还有一些额外的好处,它允许在端口8022上通过 SFTP 访问文件系统。
与远程 shell 相比,通常使用设备上的 shell(终端模拟器))可能非常单调乏味,但对于调试,例如网络问题或检查某些配置来说,它很方便。
Termux 是一个用于 Android 的终端模拟器,它提供了一个 Linux 环境,可以直接使用或不使用 root,并且不需要设置。安装额外的包是一项琐碎的任务,但是它有自己的 APT 包管理器(与其他终端模拟器应用程序相比的不同之处)。你可以使用命令
pkg search <pkg_name>
来搜索特定的包,并使用
pkg install <pkg_name>
来安装包。你可以直接从
Google Play
安装 Termux。
你可以使用
adb pull <remote> <local>
和
adb push <local> <remote>
命令将文件复制到设备或从设备中复制。它们的用法非常简单,例如,下面的操作将把
foo.txt
从当前目录(本地)复制到
sdcard
文件夹(远程):
Android Studio 有一个
内置的设备文件资源管理器
,你可以通过 View -> Tool Windows -> Device File Explorer 打开它。
如果你使用的是一个 root 设备,那么现在可以开始浏览整个文件系统了。然而,当使用未 root 设备访问应用程序沙箱时,除非应用程序是可调试的,否则不会工作,即使那样,你也会被“监禁”在应用程序沙箱中。
当你在一个特定的应用程序上工作并且想要复制你可能在它的沙箱中遇到的文件时,这个选项非常有用(注意你只能访问目标应用程序能够访问的文件)。这种方法不需要将应用设置为可调试的,在使用 Android Studio 的设备文件浏览器时需要这样做。
首先,如“推荐工具-Objection”中所述,以 Objection 的方式连接到应用程序。然后,像往常一样在终端上使用
ls
和
cd
浏览可用文件:
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$
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
...[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
.
.
.
[
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>
下载文件。
如果你有一个 root 设备,并且安装了 Termux 以及在其上
正确配置了 SSH 访问权限
,那么在端口8022上应该已经运行了一个 SFTP(SSH 文件传输协议)服务器,你可从终端访问它:
查看
Termux Wiki
以了解更多有关远程文件访问方法的信息。
获取和解压应用程序
从设备中提取APK文件有几种方法。根据应用程序是公共的还是私有的,你需要决定哪种方法是最简单的。
从设备中提取 APK 文件有几种方法。你需要根据应用程序是公共的还是私有的,决定哪种方法是最简单的。
最简单的选择之一是从 Google Play Store 的公共应用程序镜像的网站下载 APK。但是,请记住,这些站点不是官方站点,并且不能保证该应用程序没有重新打包或包含恶意程序。一些著名网站托管 APK,并以不修改应用程序而闻名,甚至列出了应用程序的 SHA-1 和 SHA-256 校验和:
APKMirror
APKPure
注意,你不能控制这些网站,你不能保证他们在未来做什么,在没有其他选择时再使用它们。
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
-
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 -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
$
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,如果你不知道包名,第一步是列出设备上安装的所有应用程序:
APK将下载到你的工作目录中。
另外,还有一些像
APK Extractor
这样的应用程序不需要 root,甚至可以通过你喜欢的方法共享所提取的 APK。如果你不喜欢通过网络连接设备或设置 adb 来传输文件,那么这将非常有用。
使用
adb install
在模拟器或连接的设备上安装 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
$ 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
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
$ 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
$
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
$
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
classes.dex:以 DEX 文件格式编译的类,Dalvik 虚拟机/ Android Runtime 可以运行。 DEX 是 Dalvik 虚拟机的 Java 字节码, 针对小型设备进行了优化
lib:包含组成 APK 的第三方库的目录。
res:包含尚未编译为 resources.arsc 的资源目录
resources.arsc:包含预编译资源的文件,例如用于布局的XML文件
由于使用标准
unzip
工具解压会留下一些不可读的文件,如 AndroidManifest.xml,你最好使用 apktool 解压APK,正如“推荐工具- apktool”中所述,解压结果如下:
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
$
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 清单文件是信息的主要来源,它包含了很多有趣的信息,比如包名、权限、应用程序组件等等。
这里是一些信息和相应的关键字的非详尽列表,你可以很容易地通过检查文件或使用
grep -i <keyword> AndroidManifest.xml
在 AndroidManifest 搜索。
App权限:
permission
(见“Android 平台 API”)
Backup llowance:
android:allowBackup
(见“android上的数据存储”)
应用程序组件:
activity
,
service
,
provider
,
receiver
(见“Android 平台 API”和“Android 上的数据存储”)
可调试标志:
debuggable
(参见“Android应用程序的代码质量和编译设置”)
请参阅前面提到的章节来了解更多关于如何测试这些要点的信息。
如上文“探索应用程序包”所示,应用程序的二进制文件(
classes.dex
)可以在应用程序包的根目录中找到。它是一个所谓的 DEX(Dalvik 可执行文件)文件,包含编译后的Java代码。由于它的特性,在一些转换之后,你将能够使用反编译器来生成 Java 代码。我们还看到了运行 apktool 后生成的
smali
文件夹。它以一种叫做 smali 的中间语言包含了反汇编的 Dalvik 字节码,这是 Dalvik 可执行文件的一种人类可读的表示。
有关如何 DEX 文件逆向工程的更多信息,请参考“Android上的篡改和逆向工程”一章中的“检查反编译 Java 代码”一节。
你可以查看 APK 中的
lib
文件夹:
...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
.
.
.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
1
2
3
4
5
6
7
8
9
10
11
12
$
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
外部数据目录在
/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
.
.
.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
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 来持久化存储日志输出:
$ 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
3 packets captured
3 packets received by filter
0 packets dropped by kernel
$
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
写入文件(-w)。我们传递
-
而不是文件名,这将使 tcpdump 写入标准输出中。
通过使用管道 (
|
),我们将 tcpdump 的所有输出发送到 netcat,netcat 会在端口11111上打开一个监听器。你通常需要监视 wlan0 接口。如果你需要另一个接口,请使用命令
$ ip addr
列出可用的选项。
要访问端口 11111,你需要通过 adb 将该端口转发到你的设备上。
Wireshark 应该立即启动(-k)。它会连接到转发端口,通过 netcat 从标准输入(-i -)中获得所有数据,你应该能看到来自 wlan0 接口的所有手机流量。
你可以使用 Wireshark 以可读的格式显示捕获的流量,弄清楚使用了哪些协议以及它们是否未加密。捕获所有流量(TCP 和 UDP)非常重要,因此你应该执行测试应用程序的所有功能并对其进行分析。
Wireshark 和 tcpdump
Firebase 云消息传递(FCM)是 Google 云消息传递(GCM)的后继产品,是 Google 提供的一项免费服务,可让你在应用程序服务器和客户端应用程序之间发送消息。服务器和客户端应用程序通过 FCM/GCM 连接服务器进行通信,可处理下游和上游消息。
下游消息(推送通知)从应用服务器发送到客户端应用程序;上游消息从客户端应用程序发送到服务器。
FCM 适用于 Android、iOS 和 Chrome。FCM目前提供了两种连接服务器协议:HTTP 和 XMPP。如
官方文档中
所述,这些协议的实现方式不同,下面的示例演示如何拦截这两个协议。
您需要在你的手机上配置 iptables 或使用 bettercap 来拦截流量。
FCM 可以使用 XMPP 或 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 -
$ 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 -
作为额外的安全层,推送通知可以使用
Capillary
加密。Capillary 是一个简化从基于 Java 的应用程序服务器向 Android 客户端发送端到端(E2E)加密推送消息的库。
有几个工具支持对依赖 HTTP(S) 协议的应用程序进行网络分析。最重要的工具是所谓的拦截代理,OWASP ZAP 和 Burp Suite Professional 是最有名的。一个拦截代理给测试人员一个中间人的位置。这个位置对于读取和修改所有应用程序请求和端点响应非常有用,用于测试授权、会话、管理等。
下面的过程,为 Android Studio 3.x 附带的 Android 模拟器设置 HTTP 代理:
设置代理监听本地主机,例如端口8080。
在模拟器设置中配置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。
安装 CA 证书的一种简单方法是将证书推送到设备上,并通过安全设置将其添加到证书存储中。例如,你可以按以下方式安装 PortSwigger (Burp)CA 证书:
启动 Burp 并使用主机上的 web 浏览器导航到 burp/,然后单击“CA Certificate”按钮下载
cacert.der
。
改变文件后缀
.der
为
.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 属性添加非系统证书的颁发机构:
你需要重新打包应用程序,正如“逆向工程和篡改”一章的“重打包”部分所解释的那样。有关重新打包过程的更多细节,你也可以参考
Android 开发者文档
,它从整体上解释了这个过程。
请注意,即使这种方法非常简单,它的主要缺点是你必须对想要评估的每个应用程序应用此操作,这是额外的测试开销。
请记住,如果您正在测试的应用程序有额外的强化措施,比如验证应用程序签名,那么你可能无法再启动该应用程序。作为重打包的一部分,你将使用自己的密钥签名应用程序,因此签名变化将导致这样的检查立即终止程序。你需要识别和禁用这些检查通过在重新打包的应用程序修改他们或使用 Frida 动态分析。
使用
Android-CertKiller
的 python 脚本可以自动执行上述步骤。这个脚本可以从安装的 Android 应用程序中提取APK,对其进行反编译,使其可调试,添加允许用户证书的新的网络安全配置,编译并签名新的 APK,并通过 SSL Bypass 安装新的 APK。
***************************************
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 证书是否在“设置”,“安全和位置”,“加密和凭据”,“受信任的凭据”中列出(位置可能有所不同)。
或者,您也可以手动遵循以下步骤,以达到相同的结果:
使
/
系统分区可写,这可能只在 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
通过遵循上述步骤,你可以让任何应用程序信任代理的证书,这允许拦截其流量,当然,除非应用程序使用 SSL pinning。
应用程序通常实现一些安全控制,这些控制使得对应用程序执行安全检查变得更加困难,例如 root 检测和证书固定。理想情况下,你将获得启用和禁用这些控制的两个应用程序版本,这允许你分析控制的正确实现,然后可以继续使用不太安全的版本进行进一步测试。
当然,这并不总是可能的,你可能需要对启用了所有安全控制的应用程序执行黑盒评估。下面的部分将向您展示如何绕过针对不同应用程序的证书固定。
一旦你设置了一个拦截代理并且有了一个中间人的位置,你可能仍然不能看到任何东西。这可能是由于应用程序中的限制(参见下一节),但也可能是由于你所连接的 Wi-Fi 中所谓的客户端隔离。
无线客户端隔离
是一种安全特性,它阻止无线客户端彼此通信。此功能对游客和 BYOD SSID 很有用,它增加了安全级别,以限制连接到无线网络的设备之间的攻击和威胁。
如果我们需要测试的Wi-Fi具有客户端隔离怎么办?
你可以在 Android 设备上配置代理指向 127.0.0.1:8080,通过 USB 连接手机到笔记本电脑,使用 adb 做一个反向端口转发:
一旦你完成了这个操作,Android 手机上所有的代理流量将会在127.0.0.1上转到8080端口,它将会通过 adb 重定向到笔记本电脑上的127.0.0.1:8080,你将会在 Burp 中看到流量。使用此技巧,还可以在具有客户端隔离的 Wi-Fi 中测试和拦截流量。
一旦你设置了一个拦截代理并且有了一个中间人位置,你可能仍然不能看到任何东西,这主要是由于以下原因:
这款应用使用了一个类似 Xamarin 的框架,它没有使用Android操作系统的代理设置。
你正在测试的应用程序检测了设置的代理,不允许进行任何通信。
在这两种场景中,你都需要额外的步骤最后才能看到流量,在下面的部分中,我们将描述两种不同的解决方案,bettercap 和 iptables。
还可以使用在你控制下的接入点来重定向通信,但这将需要额外的硬件,我们现在主要关注软件层面的解决方案。
对于这两个解决方案,你需要在 Burp 激活“支持不可见代理”,在代 Proxy Tab/Options/Edit 界面。
可以在 Android 设备上使用 iptables 将所有通信重定向到监听代理,下面的命令将把端口80重定向到在端口8080上你运行的代理:
$ 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$
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
$ 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.
$
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。
var Proxy = Java.use("<package-name>.<class-name>")
Proxy.isProxySet.overload().implementation = function() {
console.log("[*] isProxySet function invoked")
return false
1
2
3
4
5
6
7
8
9
10
11
12
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”。
Signing Manually (Android developer documentation) -
https://developer.android.com/studio/publish/app-signing#signing-manually
Custom Trust -
https://developer.android.com/training/articles/security-config#CustomTrust
Basic Network Security Configuration -
https://codelabs.developers.google.com/codelabs/android-network-security-config/#3
Security Analyst’s Guide to Network Security Configuration in Android P -
https://www.nowsecure.com/blog/2018/08/15/a-security-analysts-guide-to-network-security-configuration-in-android-p/
Android developer documentation -
https://developer.android.com/studio/publish/app-signing#signing-manually
Android 8.0 Behavior Changes -
https://developer.android.com/about/versions/oreo/android-8.0-changes
Android 9.0 Behavior Changes -
https://developer.android.com/about/versions/pie/android-9.0-changes-all#device-security-changes
Codenames, Tags and Build Numbers -
https://source.android.com/setup/start/build-numbers
Create and Manage Virtual Devices -
https://developer.android.com/studio/run/managing-avds.html
Guide to rooting mobile devices -
https://www.xda-developers.com/root/
API Levels -
https://developer.android.com/guide/topics/manifest/uses-sdk-element#ApiLevels
AssetManager -
https://developer.android.com/reference/android/content/res/AssetManager
SharedPreferences APIs -
https://developer.android.com/training/basics/data-storage/shared-preferences.html
Debugging with Logcat -
https://developer.android.com/tools/debugging/debugging-log.html
Android's APK format -
https://en.wikipedia.org/wiki/Android_application_package
Android remote sniffing using Tcpdump, nc and Wireshark -
https://blog.dornea.nu/2015/02/20/android-remote-sniffing-using-tcpdump-nc-and-wireshark/
Wireless Client Isolation -
https://documentation.meraki.com/MR/Firewall_and_Traffic_Shaping/Wireless_Client_Isolation
adb -
https://developer.android.com/studio/command-line/adb
Androbugs -
https://github.com/AndroBugs/AndroBugs_Framework
Android NDK Downloads -
https://developer.android.com/ndk/downloads/index.html#stable-downloads
Android Platform Tools -
https://developer.android.com/studio/releases/platform-tools.html
Android Studio -
https://developer.android.com/studio/index.html
Android tcpdump -
https://www.androidtcpdump.com/
Android-CertKiller -
https://github.com/51j0/Android-CertKiller
Android-SSL-TrustKiller -
https://github.com/iSECPartners/Android-SSL-TrustKiller
angr -
https://github.com/angr/angr
APK Extractor -
https://play.google.com/store/apps/details?id=com.ext.ui
APKMirror -
https://apkmirror.com
APKPure -
https://apkpure.com
apktool -
https://github.com/iBotPeaches/Apktool
apkx -
https://github.com/b-mueller/apkx
Burp Suite Professional -
https://portswigger.net/burp/
Burp-non-HTTP-Extension -
https://github.com/summitt/Burp-Non-HTTP-Extension
Capillary -
https://github.com/google/capillary
Device File Explorer -
https://developer.android.com/studio/debug/device-file-explorer
Drozer -
https://labs.f-secure.com/tools/drozer/
FileZilla -
https://filezilla-project.org/download.php
Frida -
https://www.frida.re/docs/android/
Frida CLI -
https://www.frida.re/docs/frida-cli/
frida-ls-devices -
https://www.frida.re/docs/frida-ls-devices/
frida-ps -
https://www.frida.re/docs/frida-ps/
frida-trace -
https://www.frida.re/docs/frida-trace/
gplaycli -
https://github.com/matlink/gplaycli
House -
https://github.com/nccgroup/house
InsecureBankv2 -
https://github.com/dineshshetty/Android-InsecureBankv2
Inspeckage -
https://github.com/ac-pm/Inspeckage
JAADAS -
https://github.com/flankerhqd/JAADAS
JustTrustMe -
https://github.com/Fuzion24/JustTrustMe
Magisk Modules repository -
https://github.com/Magisk-Modules-Repo
Magisk Trust User Certs module -
https://github.com/NVISO-BE/MagiskTrustUserCerts/releases
Mitm-relay -
https://github.com/jrmdev/mitm_relay
MobSF -
https://github.com/MobSF/Mobile-Security-Framework-MobSF
Nathan -
https://github.com/mseclab/nathan
Objection -
https://github.com/sensepost/objection
OWASP ZAP -
https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project
QARK -
https://github.com/linkedin/qark/
R2frida -
https://github.com/nowsecure/r2frida/
Radare2 -
https://rada.re/r/
SDK tools -
https://developer.android.com/studio/index.html#downloads
SSLUnpinning -
https://github.com/ac-pm/SSLUnpinning_Xposed
Termux -
https://play.google.com/store/apps/details?id=com.termux
Wireshark -
https://www.wireshark.org/
Xposed -
https://www.xda-developers.com/xposed-framework-hub/
在反逆向方面,root 检测的目标是让应用程序在一个 root 设备上运行更加困难,这反过来就阻碍了一些逆向工程师喜欢使用的工具和技术。和大多数其他防御措施一样,root 检测本身并不是很有效,但是在整个应用程序中分散实现多个 root 检测可以提高整个防篡改方案的有效性。
对于 Android,我们对“root 检测”的定义更广泛一些,包括自定义 ROM 检测,即确定该设备是现有的 Android 版本还是自定义版本。
在下一节中,我们将列出一些常见的 root 检测方法,你会在 OWASP 移动测试指南的
crackme 示例
中发现其中一些方法的实现。
Root 检测也可以通过像
RootBeer
这样的库实现。
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
:检查自你发出请求并获得响应以来经过了多长时间,延迟的响应可能表明活动可疑。
apkPackageName
,
apkCertificateDigestSha256
,
apkDigestSha256
:提供有关 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,
{
"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
会给你一个关于设备兼容性的更严格的信号,只有通过谷歌认证的未修改设备才能通过
ctsProfileMatch
。
ctsProfileMatch
会失败的设备包括:
basicIntegrity
失败的设备
具有解锁的引导加载程序的设备
具有自定义系统镜像(自定义 ROM)的设备
制造商未申请或未通过 Google 认证的设备
设备的系统映像直接从 Android 开源程序源文件编译
系统映像作为测试版或开发者预览程序(包括Android测试版程序)的一部分分发的设备
使用加密安全的随机函数在服务器上创建一个大的(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
public static boolean checkRoot(){
for(String pathDir : System.getenv("PATH").split(":")){
if(new File(pathDir, "su").exists()) {
return true;
return false;
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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
是否存在的另一种方法是尝试通过
Runtime.getRuntime.exec
方法执行它。如果
su
不在路径上,就会抛出 IOException。同样的方法可以用来检查在 root 设备上经常发现的其他程序,比如 busybox 和通常指向它的符号链接。
supersu 是目前最流行的 root 工具,它运行一个名为
daemonsu
的身份验证守护进程,因此这个进程的出现是 root 设备的另一个标志。可以使用
ActivityManager.getRunningAppProcesses
和
manager.getRunningServices
API ,
ps
命令和浏览
/proc
目录枚举正在运行的进程。下面是一个在
rootinspector
中实现的例子:
// 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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
;
}
系统目录上不寻常的权限表明可能是自定义的或 root 的设备。系统和数据目录通常是挂载只读的,但当设备是 root 时,有时会发现它们挂载读写的。寻找这些挂载“rw”标志的文件系统,或者尝试在数据目录中创建文件。
检查测试版本和自定义 ROM 的标志也很有帮助。一种方法是检查 BUILD 标签中的 test-key,它通常
表示自定义的 Android 镜像,检查 BUILD 标签如下
:
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 的公开证书。
使用 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 检测是必要的吗?
你需要编写自定义代码吗?
成功绕过这些机制需要多长时间?
你认为绕过这些机制的难度如何?
如果根检测缺失或太容易被绕过,请按照上面列出的有效性标准提出建议。这些建议可能包括更多的检测机制,以及更好地将现有机制与其他防御机制集成。
调试是分析应用程序运行时行为的一种非常有效的方法。它允许逆向工程师单步执行代码、在任意点停止应用程序执行、检查变量的状态、读取和修改内存等等。
反调试特性可以是预防式的,也可以是反应式的。顾名思义,预防式反调试首先阻止调试器进行附加,反应式反调试包括检测调试器并以某种方式对它们做出反应(例如,终止应用程序或触发隐藏行为)。“越多越好”的规则适用于:为了最大化效率,防御者结合了多种预防和检测方法,在不同的 API 层上运行,并且分布在整个应用程序中。
正如在“逆向工程和篡改”一章中提到的,我们在 Android 上必须处理两种调试协议:我们可以使用 JDWP 在 Java 层进行调试,也可以使用基于 ptrace 的调试器在原生层进行调试。一个好的反调试方案应该能防御这两种类型的调试。
在“逆向工程和篡改”一章中,我们讨论了 JDWP,这是调试器和 Java 虚拟机之间的通信协议。我们展示了,通过给应用程序的清单文件打补丁和更改
ro. debugging
属性来启用所有应用程序的调试是很容易的。让我们看看开发人员检测和禁用 JDWP 调试器所做的一些事情。
我们已经遇到过
android:debuggable
属性。Android 清单文件中的这个标志决定是否为应用程序启动 JDWP 线程。它的值可以通过应用程序的 ApplicationInfo 对象用程序代码来确定。如果设置了标志,则说明清单文件已被篡改并允许调试。
public static boolean isDebuggable(Context context){
return ((context.getApplicationContext().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0);
JNIEXPORT jboolean JNICALL Java_com_test_debugging_DebuggerConnectedJNI(JNIenv * env, jobject obj) {
if (gDvm.debuggerConnected || gDvm.debuggerActive)
return JNI_TRUE;
return JNI_FALSE;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
;
}
}
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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
;
}
;
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 <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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#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
,通常利用了一次只能连接到进程的一个调试器这一事实。
当你调试应用程序并在原生代码上设置断点时,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
$
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 进程。
你可以通过类似于以下简单示例代码,将子进程附加到父进程作为调试器来防止进程调试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
)
;
}
}
}
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
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 函数实现的完整的改进代码:
JNIEXPORT void JNICALL
Java_sg_vantagepoint_antidebug_MainActivity_antidebug(JNIEnv *env, jobject instance) {
anti_debug();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#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
(
)
;
}
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 中,可以找到一个示例。
乍一看,代码看起来像之前的挑战,名为
CodeCheck
的类负责验证用户输入的代码,真正检查似乎在
bar
方法中进行,该方法被声明为原生方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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"
)
;
}
如果反调试机制缺失或太容易被绕过,请按照上述有效性标准提出建议,这些建议可能包括添加更多的检测机制,以及更好地将现有机制与其他防御机制集成。
关于文件完整性有两个主题:
代码完整性检查:在“Android上的篡改和逆向工程”一章中,我们讨论了 Android 的 APK 代码签名验证。我们还发现,逆向工程师可以通过重新打包和重新签名应用程序,轻松绕过这个检查。为了让这个绕过过程更加复杂,可以通过对应用程序字节码、原生库和重要数据文件进行 CRC 校验来增强保护方案。这些检查可以在 Java 层和原生层上实现,思路是在适当的地方进行额外的控制,以让应用程序只能在未修改的状态下正确运行,即使代码签名是有效的。
文件存储完整性检查:应用程序存储在 SD 卡或公共存储中的文件的完整性以及存储在
SharedPreferences
中的键值对的完整性应该受到保护。
当为存储本身提供完整性时,你可以通过给定的键值对(对于 Android
SharedPreferences
)或者通过文件系统提供的完整文件创建 HMAC。
在使用 HMAC 时,可以
使用 bouncy castle 实现或用 AndroidKeyStore 对给定内容进行 HMAC
。
使用 BouncyCastle 生成 HMAC 时,请完成以下步骤:
确保 BouncyCastle 或 SpongyCastle 注册为安全提供商。
使用密钥(可以存储在密钥存储库中)初始化HMAC。
获取需要 HMAC 的内容的字节数组。
在 HMAC 上使用字节码调用
doFinal
。
将 HMAC 附加到步骤3中获得的字节数组中。
存储步骤5的结果。
使用 BouncyCastle 验证 HMAC 时,请完成以下步骤:
确保 BouncyCastle 或 SpongyCastle 注册为安全提供商。
将消息和 HMAC 字节提取为单独的数组。
重复生成 HMAC 过程的步骤1-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
(
)
)
;
}
}
以未修改的状态运行应用程序,并确保一切正常。对应用程序包中的
class .dex
和任意 .so 库进行简单的修改。如“基本安全测试”章节所述,重新打包并重新签名应用程序,然后运行应。应用程序应该检测到修改并以某种方式做出响应,至少应该警告用户或终止。努力绕过防御,并回答以下问题:
这些机制可以被简单地绕过吗(例如,通过 hook 单个 API 函数)?
通过静态和动态分析识别反调试代码有多难?
你是否需要编写自定义代码来禁用防御?你需要多少时间?
你认为绕过这些机制的难度如何?
应用与应用程序源代码完整性检查类似的方法,回答以下问题:
可以简单地绕过这些机制吗(例如,通过更改文件或键值的内容)?
获得 HMAC 密钥或非对称私钥有多难?
你是否需要编写自定义代码来禁用防御?你需要多少时间?
你认为绕过这些机制的难度如何?
逆向工程师常用的工具、框架和应用程序由可能代表着对应用的一次逆向尝试。这些工具中的一些只能在 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
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
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
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 函数)?
通过静态和动态分析识别反逆向工程代码有多难?
您是否需要编写自定义代码来禁用防御?你需要多少时间?
你认为绕过这些机制的难度如何?
以下步骤将指导你绕过逆向工程工具检测:
修改反逆向工程功能,通过使用 NOP 指令覆盖相关的字节码或原生代码来禁用不希望的行为。
使用 Frida 或 Xposed 在 Java 和原生层上 hook 文件系统 API,返回原始文件的句柄,而不是修改后的文件。
使用内核模块拦截与文件相关的系统调用,当进程尝试打开修改后的文件时,返回文件未修改版本的文件描述符。
关于修改、代码注入和内核模块的例子,请参考“Android上的篡改和逆向工程”一章。
在防逆向方面,模拟器检测的目的是增加在模拟器设备上运行应用程序的难度,这阻碍了逆向工程师使用某些喜欢的工具和技术。这种难度的增加强制逆向工程师去对抗模拟器检测或使用真实的物理设备,从而限制了大规模设备分析所需的访问。
有几个指标可以表明设备是模拟器。尽管所有这些 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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
1
2
3
4
5
6
7
8
9
10
11
12
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
修改模拟器检测功能,通过使用 NOP 指令覆盖相关的字节码或原生代码来禁用不希望的行为。
使用 Frida 或 Xposed API 在 Java 层和原生层 hook 文件 API,返回看起来正常的值(最好取自真实设备),而不是模拟器的值。例如,你可以重写
TelephonyManager.getDeviceID
方法返回一个 IMEI 值。
关于修改、代码注入和内核模块的例子,请参考“Android上的篡改和逆向工程”一章。
在模拟器中安装并运行应用程序。应用程序应该检测到它正在模拟器中执行,并终止或拒绝执行本应受到保护的功能。
努力绕过防御,并回答以下问题:
通过静态和动态分析识别模拟器检测代码有多难?
检测机制可以被简单地绕过吗(例如,通过 hook 单一的 API 函数)?
你是否需要编写自定义代码来禁用反模拟器功能?你需要多少时间?
你认为绕过这些机制的难度如何?
此类别中的控制会验证应用程序内存空间的完整性,以保护应用程序不受在运行时的内存补丁的影响。这些补丁包括对二进制代码、字节码、函数指针表和重要数据结构不希望的修改,以及加载到进程内存中的恶意代码。完整性可通过以下方式验证:
将内存或内存校验与正确值进行比较
在内存中搜索不希望被修改的签名。
这与“检测逆向工程工具和框架”有一些重叠,事实上,我们在那一章中展示了基于签名的方法,当时我们展示了如何搜索进程内存寻找 Frida 相关的字符串的,下面是各种完整性监控的更多示例。
下面的检测代码来自
dead && end 的博客
。
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.");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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."
)
;
}
}
}
通过使用 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。应用程序应该能够检测到内存中的“恶意”代码,并做出相应的反应。
使用以下技术绕过检查:
修改完整性检查,通过使用 NOP 指令覆盖相应的字节码或原生代码来禁用不希望的行为。
使用 Frida 或 Xposed 来 hook 用于检测的 API 然后返回虚假值。
关于修改、代码注入和内核模块的例子,请参考“Android上的篡改和逆向工程”一章。
混淆是将代码和数据转换为更难理解的过程,它是每个软件保护方案中不可分割的一部分。需要理解的重要一点是,混淆不是可以简单打开或关闭的东西,程序代码可能以多种方式、不同程度变得完全或部分不可理解。
在“Android 应用程序的代码质量和构建设置”一章的测试案例“Make Sure That Free Security Features Are Activated (MSTG-CODE-9)”中,我们描述了一些基本的混淆技术,这些技术一般通过 R8 和 Pro-Guard 在 Android 上使用。
尝试反编译字节码,反汇编包含的库文件,并进行静态分析,至少应用程序的核心功能(例如要混淆的功能)不应该被容易识别,验证以下内容:
有意义的标识符,如类名、方法名和变量名
字符串资源和二进制文件中的字符串被加密,
被加密、打包或以其他方式隐藏的与受保护功能相关的代码和数据
要进行更详细的评估,你需要详细了解相关威胁和所使用的混淆方法。
设备绑定的目标是阻止攻击者试图将应用及其状态从设备 A 复制到设备 B ,并继续在设备 B 上执行该应用程序。确定设备 A 是可信任的之后,它可能比设备 B 具有更多的特权。将应用程序从设备 A 复制到设备 B 时,这些差异特权不应更改。
在描述可用标识符之前,让我们快速讨论如何将它们用于绑定,这里有三种方法运行设备绑定:
使用设备标识符增强用于身份验证的凭据,如果应用程序需要频繁地重新对其自身或用户进行身份验证,这是有意义的。
使用与设备牢固绑定的密钥材料对存储在设备中的数据进行加密可以加强设备绑定,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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//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
(
)
;
//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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//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
)
;
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.
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.
e. 当需要访问令牌等身份验证数据或其他敏感数据时,使用存储在
Android
密钥存储库中的私钥对密钥进行解密,然后使用解密后的私钥对密文进行解密。
使用基于令牌的设备身份验证(实例
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 至少允许识别设备上当前安装的软件。
有几种方法可以测试应用程序绑定。
在模拟器上运行应用程序。
确保你可以在应用程序实例中提高信任(例如在应用程序中进行身份验证)。
按照以下步骤从模拟器中检索数据:
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 提取内容。
在另一个模拟器上安装应用程序。
在应用程序的数据文件夹中,覆盖步骤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 卡。
你可以继续通过身份验证吗? 如果是这样,则绑定可能无法正常工作。
在你的 root 设备上运行应用程序。
确保你可以在应用程序实例中提高信任(例如在应用程序中进行身份验证)。
从第一个带根的设备检索数据。
在第二个 root 设备上安装应用程序。
在应用程序的数据文件夹中,用步骤3中的数据覆盖。
你可以继续通过身份验证吗? 如果是这样,则绑定可能无法正常工作。
谷歌 Instance ID
使用令牌对正在运行的应用程序实例进行身份验证,当应用程序被重置、卸载等时候,实例 ID 被重置,这意味着你将拥有该应用程序的新“实例”。 通过以下步骤获取 Instance ID:
在
Google Developer Console
中为给定应用程序配置
Instance ID
,这包括管理
PROJECT_ID
。
设置
Google Play
服务,在文件
build.gradle
中,添加
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.
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.
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}
}
}
;
<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
。
谷歌建议不要使用这些标识符,除非应用程序处于高风险中。
在 Android 8.0(API 等级26)之前的 Android 设备,可以请求如下序列号:
在运行时向用户请求权限:查看
https://developer.android.com/training/permissions/requesting.html
了解更多细节。
获得序列号:
如果您使用的是 Android 6(API 等级23)或更高版本,请在运行时向用户请求权限:有关更多详细信息,请参阅
https://developer.android.com/training/permissions/requesting.html
。
获得 IMEI:
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String IMEI = tm.getDeviceId();
自
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 设备”。
MSTG-RESILIENCE-1:“应用程序通过提醒用户或终止应用程序来检测到已生根或越狱的设备并对其做出响应。”
MSTG-RESILIENCE-2:“应用程序阻止调试或检测并响应所附加的调试器,必须覆盖所有可用的调试协议。”
MSTG-RESILIENCE-3:“应用程序会检测并响应其自身沙箱中的可执行文件和关键数据的篡改。”
MSTG-RESILIENCE-4:“应用程序可以检测并响应设备上使用广泛的逆向工程工具和框架。”
MSTG-RESILIENCE-5:“应用程序检测并响应在模拟器中运行的情况。”
MSTG-RESILIENCE-6:“应用程序检测并响应对其自身存储空间中的代码和数据的篡改。”
MSTG-RESILIENCE-9:“混淆应用于编程防御,这反过来会阻碍通过动态分析去混淆。”
MSTG-RESILIENCE-10:“应用程序通过设备的多种独特属性获取设备指纹实现了设备绑定功能。”
Developer Guideline -
https://developer.android.com/training/safetynet/attestation.html
SafetyNet Attestation Checklist -
https://developer.android.com/training/safetynet/attestation-checklist
Do's & Don'ts of SafetyNet Attestation -
https://android-developers.googleblog.com/2017/11/10-things-you-might-be-doing-wrong-when.html
SafetyNet Verification Samples -
https://github.com/googlesamples/android-play-safetynet/
SafetyNet Attestation API - Quota Request -
https://support.google.com/googleplay/android-developer/contact/safetynetqr
adb -
https://developer.android.com/studio/command-line/adb
Frida -
https://www.frida.re
DDMS -
https://developer.android.com/studio/profile/monitor
[翻译]OWASP 安卓测试指南(v1.2 - 14 May 2020)节选