白帽故事 · 2025年4月10日 0

Android 原生组件模糊测试

环境:
Ubuntu 20.04 x86/64
AFL++

下载NDK

https://developer.android.com/ndk/downloads

下载后解压,设置环境路径:

~/.bashrc文件中增加:

`export PATH="/home/bugchong/Downloads/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"

image.png

安装Qiling

Qiling 框架是一个基于 Unicorn 的开源二进制仿真和模拟器框架,Unicorn 是一个 CPU 仿真器,仅限于仿真原始指令而不考虑操作系统上下文。虽然 Unicorn 处理低级仿真,但 Qiling 框架负责高级任务,包括支持不同的可执行文件格式、动态链接器以及系统调用和输入/输出处理程序。这使得 Qiling 能够执行通常需要原生操作系统才能运行的二进制文件。由于 Qiling 框架支持 ARM64 Android 二进制文件,我们可以在项目结构中找到这些动态库。

$ git clone https://github.com/qilingframework/qiling
$ cd qiling
$ git submodule update --init --recursive

下载 Qiling 框架后,我们可以在目录 examples/rootfs/arm64_android/system 中验证运行应用程序所需的文件是否存在:

image.png

安装及编译AFL++

根据官方指导手册,安装相关依赖:

sudo apt-get update
sudo apt-get install -y build-essential python3-dev automake cmake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools cargo libgtk-3-dev
# try to install llvm 14 and install the distro default if that fails
sudo apt-get install -y lld-14 llvm-14 llvm-14-dev clang-14 || sudo apt-get install -y lld llvm llvm-dev clang
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-dev
sudo apt-get install -y ninja-build # for QEMU mode
git clone https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
make distrib
sudo make install

需要注意的是,如果你需要调试目标应用,必须取消注释调用 signal_init() 的那行代码。否则,调试器将无法接收到应用执行过程中发出的信号。

AFLplusplus/qemu_mode/qemuafl/linux-user/main.c: 注释掉对 signal_init() 函数的调用。该脚本运行的命令可能会覆盖本地修改,因此为了确保 main.c 中的更改不会丢失,在运行脚本之前必须注释掉这些命令。

image.png

AFLplusplus/qemu_mode/build_qemu_support.sh: 注释掉 QEMU 构建脚本中的所有 git 命令。该脚本运行的命令可能会覆盖本地修改,因此为了确保 main.c 中的更改不会丢失,在运行脚本之前必须注释掉这些命令。

image.png

修改完成后 ,使用如下命令编译QEMU支持:

sudo apt install ninja-build
cd qemu_mode
CPU_TARGET=aarch64 ./build_qemu_support.sh

image.png

Fuzzing

假设我们可以访问 Android 应用程序的动态库,下面展示了简化的 C 代码(选择这种方法来简化过程可以避免处理 ARM64 代码的逆向工程复杂性)。

在实际情况下,需要在动态库中识别一个合适的目标函数,并使用反汇编工具如 IDA Pro、Ghidra、radare2 或 Hopper 进行代码分析。strcpy 函数将源字符串复制到目标位置,但如果目标位置没有足够的空间存储源字符串,可能会导致内存损坏,从而引起内存泄露和潜在的安全漏洞。

#include <stdio.h>
#include <string.h>

int checkBuffer(const char *data)
{
        char localBuffer[256];
        if (data[0] == 'c') {
                if (data[1] == 'o') {
                        if (data[2] == 'n') {
                                if (data[3] == 'v') {
                                        strcpy(localBuffer, data);
                                        return 0;
                                }
                        }
                }
        }
        return 1;
}

然后使用下面的命令生成ARM架构的so文件:

$ aarch64-linux-android35-clang libfuzzconviso.c -o libfuzzconviso.so -shared -fPIC

在获取并分析动态库之后,我们将创建一个装载动态库并使用 AFL++生成的变异输入作为参数传递给目标函数的框架。

在本实验中,为 checkBuffer 函数。AFL++将监控框架的行为,以识别应用程序中的潜在故障或崩溃。框架将获取由 AFL++生成的文件路径,将其内容读入缓冲区,并传递给目标函数。

#include <stdio.h>

extern int checkBuffer(char *);

int main(int argc, char *argv[])
{
        char buffer[4096];
        FILE *fp = fopen(argv[1], "r");
        fread(buffer, 4096, 1, fp);
        checkBuffer(buffer);
        fclose(fp);
        return 0;
}

使用命令进行编译:

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

有了应用的动态库和测试框架(harness),还需要配置两个环境变量:

$ export QEMU_LD_PREFIX="/home/thiago/Conviso/qiling/examples/rootfs/arm64_android/"
$ export QEMU_SET_ENV=LD_LIBRARY_PATH="/home/thiago/Conviso/workspace/"
  • QEMU_LD_PREFIX: 配置 QEMU 查找模拟架构共享库的位置。此变量必须设置为 Android 的/system 目录路径,该路径包含在 Qiling 框架中,允许在 QEMU 中执行 Android 编译的二进制文件,Android 的/system 目录包含操作系统文件,包括二进制文件、库和系统应用程序。

  • QEMU_SET_ENV: 配置由 QEMU 模拟过程的环境变量,在使用 AFL++和 QEMU 进行 fuzzing 的上下文中,我们将指定 LD_LIBRARY_PATH 环境变量,该变量定义了测试框架使用的附加动态库路径,例如 libfuzzconviso.so 所在的目录。

$ mkdir afl_in
$ echo "AAAA" > afl_in /input.txt

然后创建一个目录来存储 fuzzing 过程中使用的语料,在实际场景中,语料库通常会根据要 fuzz 的目标类型来选择,以最大化覆盖率和 fuzzing 效率。

对于本实验的 checkBuffer 示例,一个简单的文本文件就足够了。

最初,该目录中将只包含一个文本文件,该文件代表将传递给 checkBuffer 函数的缓冲区内容。AFL++ 将修改该文件,并将其作为输入提供给 Android 应用程序动态库中的目标函数,通过测试框架来实现。

准备好语料后,我们就可以使用以下命令通过 afl-fuzz 开始 fuzzing 了:

$ AFL_INST_LIBS=1 afl-fuzz -Q -i afl_in/ -o afl_out/ -- ./harness @@
  • AFL_INST_LIBS=1 :此环境变量配置 AFL++ 以检测动态库中包含的代码,由于我们的目标是检测库文件 ,因此需要设置这个环境变量。
  • -Q :此选项告诉 AFL++ 使用 QEMU 的检测模式来监视和分析目标应用程序在执行期间的行为
  • -i afl_in/ :指定 AFL++ 用于模糊测试过程的语料库文件所在的输入目录
  • -o afl_out/ :定义 AFL++ 存储模糊测试结果的目录,例如导致崩溃的测试用例和执行日志
  •  :为分隔符,标记 AFL-fuzz 选项的结束和要执行的目标应用程序命令行的开始,在本例中为harness
  • ./harness :指定在模糊测试过程中要执行的应用程序路径
  • @@ :这是 AFL++ 使用的占位符,它将被替换为fuzzer在执行期间生成的输入文件的路径

首次运行 AFL++ 时,可能会遇到以下错误:

image.png

要解决该问题,只需要使用root权限输入:

echo core > /proc/sys/kernel/core_pattern 即可。

顺利开启Fuzz:

image.png

当运行一段时间后,就会出现crash,crash样本会被保存在./afl_out/default/crashes目录中,我们可以使用以下命令通过afl-qemu-trace 检测应用程序的执行情况,从而使我们能够监视输入是否导致任何错误或失败:

AFLplusplus/afl-qemu-trace ./harness afl_out/default/crashes/id\:000000\,src\:001035\,time\:1046267\,execs\:1570466\,op\:havoc\,rep\:3

image.png

通过上图,可以看到crash样本的输入导致了应用程序中出现段错误(Segmentation fault)。

参考资料

https://blog.convisoappsec.com/en/introduction-to-fuzzing-android-native-components/