Android 原生组件模糊测试简介:工具创建策略

admin 2025年2月4日21:01:07评论5 views字数 6349阅读21分9秒阅读模式

Android 原生组件模糊测试简介:工具创建策略

在上一篇文章中,我们介绍了 Android 应用程序市场,探讨了基本的模糊测试概念,讨论了本机方法在 Android 应用程序中的工作方式,并介绍了创建一个简单的线束来演示 AFL++ 的基本功能。如果您错过了这些内容,可以通过此链接获取该文章。在这篇新文章中,我们将探索一个真实世界的应用程序,并讨论在线束构建过程中采用的一些策略。

为了撰写本文,我们最初开发了一个脚本来下载几个 APK 文件,作为我们测试的基础。所选的应用程序是一个简单的图像转换器,是根据特定标准选择的。我们重点介绍以下选择的原因:

  • 为动态库中的目标函数创建线束相对简单,使我们能够将整个过程浓缩为一篇文章。

  • 在上一篇文章中,我们讨论了 JNI 方法需要两个必需参数:JNIEnv 和 jobject/jclass。Android 运行时会初始化这些结构,这使得创建线束更加复杂。这种复杂性源于需要实例化 JVM 并确保其保持一致状态。此状态对于 JNIEnv 中指针的有效性至关重要,因为如果线束尝试访问无效指针,它将失败。为了避免第二种情况,采用的策略是选择 JNI 方法调用的本机函数作为模糊测试目标,重新创建线束中 JNI 方法执行的步骤。虽然这增加了一层额外的复杂性,但它消除了对有效 JNIEnv 指针的依赖。下一篇文章将讨论和克服这一限制。

JNIEXPORT jstring JNICALL
Java_com_conviso_example_jni_HelloJni_stringFromJNI(
     JNIEnv* env,
     jobject thiz
)
{
     return (*env)->NewStringUTF(env, "Hello from JNI!");
}

撰写本文的动机是创建一种日志,记录开发线束过程中面临的挑战和采用的策略。目标是彻底记录通过模糊测试识别潜在漏洞所采取的步骤,清晰地展示整个过程、遇到的困难以及在此过程中实施的解决方案。那么,让我们开始吧。

1. 从Java层获取初始数据

提取应用程序的 APK 文件后,检查是否存在库文件夹结构中的目录。在本例中,名为图像魔法库被识别,使我们能够继续进行分析。如果存在动态库,请在 Java 反编译器(如 JADX)中打开 APK,并检查包含静态初始化块的类,如上一篇文章所述。一种策略是查找对系统.loadLibrary()方法,它有助于识别使用本机方法的类。下面,我们能够识别魔法类声明了三个本机方法。

Android 原生组件模糊测试简介:工具创建策略

继续进行应用程序代码分析,我们发现了魔法图像类,它继承自魔法类并声明额外的本机方法。

Android 原生组件模糊测试简介:工具创建策略

良好的模糊测试目标通常是处理复杂或模糊数据结构的本机函数,例如文件解析器。理想情况下,这些函数应该处理可由攻击者控制的输入,例如通过套接字或文件等不受信任的渠道接收的数据。这些函数通常处理数据格式或验证,因此在处理意外或格式错误的输入时容易失败。

在寻找模糊测试的本地方法的过程中,我们发现读取图像方法,这似乎是进行分析的绝佳候选。下一步将是在反汇编程序中检查此方法的代码,以了解其行为。

Android 原生组件模糊测试简介:工具创建策略

可以静态地识别 Java 层调用的本机方法,方法是搜索具有本国的关键字,或者动态地使用像jnitrace(基于 Frida)这样的工具在运行时跟踪调用。

2. 使用反汇编器进行本机代码分析

在 Java 层中识别本机方法后,下一步是在反汇编程序(例如 Ghidra 或 IDA Pro)中打开库,并分析 JNI 方法代码。在本文中,我们将使用 Ghidra。如上一篇文章所述,JNI 方法通常由前缀标识Java,后跟 Java 包结构、类和方法名称,全部用下划线分隔。我们将在 Ghidra 中搜索的方法具有以下签名:Java_magick_MagickImage_readImage。反编译代码后,我们发现了以下代码片段:

Android 原生组件模糊测试简介:工具创建策略

在执行逆向工程时,对函数使用的数据和结构的理解越好,代码的可视化和导航就越好,从而提高了分析的准确性。这也有助于流程的自动化和漏洞的识别。IDA Pro 和 Ghidra 等反汇编程序允许导入新的数据类型,从简单类型到 JNI 方法使用的复杂结构(如 JNIEnv)。在 IDA Pro 中,只需导入jni.h标头添加了必要的类型。在 Ghidra 中,GDT(Ghidra 数据类型)文件存储自定义类型的定义。要分析 JNI 方法,我们可以导入jni_all.gdt文件并开始使用定义的类型。导入后,可以修改 JNI 方法签名以包含通过分析 Java 代码获得的参数 JNIEnv env、jobject thiz 和 imageInfo。虽然在分析的方法中反编译器输出没有发生显着变化,但在更复杂的情况下,JNIEnv 中指针的使用非常密集,导入类型可以大大提高对代码的理解。

