跳过内容

MASTG-TECH-0018: 反汇编原生代码

Dalvik 和 ART 都支持 Java 本地接口 (JNI),它定义了一种 Java 代码与 C/C++ 编写的原生代码交互的方式。与其他基于 Linux 的操作系统一样,原生代码被打包(编译)到 ELF 动态库 (*.so) 中,Android 应用程序在运行时通过 System.load 方法加载这些库。然而,Android 二进制文件不是依赖于广泛使用的 C 库(例如 glibc),而是基于一个名为 Bionic 的自定义 libc 构建的。 Bionic 添加了对重要的 Android 特定服务的支持,例如系统属性和日志记录,但它并不完全与 POSIX 兼容。

当逆向包含原生代码的 Android 应用程序时,我们需要了解一些与 Java 和原生代码之间的 JNI 桥相关的数据结构。从逆向的角度来看,我们需要了解两个关键数据结构:JavaVMJNIEnv。它们都是指向函数表指针的指针

  • JavaVM 提供了一个接口来调用创建和销毁 JavaVM 的函数。 Android 每个进程只允许一个 JavaVM,这与我们的逆向目的并不太相关。
  • JNIEnv 提供了对大多数 JNI 函数的访问,这些函数可以通过 JNIEnv 指针以固定的偏移量访问。这个 JNIEnv 指针是传递给每个 JNI 函数的第一个参数。我们将在本章后面借助示例再次讨论这个概念。

值得强调的是,分析反汇编的原生代码比反汇编的 Java 代码更具挑战性。当逆向 Android 应用程序中的原生代码时,我们需要一个反汇编器。

在下一个示例中,我们将逆向来自 OWASP MASTG 存储库的 HelloWorld-JNI.apk。在模拟器或 Android 设备中安装和运行它是可选的。

wget https://github.com/OWASP/mastg/raw/master/Samples/Android/01_HelloWorld-JNI/HelloWord-JNI.apk

这个应用程序并不完全壮观,它所做的只是显示一个带有文本“Hello from C++”的标签。这是 Android 在您创建一个支持 C/C++ 的新项目时默认生成的应用程序,这足以显示 JNI 调用的基本原理。

使用 apkx 反编译 APK。

$ apkx HelloWord-JNI.apk
Extracting HelloWord-JNI.apk to HelloWord-JNI
Converting: classes.dex -> classes.jar (dex2jar)
dex2jar HelloWord-JNI/classes.dex -> HelloWord-JNI/classes.jar
Decompiling to HelloWord-JNI/src (cfr)

这会将源代码提取到 HelloWord-JNI/src 目录中。主活动可以在文件 HelloWord-JNI/src/sg/vantagepoint/helloworldjni/MainActivity.java 中找到。“Hello World”文本视图在 onCreate 方法中填充

public class MainActivity
extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        this.setContentView(2130968603);
        ((TextView)this.findViewById(2131427422)).setText((CharSequence)this. \
        stringFromJNI());
    }

    public native String stringFromJNI();
}

请注意底部的 public native String stringFromJNI 的声明。关键字“native”告诉 Java 编译器该方法是用原生语言实现的。相应的函数在运行时解析,但前提是加载了一个导出具有预期签名的全局符号的原生库(签名包含包名、类名和方法名)。在本例中,以下 C 或 C++ 函数满足此要求

JNIEXPORT jstring JNICALL Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI(JNIEnv *env, jobject)

那么这个原生函数的实现在哪里呢?如果查看解压的 APK 存档的“lib”目录,您会看到几个子目录(每个支持的处理器架构一个),每个子目录都包含一个原生库的版本,在本例中是 libnative-lib.so。当调用 System.loadLibrary 时,加载器会根据应用程序运行的设备选择正确的版本。在继续之前,请注意传递给当前 JNI 函数的第一个参数。它是前面在本节中讨论过的同一个 JNIEnv 数据结构。

按照上面提到的命名约定,您可以期望该库导出一个名为 Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI 的符号。在 Linux 系统上,您可以使用 readelf(包含在 GNU binutils 中)或 nm 检索符号列表。在 macOS 上使用 greadelf 工具执行此操作,您可以通过 Macports 或 Homebrew 安装它。以下示例使用 greadelf

$ greadelf -W -s libnative-lib.so | grep Java
     3: 00004e49   112 FUNC    GLOBAL DEFAULT   11 Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI

您也可以使用 radare2 的 rabin2

$ rabin2 -s HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so | grep -i Java
003 0x00000e78 0x00000e78 GLOBAL   FUNC   16 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI

这是在调用 stringFromJNI 本地方法时最终执行的原生函数。

要反汇编代码,您可以将 libnative-lib.so 加载到任何理解 ELF 二进制文件的反汇编器中(即,任何反汇编器)。如果应用程序附带针对不同架构的二进制文件,理论上您可以选择您最熟悉的架构,只要它与反汇编器兼容即可。每个版本都从相同的源代码编译而来,并实现相同的功能。但是,如果您计划稍后在实时设备上调试该库,通常明智的做法是选择 ARM 构建。

