在过去的两章中,您了解了逆向工程的基础知识,并查看了一些工具及其安装方法。现在,您应该能够创建一个基于 Ubuntu 的虚拟机环境(或已经创建了这样的环境)。然后,您学会了如何安装和运行第二章中列出的逆向工程工具,《使用现代工具设置移动应用逆向工程环境》(仅涵盖了部分工具的基本操作,而非所有功能)。
在本章中,我们将涵盖以下内容:
- 安卓应用开发
- 安卓应用的逆向工程
- 提取 Java 源代码
- 将 .dex 文件转换为 smali
- 逆向工程和渗透测试
- 安卓应用中的代码混淆
技术要求
本章有以下技术要求:
- 一个带有第二章列出的工具的 Ubuntu 虚拟机环境(仅包含安卓特定工具)。
- 从以下链接下载 SecureStorage 安卓应用的源代码:github.com/0ctac0der/S...
Android 应用开发
在我们深入逆向工程之前,了解正向工程过程以及应用程序是如何开发的非常重要。为了创建一个应用程序,开发人员根据应用程序预期运行的操作系统选择编程语言。例如,在安卓应用的情况下,开发人员通常可以选择使用 Java、Kotlin 或 C++ 来开发他们的应用程序(也可以使用其他语言)。Kotlin 是安卓应用的官方语言。Android Studio 是构建安卓应用的官方开发环境。我们在第 1 章《逆向工程基础 - 理解移动应用的结构》的安卓应用基础部分中已经使用了 Android Studio。Android Studio 包含了一套完整的开发工具,如 Android 调试桥 (ADB)、fastboot 和原生开发工具包 (NDK)。安卓软件开发工具包 (SDK) 已完全集成到 Android Studio 中,并且可以通过 SDK 管理器轻松安装。但是,我们也可以在没有 Android Studio 的情况下单独使用安卓 SDK。请参考以下截图,其中显示了 Android SDK 作为 Android Studio 的一部分:
开发人员编写应用程序的所有功能代码、用户界面以及数据处理逻辑。除了代码之外,所有必需的资源文件、配置文件和图像也作为 Android Studio 项目的一部分添加进来。然后,使用 SDK 编译这些代码(连同资源)。
假设代码是用 Java 编写的。它将由 Java 编译器 (javac) 编译为 Java 字节码文件(class 文件)。然后,这些 Java 字节码文件被发送到 Java 虚拟机 (JVM),JVM 使用即时编译器 (JIT) 将它们转换为机器码并在设备上运行。
重要提示
字节码是什么? 要在计算机上运行 Java 程序,我们需要将高级代码(源代码或程序代码)转换为机器码。编译器将代码从高级语言转换为低级语言;编译过程的输出是字节码。字节码是低级代码,主要是软件解释器或虚拟机(如 JVM)的指令集。最后,解释器将字节码转换为机器码。
如果代码是用 Kotlin 编写的,则可以使用编译器(如 kotlinc)生成兼容 Java 的字节码。字节码只是由软件解释器执行的一种指令集。
在 Android 版本 1.0 到 4.4 之前,使用 Dalvik 虚拟机来运行应用程序。在 Android 4.4 中,Google 添加了 Android Runtime (ART) 以及 Dalvik 虚拟机。Dalvik 虚拟机运行一种优化的字节码格式,称为 Dalvik 字节码。在编译过程中,.class 文件和 .jar 库被转换为包含 Dalvik 字节码的 classes.dex 文件。ART 环境也执行 .dex 文件。
让我们总结一下:
- 开发人员在 Android 开发环境 Android Studio 中编写应用程序代码(如 Java 或 Kotlin)。
- 使用 DEX 编译器将编写的代码编译为 Dalvik 可执行 (.dex) 文件,这些文件将在 ART 环境中运行(以及 Dalvik 虚拟机中)。
- Android Studio 中的编译器还将编译其他资源文件、JAR 库以及其他库(如果有)。
- 编译后的 .dex 文件和编译后的资源将一起打包,创建 Android 包(APK)。
重要提示
对于使用 Java 开发的 Android 应用程序,代码被编译为 DEX 字节码。逆向工程过程是反向的 - 从 APK 中提取 .dex 文件并将其转换为 Java 代码。
最终的应用程序包(即 APK)将包含以下重要文件和文件夹,以及其他实体:
- res 文件夹中的资源文件
- AndroidManifest.xml
- 包含所有元信息的 resources.arsc 文件
- .dex 文件
在第 2 章《使用现代工具设置移动应用逆向工程环境》中,当我们简单地将 APK 文件的所有内容解压缩,将其扩展名更改为 zip 时,我们发现了上述列表中的文件。请参阅第 2 章,图 2.1 - 运行 unzip 实用程序解压缩 APK 文件。
在安装之前,Android 操作系统要求所有应用程序使用数字证书进行数字签名。在安装过程中,Android 操作系统使用包管理器验证 APK 是否已使用该 APK 中包含的证书正确签名。开发人员可以使用自签名证书签署他们开发的应用程序。开发人员生成证书并在生成发布构建之前使用它来签署应用程序。证书文件也是 APK 的一部分。
Android 应用程序的逆向工程
现在让我们来看看逆向工程 Android 应用程序的方法,并研究可以从编译的应用程序包中提取的内容。
在本节中,我们将使用一个名为 SecureStorage 的特别构建的应用程序。您可以从以下 GitHub 链接下载该应用程序的不同版本:
- SecureStorage 的调试版本(github.com/0ctac0der/S...
- SecureStorage 的发布版本(github.com/0ctac0der/S...
SecureStorage 是一个简单的 Android 应用程序,可用于在设备上存储信用卡信息。用户必须使用正确的密码登录才能访问存储的信息。
您可以将下载的应用程序安装到 Android 虚拟设备上,以查看其功能。以下是一些屏幕截图:
如果用户在应用程序中没有帐户,他们可以从"立即加入"屏幕创建一个帐户,如下所示:
登录后,用户可以选择保存信用卡、修改已存储的卡片以及查看以前添加的卡片。以下屏幕显示了所有这些选项:
对SecureStorage应用的重要要点:
- 该应用完全在客户端运行,这意味着没有后端或服务器端。
- SecureStorage试图在用户设备存储中安全存储数据。
到目前为止,我们已经详细了解了Android应用是如何开发和在设备上运行的,以及它们的内部组件,并且已经安装了一个应用(SecureStorage)。现在,让我们继续向前,开始对该应用进行逆向工程,并查看其中隐藏的内容。
提取Java源代码
提取Java源代码是逆向工程的首要目标,要尽可能准确地获取原始源代码。由于我们已经在Ubuntu虚拟机上下载了应用程序包,让我们使用JADX工具获取Java代码。不过,简单地解压APK并提取其内容也是一个不错的主意,以便查看其中的内容:
要使用JADX工具,请打开您解压JADX .zip文件的目录(如第2章"使用现代工具设置移动应用逆向工程环境"中所述)。进入该目录后,右键单击以选择"在终端中打开"选项。在打开的终端窗口中,输入以下命令运行JADX:
bash
cd bin/
./jadx-gui
在JADX窗口中,打开您刚刚下载的APK文件。有关如何执行此操作,请参阅第2章"使用现代工具设置移动应用逆向工程环境"中的说明:
在大多数应用程序中,可以将应用程序包反向工程到反编译的Java源代码。然而,在某些情况下,反编译的Java代码可能看起来不太清晰,或者Java反编译器中的多个部分根本无法读取。
重要提示: 从JADX反编译的Java代码通常无法重新编译。
当代码高度混淆或者Java代码难以获取时,我们可以将.dex文件(Dalvik字节码)转换为smali。 .dex文件包含二进制的Dalvik字节码,这些字节码非常难以阅读或修改,因此将这些字节码转换为更易读的格式smali非常有用。因此,通过从JADX获得的反编译代码来理解代码,然后使用smali版本来编辑其中的任何部分,然后重新编译它是一种方法。一对称为smali和baksmali的工具,它们在技术上是汇编器和反汇编器,可用于将smali代码转换为.dex格式,以及.dex转换为smali。
将DEX文件转换为smali
让我们尝试使用第二章中使用的另一个工具将相同的APK转换为smali文件,该工具是《使用现代工具设置移动应用程序逆向工程环境》。为了反编译APK,请运行以下命令:
arduino
# apktool d app-debug.apk
apktool在解编APK文件时内部使用smali/baksmali。以下图显示了apktool正在解码提供的app-debug.apk文件:
一旦APK被反编译,导航到创建的文件夹(在这种情况下是app-debug文件夹),你会在其中找到几个名称为smali*的子文件夹。这些文件夹包含APK中.dex文件转换而来的smali文件:
打开任何一个smali文件都会显示相应版本的代码。让我们查看classes5.dex文件的smali文件的内容。为此,我们需要导航到smali_classes5/com/example/securestorage/adapter目录:
shell
# cd smali_classes5/com/example/securestorage/adapter
以下图显示了该目录中的smali文件列表:
现在,我们可以使用cat实用程序读取smali文件的内容:
shell
# cat CardDetailsAdapter.smali
参考截图如下:
对于应用程序的相同部分CardDetailsAdapter,将 JADX 和 smali 中的代码进行比较也是一个很好的练习。
以下图显示了使用 JADX 获得的Java源代码与相应部分的smali代码之间的比较:
让我们总结一下我们到目前为止所做的工作:
- 使用诸如unzip之类的工具提取了APK的内容。这不是APK的反编译,而是内容的简单提取,遵循解压缩过程。这就是为什么编译的资源,例如AndroidManifest.xml,将无法读取。(参见图3.5。)
- 使用JADX工具从APK中获取了反编译的Java源代码。使用JADX中的这些反编译代码更容易理解应用程序的功能和不同类,例如。然而,如果我们需要修改此源代码的任何内容,重新编译将会非常困难。此外,JADX可能无法正确地将所有.dex文件转换为Java代码(以可读格式)。 (参见图3.6。)
- 使用apktool对APK进行了反编译,这也导致获取了代码的smali版本。与Java代码相比,smali格式相对容易修改和重新编译。(参见图3.7。)
我们还可以独立使用smali/baksmali工具将.dex文件转换为smali代码,反之亦然。为此,我们可以从ZIP提取的内容中获取任何.dex文件,并在其上运行baksmali。
让我们将classes4.dex文件复制到保存smali/baksmali工具的文件夹中:
现在,在同一文件夹中右键单击并选择"在终端中打开"选项,打开终端窗口。在终端窗口中,运行以下命令:
arduino
# java -jar baksmali-2.5.2.jar disassemble classes4.dex -o app
你会看到一个名为app的新目录被创建,并且它将包含位于app/com/example/securestorage/adapter中的smali文件:
通常,逆向工程用于寻找特定问题的解决方案,例如,"该应用程序如何存储用户信息?"或"该应用程序如何实现Root检测?"让我们看一个类似的案例,并了解何时可以在渗透测试期间使用逆向工程来发现安全问题。
逆向工程和渗透测试
由于我们已成功将一个APK逆向工程到了Java源代码,现在重要的是理解为什么逆向工程非常重要,以及在渗透测试期间可能需要它。通常,在渗透测试过程中(黑盒测试),渗透测试人员只能获得应用程序的名称。渗透测试人员下载应用程序到设备上并提取APK文件。
在某些情况下,仅仅使用应用程序并不明显地了解应用程序的某些功能是如何实现的。为了发现应用程序的漏洞,需要了解它是如何工作的。逆向工程有助于回答其中一些问题。
让我们举个例子。假设你被要求测试一个银行应用程序(渗透测试)。在使用应用程序时,你注意到应用程序实现了一种安全控制,即在将用户提交的所有数据值作为HTTP请求的一部分发送到后端服务器之前对其进行加密。
例如,捕获的登录请求(HTTP)可能如下所示:
bash
POST api/login HTTP/1.1
HOST: applicationdomainname.com
Content-Type: application/json
{
"email":" 41ZEyV2TFKvkjJwulP7I4hY8qEZaYagik2R6BHJFrPg= ",
"password":"crGTh+mckBpwBxXOKTQpWQ=="
}
在这种情况下,重要的是要了解如何加密电子邮件和密码的值,以创建作为HTTP请求的一部分发送的密文。这个问题的答案可能通过逆向工程应用程序并探索源代码来了解执行输入值加密的类来找到。
同样,还可能有其他一些例子,其中应用程序的不同功能具有复杂的实现方式。在所有这些情况下,逆向工程应用程序通常有助于理解逻辑。
此外,还有其他一些情况,其中在渗透测试期间逆向工程应用程序可能非常必要。其中一些情况如下:
- 查找应用程序发送到后端的API调用或端点。
- 了解某些安全控制的实现方式以绕过它们。例如,证书固定是许多移动应用程序中实施的安全控制,以确保应用程序只使用包内的证书建立TLS连接,不信任外部用户安装或系统安装的TLS证书。为了实现这一点,应用程序代码验证SSL握手期间呈现的TLS证书是否与存储在应用程序包内的证书相同。
- 在root检测期间执行的常见测试之一是验证设备上是否安装了名为SuperUser的应用程序。通过逆向工程应用程序,渗透测试人员可以找到应用程序执行的这些类型的测试。然后,他们可以修改相应的smali文件以返回错误结果,从而绕过root检测。
- 查找应用程序代码中硬编码的敏感信息,例如后门账户、API密钥和秘密、未发布的后端端点以及隐藏菜单。
- 查找代码中有趣的字符串。
- 查找加密和混淆点,以便可以解密和去混淆它们。 逆向工程还有助于了解应用程序的重要组件。通过逆向工程,渗透测试人员可以找到以下组件的详细信息:
- Activities:提供用户可以与之交互的屏幕的组件。例如,图3.2、图3.3和图3.4显示了SecureStorage应用程序的活动。
- Broadcast receivers:接收并响应来自其他应用程序或操作系统的广播消息的组件。
- Services:在后台执行操作的组件。
这些组件的大部分都列在AndroidMinfest.xml文件中,可以从JADX代码或smali文件中读取/探索相同的组件。让我们看一下SecureStorage应用程序的AndroidManifest.xml文件:
AndroidManifest.xml文件中提到这个应用程序有五个活动:
- com.example.securestorage.activity.LoginActivity
- com.example.securestorage.activity.HomeActivity
- com.example.securestorage.activity.SignupActivity
- com.example.securestorage.activity.SaveCardInfoActivity
- com.example.securestorage.activity.CardDetailsActivity
我们可以进一步探索任何这些活动组件的实现,如下图所示,在JADX中的相应部分:
我们可以选择一个活动并查看代码。让我们看看SaveCardInfoActivity是如何实现的:
有趣的是,如果你看下面的代码部分,你会发现应用程序在保存卡片详细信息到设备之前执行了某种加密操作。了解应用程序如何加密提交的数据以及是否可能在该部分找到弱点可能会很有趣:
kotlin
private void updateCardInfo() {
CardInfo cardInfo = new CardInfo();
cardInfo.setCardNumber(SaveDataUtils.getEncryptedData(this.etCardNumber.getText().toString()));
cardInfo.setCardExpiry(SaveDataUtils.getEncryptedData(this.etExpirationDate.getText().toString()));
cardInfo.setCardCvv(SaveDataUtils.getEncryptedData(this.etCVV.getText().toString()));
cardInfo.setCardHolderName(SaveDataUtils.getEncryptedData(this.etCardHolderName.getText().toString()));
SaveDataUtils.updateCardInfo(cardInfo, this.itemPosition);
CommonUtils.showToast(this, "Card Details Edit Successfully");
}
private void saveCardInfo() {
SaveDataUtils.addCardInfo(this.etCardNumber.getText().toString(), this.etExpirationDate.getText().toString(), this.etCVV.getText().toString(), this.etCardHolderName.getText().toString());
this.etCardNumber.setText("");
this.etExpirationDate.setText("");
this.etCVV.setText("");
this.etCardHolderName.setText("");
CommonUtils.showToast(this, "Card Details Saved Successfully");
}
还应该注意到,在utils部分中有一个EncryptionUtils。通过getEncryptedData函数(在SaveDataUtils中)调用EncryptionUtils:
java
public class EncryptionUtils {
public static final String password = "qkjll5@2md3gs5Q@";
public static SecretKey generateKey() {
return new SecretKeySpec(password.getBytes(), "AES");
}
public static byte[] encryptMsg(String message) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
SecretKey secret = generateKey();
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(1, secret);
return cipher.doFinal(message.getBytes("UTF-8"));
}
public static String decryptMsg(byte[] cipherText) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {
SecretKey secret = generateKey();
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(2, secret);
return new String(cipher.doFinal(cipherText), "UTF-8");
}
}
从上述代码中,我们可以看到该应用程序似乎在将数据保存到设备之前对其进行了高级加密标准(AES)加密。该加密是对称加密,使用相同的密钥加密和解密数据。该密钥也是反编译源代码的一部分。 这是一个安全问题 - 在应用程序代码本身中硬编码加密/解密密钥。密钥在以下行中被提及:
arduino
public static final String password = "qkjll5@2md3gs5Q@";
通过分析逆向工程的代码,可以发现多种不同类型的漏洞。例如,如果应用程序代码的某个部分允许来自其他应用程序的代码运行,则可以发现任意代码执行。通过逆向工程应用程序并分析其代码,可以发现这种类型的问题。
到目前为止,我们一直在使用应用程序的调试版本。调试版本并不总是包含发布版本应用程序所具有的所有安全控制。调试版本中最重要的缺失之一通常是代码混淆。
修改和重新编译应用程序
通常,不仅需要逆向工程应用程序,还需要进行一些修改,然后重新打包。要创建修改后的APK,您需要重新编译修改后的代码,然后再次对APK进行签名。
假设我们想要修改应用程序中的加密密钥,然后重新编译它。为此,您需要执行以下步骤:
- 对APK进行反编译。我们已经使用apktool对SecureStorage应用程序进行了反编译,使用了# apktool d app-debug.apk命令。
- 反编译过程将为我们提供所需的smali文件。因此,让我们打开EncryptionUtils.smali文件。
- 在这个smali文件中,我们可以将加密密钥的值更改为其他值,比如abcdef12345。
- 要重新编译应用程序,我们可以再次使用apktool。运行# apktool b命令。确保此命令在提取应用程序的同一目录中运行。它将在dist文件夹内编译新的APK。
重要提示
要在设备上安装此修改后的APK,我们将需要使用密钥进行签名。您可以使用keytool工具生成密钥,然后再次使用jarsigner对应用程序进行签名。
进行较小的更改,比如修改字符串或更改应用程序的一些静态元素,更容易,可以通过简单地按照上述步骤进行。然而,如果在smali代码中进行了较大的更改后要重新编译应用程序,您将需要遵循一些额外的建议。
Android应用程序中的代码混淆
代码混淆是一种修改代码的过程,旨在保护知识产权并使其难以逆向工程。代码混淆只修改方法指令或元数据;它不会改变代码操作的逻辑/流程或输出。
Android恶意软件也以利用混淆技术来隐藏其恶意行为而闻名。然而,混淆也可以被打败。一名熟练的逆向工程师可以抵制实施的混淆技术,仍然可以找到应用程序代码中的有趣部分。
开发人员可以使用Android Studio中提供的默认混淆工具ProGuard,也可以使用市场上提供的第三方混淆工具。根据使用的混淆类型,应调整解混淆技术。ProGuard是一个开源命令行工具,可用于混淆Java代码。
解混淆DEX字节码的一种方法是识别并使用应用程序中的解淆方法。这可以通过在其他要解混淆的类上运行解混淆方法(实现的解混淆方法代码)的Java代码来完成。
为了更好地理解,您可以从以下链接下载SecureStorage应用程序的发布版本:
- SecureStorage(发布版本):github.com/0ctac0der/S...。该发布版本使用ProGuard对其进行了基本级别的混淆。让我们使用JADX对其进行反编译。按照"提取Java源代码"部分中的相同步骤进行。
您可以看到类名已经被修改为随机字母。进一步分析时,您会注意到utils部分似乎不再包含除了CommonUtils之外的类。但是,为了使应用程序正常运行,加密类和密钥必须存在于代码中。 可以进一步探索逆向工程源代码,找到EncryptionUtils类所在的正确位置:
我们可以注意到,即使代码已经被混淆,应用程序中使用的加密密钥仍然是相同的。这是因为混淆是以一种不改变应用程序功能的方式进行的。 前面的混淆代码是ProGuard混淆的结果,基于以下规则:
ini
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class com.example.securestorage.utils.CommonUtils { *; }
ProGuard规则指定了com.example.securestorage.utils.CommonUtils应保持原样,而应用程序的其余代码应进行混淆。这正是您在应用程序发布版本的JADX反编译代码中所看到的。
在执行渗透测试时,通常不需要解混淆整个代码。通常,您只需要理解代码逻辑,或只解混淆代码的一部分。还有一些解混淆工具可用,有时如果您真的被应用程序代码的某一部分所困扰,解混淆工具可能会有所帮助,不过我建议通过手动分析代码来更好地理解它;只在万不得已时使用解混淆工具。