Android 原生组件模糊测试简介:工具创建策略

我们可以观察到,JNI 方法检索 ImageInfo 结构并直接将其传递给读取图像函数。掌握了这些信息后,让我们进一步研究图像魔术师图书馆,以更深入地了解如何读取图像从而促进了线束的发展。

3. 项目文档和测试目录中的信息

如果所分析的库是开源的,我们可以浏览其文档,其中通常包含有关其功能、数据结构和执行流程的信息。文档对于理解库的预期行为和确定模糊测试的潜在入口点至关重要。此外,它还可以提供有关配置、依赖项和使用示例的详细信息,从而更容易在模糊测试过程中创建更有效的测试用例并分析结果。有关更多信息,我们推荐 Salim Largo 撰写的优秀帖子:利用库进行有效模糊测试。

项目存储库可能包含专用于测试用例的目录,您可以在其中找到用于模糊测试过程的线束示例、输入语料库和其他有用的工件。此外,这些目录可能包含用于练习库不同部分的执行配置,以及如何将线束与分析的库集成的现成示例。

如果存储库不包含此信息,另一种方法是在互联网上搜索库的使用示例或查阅专门论坛中的教程和讨论。如有必要,还可以通过分析库的二进制代码或使用调试工具进行逆向工程,以更好地了解库的行为。

对 ImageMagick 库进行研究后,下面提供了开发的线束的代码。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <magick/MagickCore.h>
 
int main(int argc, char **argv) {
    InitializeMagick(*argv);
 
    ExceptionInfo *exception = AcquireExceptionInfo();
    ImageInfo *image_info = CloneImageInfo(NULL);
    strcpy(image_info->filename, argv[1]);
 
    Image *image = ReadImage(image_info, exception);
    if (exception->severity != UndefinedException) {
        CatchException(exception);
        return EXIT_FAILURE;
    }
 
    printf("Image width: %lun", image->columns);
    printf("Image height: %lun", image->rows);
     
    image = DestroyImage(image);
    image_info = DestroyImageInfo(image_info);
    DestroyExceptionInfo(exception);
     
    DestroyMagick();
 
    return EXIT_SUCCESS;
}
4. Android 库中的初始化函数
在分析库的过程中,查找初始化函数(例如 libmp3lame 中的 lame_init() 或 PDFium 中的 FPDF_InitLibrary())非常重要,因为执行这些函数对于库的正常运行至关重要。如果在调用其他本机方法之前未调用这些初始化函数,则线束可能会失败甚至导致崩溃。
在Android应用程序的Java层,具有本机方法的类的构造函数可以指示必要的初始化函数,以确保正确执行。
对于 ImageMagick 库来说,初始化魔法函数必须在调用之前调用读取图像功能。
InitializeMagick(*argv);
5. 线束创建中的标头版本和结构偏移
如果库是开源的,则必须确保线束使用的库头与应用程序中使用的 Android 库版本完全匹配。不一致的数据(例如复杂结构的不同版本)可能会导致线束执行时出现意外结果或失败。

例如,展示了一个在更新后字段被修改的结构。如果函数复制事务(数据传输*)在 Android 库中,需要按照CorrectHeader.h文件,但我们提供的版本来自错误头文件,字节计算内存复制操作将不正确。具体来说,当函数尝试访问字节计数(由數量字段中的数据传输结构),基于偏移计算[8+16](在哪里x8是结构的基地址),新字段将被解释为要复制的字节数。这可能会导致内存损坏并导致线束执行失败。

Android 原生组件模糊测试简介:工具创建策略

以下命令概述了使用 afl-qemu-trace 编译工具和运行测试的步骤。需要注意的是,ImageMagick 标头来自最新版本,正如下面将要演示的那样,它与 Android 应用程序使用的库的版本不对应。

$ aarch64-linux-android35-clang -o harness harness.c

下图是运行 afl-qemu-trace 的结果,可以看出由于偏移量不正确,进程无法完成执行,进入阻塞状态。

Android 原生组件模糊测试简介:工具创建策略

要获取有关库版本的信息,我们可以查阅 ELF 文件、搜索特定字符串或使用直接返回版本的函数。在我们的测试中,ImageMagick 库提供了获取魔法版本()函数,可用于检索库的版本。通过临时修改线束代码,我们可以获取库版本。

#include <stdio.h>
#include <stdlib.h>
#include <magick/MagickCore.h>
 
int main(int argc, char **argv) {
    size_t length;
    const char *version = GetMagickVersion(&length);
    printf("ImageMagick version: %.*sn", (int)length, version);
    return EXIT_SUCCESS;
}

可执行文件的输出显示,Android 库中使用的 ImageMagick 版本是 6.7.3-0,这是一个过时的版本。恢复工具代码并使用正确的标头重新编译后,工具就可以正常工作了。

Android 原生组件模糊测试简介:工具创建策略