为了支持较旧和较新的 ARM 处理器,Android 应用程序附带针对不同应用程序二进制接口 (ABI) 版本编译的多个 ARM 构建。 ABI 定义了应用程序的机器代码在运行时应如何与系统交互。支持以下 ABI

  • armeabi:ABI 适用于至少支持 ARMv5TE 指令集的基于 ARM 的 CPU。
  • armeabi-v7a:此 ABI 扩展了 armeabi 以包含多个 CPU 指令集扩展。
  • arm64-v8a:ABI 适用于支持 AArch64(新的 64 位 ARM 架构)的基于 ARMv8 的 CPU。

大多数反汇编器都可以处理这些架构中的任何一种。下面,我们将在 radare2 和 IDA Pro 中查看 armeabi-v7a 版本(位于 HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so 中)。请参阅 查看反汇编的原生代码 以了解如何在检查反汇编的原生代码时进行操作。

radare2

要在 Android 的 radare2 中打开该文件,您只需运行 r2 -A HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so。章节“Android 基本安全测试”已经介绍了 radare2。请记住,您可以使用标志 -A 在加载二进制文件后立即运行 aaa 命令,以便分析所有引用的代码。

$ r2 -A HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so

[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Finding xrefs in noncode section with anal.in=io.maps
[x] Analyze value pointers (aav)
[x] Value from 0x00000000 to 0x00001dcf (aav)
[x] 0x00000000-0x00001dcf in 0x0-0x1dcf (aav)
[x] Emulate code to find computed references (aae)
[x] Type matching analysis for all functions (aaft)
[x] Use -AA or aaaa to perform additional experimental analysis.
 -- Print the contents of the current block with the 'p' command
[0x00000e3c]>

请注意,对于较大的二进制文件,直接使用标志 -A 启动可能非常耗时且不必要。根据您的目的,您可以不使用此选项打开二进制文件,然后应用不太复杂的分析(如 aa)或更具体的分析类型(如 aa(所有函数的基本分析)或 aac(分析函数调用)中提供的分析类型)。请记住始终键入 ? 以获取帮助,或将其附加到命令以查看更多命令或选项。例如,如果您输入 aa?,您将获得完整的分析命令列表。

[0x00001760]> aa?
Usage: aa[0*?]   # see also 'af' and 'afna'
| aa                  alias for 'af@@ sym.*;af@entry0;afva'
| aaa[?]              autoname functions after aa (see afna)
| aab                 abb across bin.sections.rx
| aac [len]           analyze function calls (af @@ `pi len~call[1]`)
| aac* [len]          flag function calls without performing a complete analysis
| aad [len]           analyze data references to code
| aae [len] ([addr])  analyze references with ESIL (optionally to address)
| aaf[e|t]            analyze all functions (e anal.hasnext=1;afr @@c:isq) (aafe=aef@@f)
| aaF [sym*]          set anal.in=block for all the spaces between flags matching glob
| aaFa [sym*]         same as aaF but uses af/a2f instead of af+/afb+ (slower but more accurate)
| aai[j]              show info of all analysis parameters
| aan                 autoname functions that either start with fcn.* or sym.func.*
| aang                find function and symbol names from golang binaries
| aao                 analyze all objc references
| aap                 find and analyze function preludes
| aar[?] [len]        analyze len bytes of instructions for references
| aas [len]           analyze symbols (af @@= `isq~[0]`)
| aaS                 analyze all flags starting with sym. (af @@ sym.*)
| aat [len]           analyze all consecutive functions in section
| aaT [len]           analyze code after trap-sleds
| aau [len]           list mem areas (larger than len bytes) not covered by functions
| aav [sat]           find values referencing a specific section or map

关于 radare2 与其他反汇编器(如 IDA Pro)相比,有一点值得注意。以下引述来自 radare2 博客的这篇 文章https://radareorg.github.io/blog/),它提供了一个很好的总结。

代码分析不是一个快速的操作,甚至不是可预测的或花费线性时间来处理的。这使得启动时间非常繁重,与默认情况下仅加载标头和字符串信息相比。

习惯使用 IDA Pro 或 Hopper 的人们只需加载二进制文件,出去喝杯咖啡,然后在分析完成后,他们就开始进行手动分析,以了解程序在做什么。诚然,这些工具在后台执行分析,并且 GUI 没有被阻塞。但这需要大量的 CPU 时间,并且 r2 的目标是在比高端台式计算机更多的平台上运行。

也就是说,请参阅 查看反汇编的原生代码 以了解有关 radare2 如何帮助我们更快地执行逆向任务的更多信息。例如,获取特定函数的反汇编是一个简单的任务,可以在一个命令中执行。

IDA Pro

如果您拥有 IDA Pro 许可证,请打开该文件,并在“加载新文件”对话框中,选择“ARM 的 ELF(共享对象)”作为文件类型(IDA 应该会自动检测到),并选择“ARM Little-Endian”作为处理器类型。

不幸的是,IDA Pro 的免费软件版本不支持 ARM 处理器类型。