有了库版本,我们可以下载正确的标题,恢复以前的线束代码,并使用以下命令重新编译它。

$ aarch64-linux-android35-clang -o harness harness.c -I/home/thiago/Conviso/Android-ImageMagick/jni/ImageMagick-6.7.3-0/ -DMAGICKCORE_HDRI_ENABLE=1 -DMAGICKCORE_QUANTUM_DEPTH=16 -limagemagick -L .
$ export QEMU_SET_ENV=LD_LIBRARY_PATH="/home/thiago/Conviso/Fuzzing"
$ export QEMU_LD_PREFIX="/home/thiago/Conviso/qiling/examples/rootfs/arm64_android"
$ afl-qemu-trace ./harness ./afl_in/apple.png

运行 afl-qemu-trace 后,我们可以确认线束正常工作。

Android 原生组件模糊测试简介:工具创建策略

当库不是开源的,并且函数使用复杂的结构时,需要通过逆向工程来验证函数使用的结构字段的偏移量。如果结构字段偏移量不遵循默认的体系结构对齐,则必须确保__attribute__((打包))属性应用于线束代码中的结构声明。此属性指示编译器不要在结构成员之间添加填充(这通常是由编译器添加的,以优化内存访问性能等因素),从而防止出现错误的偏移计算。

例如,假设我们要使用模糊测试的函数接收一个复杂结构作为参数,但仅使用位于结构基址后 58 个字节的内容。在这种情况下,我们可以忽略前面的字节,只关注表示数据的字段。该结构定义如下:

typedef struct {
    char dummy[58];
    void *ptr;
} RandomStruct;

为了验证编译器如何定义结构,让我们创建一个简单的示例并检查函数的代码f(随机结构*),它接收指向随机结构作为参数并返回指针结构体的字段,如下所示。

void *f(RandomStruct *r) {
    return r->ptr;
}

下图说明了访问 ptr 字段时生成的代码在有和没有__attribute__((打包))结构上设置的属性。可以观察到,出于优化原因,当省略该属性时,编译器会调整指针到结构基地址的 64 个字节。应用此属性时,指针根据需要,变为距离基地址 58 个字节。

Android 原生组件模糊测试简介:工具创建策略

6. 耐心是一种美德

不要过早中断模糊测试过程,这一点至关重要,尤其是在 AFL++ 继续探索新的代码路径时。在测试应用程序的情况下,我们用一些小图像配置了语料库目录,并让模糊测试器运行了 5 个小时。在此期间,AFL++ 能够识别出两次崩溃。

以下是使用 AFL++ 开始模糊测试的命令。如果需要复习任何命令,请参阅上一篇文章。

$ export QEMU_SET_ENV=LD_LIBRARY_PATH="/home/thiago/Conviso/Fuzzing"
$ export QEMU_LD_PREFIX="/home/thiago/Conviso/qiling/examples/rootfs/arm64_android"
$ AFL_INST_LIBS=1 AFL_QEMU_FORCE_DFL=1 afl-fuzz -Q -i afl_in/ -o afl_out/ -- ./harness @@

Android 原生组件模糊测试简介:工具创建策略

7. 系统日志和调试

在 AFL++ 识别出线束中的崩溃后,我们可以使用 afl-qemu-trace 确认故障,并将导致崩溃的输入作为参数传递。在下图中,可以验证此输入是否导致了线束中的分段错误。

Android 原生组件模糊测试简介:工具创建策略

在实际应用中测试输入时,我们可以通过logcat工具依赖系统日志。Logcat是一个显示系统和应用程序日志的Android工具,有助于错误调试和实时事件监控。

为了在应用程序中测试输入,我们将文件重命名为常见的图像扩展名,例如 .PNG,并要求应用程序将图像转换为其他格式。转换开始时,应用程序重新启动。通过分析 logcat 中的输出,我们能够确认应用程序中发生了崩溃。

Android 原生组件模糊测试简介:工具创建策略

在回溯输出中,我们可以看到输入导致了一个问题内存复制,称为读取图像函数,这是我们选择作为AFL++目标的函数。

Android 原生组件模糊测试简介:工具创建策略

结论

总之,本文介绍的技巧有助于构建有效的工具来模糊测试 Android 应用程序中的本机代码,涵盖从代码准备到结果分析的所有内容。通过将 AFL++ 与 QEMU 相结合,可以优化故障检测并采用更全面的 Android 应用程序安全分析方法。此外,集成调试技术(例如使用 logcat 和跟踪工具)可以更详细地调查崩溃,从而有助于识别关键漏洞。

在下一篇文章中,我们将解决与 JVM 初始化、对 JNIEnv 指针的一致访问以及在模糊测试工具上下文中与 Java 层通信相关的问题。这些解决方案将有助于消除在工具代码中重新创建 JNI 方法行为的需要,而本文中则需要这样做。

原文始发于微信公众号(Ots安全):Android 原生组件模糊测试简介:工具创建策略

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月4日21:01:07
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Android 原生组件模糊测试简介:工具创建策略https://cn-sec.com/archives/3698362.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息