提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

一切起源于想要实现对android so的模糊测试。两个备选方案,一个是frida,另一个是AFL。考虑到frida需要应用执行并出发到目标so接口,有点间接。因此,先尝试用AFL对android so文件进行模糊测试。
而AFL实现对android so的模糊测试有两种思路:一是在android环境下,进行afl模糊测试;二是在pc环境下通过模拟android环境实现afl对so的模糊测试。

本篇主要是为了实现思路一。具体描述了,在ubuntu环境下,通过ndk交叉编译生成适用于android环境下的so文件和c可执行程序,然后利用android-afl实现对so文件的fuzz测试。


一、p7zip源代码下载以及ndk交叉编译

参考论文black-box-fuzzing-android-native-libraries。在apk文件中存在用于压缩的so包libp7zip.so。
本篇以此为目标(因为参考论文就是以此为目标),从github上下载了相关apk文件和代码(https://github.com/hzy3774/AndroidP7zip)。下载解压缩,进入libp7zip/src/main/cpp/目录,通过分析p7zip.cpp文件可知,apk文件中java层调用的executeCommand方法是经过封装的。真正的源码只在libp7zip/src/main/cpp/p7zip目录中。
(需要说明,之所以不能直接将apk文件中的libp7zip.so包提取出来,是因为对于executeCommand方法而言,还存在JNIEnv
,不知道用C程序wrapper这个so时应该怎么处理,所以我这边选择直接编译源码来生成libp7zip.so,但事实是后面函数能调用起来,但是解压缩功能不可用)

接下来,对其源代码进行编译。
1.在libp7zip/src/main/cpp/p7zip目录中编写Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

SRC_SUFFIX := *.cpp *.c
ALL_FILES := $(shell find $(LOCAL_PATH) -type f)
SRC_FILES := $(filter $(subst *,%,$(SRC_SUFFIX)),$(ALL_FILES))
LOCAL_SRC_FILES := $(SRC_FILES:$(LOCAL_PATH)/%=%)

#fPIC指地址无关代码

LOCAL_MODULE    := p7zip
LOCAL_CFLAGS := -fexceptions -fPIC
LOCAL_CPPFLAGS := -DANDROID_NDK -fexceptions \
	-DNDEBUG -D_REENTRANT -DENV_UNIX \
	-DEXTERNAL_CODECS \
	-DBREAK_HANDLER \
	-DUNICODE -D_UNICODE -DUNIX_USE_WIN_FILE -fPIC
#LOCAL_CFLAGS += -Wl,-Bsymbolic
LOCAL_LDFLAGS += -fPIE -pie -Wl,-Bsymbolic

ALL_FILES_H := $(shell find $(LOCAL_PATH) -type d)
LOCAL_C_INCLUDES := $(ALL_FILES_H)

#普通的so库
include $(BUILD_SHARED_LIBRARY)
#生成独立的可执行文件
#include $(BUILD_EXECUTABLE)

2.在编译过程中,曾出现错误如下:
1)error: cannot use ‘throw’ with exceptions disabled
解决办法是在LOCAL_CPPFLAGS 和LOCAL_CFLAGS 中都添加-fexceptions
2)error:requires unsupported dynamic reloc R_ARM_REL32; recompile with -fPIC;
解决办法是在LOCAL_CPPFLAGS 和LOCAL_CFLAGS 中都添加-fPIC,但是不能解决,最后通过设置LOCAL_LDFLAGS := -Wl,-Bsymbolic,实现成功编译,但是据说这个方案有隐患,目前未测试(参考https://android.googlesource.com/platform/frameworks/av/+/3b909164de79904137bb6661514d5ca6236a49c9/media/libstagefright/codecs/mp3dec/Android.mk)
3)error: case value evaluates to -2 , which cannot be narrowed
解决办法:更换ndk版本为16b,参考https://blog.csdn.net/shulianghan/article/details/116244700。

二、编写c程序以wrapper上述so

编写c语言程序来调用目标so中的方法,并将其编译为可执行程序。
harness.c代码如下(示例):

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

typedef int (*FunctionPointerOne)(int, const char**);
typedef char* (*FunctionPointerTwo)(void);

#define ARGV_LEN_MAX    512
#define ARGC_MAX        256
typedef enum
{
    true=1, false=0
}bool;
/**
 * get args from string
 */
bool str2args(const char *s, char argv[][ARGV_LEN_MAX], int* argc) {

    bool in_token, in_container, escaped;
    bool ret;
    char container_start, c;
    int len, i;
    int index = 0;
    int arg_count = 0;

    ret = true;
    container_start = 0;
    in_token = false;
    in_container = false;
    escaped = false;

    len = strlen(s);
    for (i = 0; i < len; i++) {
        c = s[i];
        switch (c) {
            case ' ':
            case '\t':
            case '\n':
                if (!in_token)
                    continue;
                if (in_container) {
                    argv[arg_count][index++] = c;
                    continue;
                }
                if (escaped) {
                    escaped = false;
                    argv[arg_count][index++] = c;
                    continue;
                }_Z5Main2iPPc
                /* if reached here, we're at end of token */
                in_token = false;
                argv[arg_count++][index] = '\0';
                index = 0;
                break;
                /* handle quotes */
            case '\'':
            case '\"':
                if (escaped) {
                    argv[arg_count][index++] = c;
                    escaped = false;
                    continue;
                }
                if (!in_token) {
                    in_token = true;
                    in_container = true;
                    container_start = c;
                    continue;
                }
                if (in_container) {
                    if (c == container_start) { //container end
                        in_container = false;
                        in_token = false;
                        argv[arg_count++][index] = '\0';
                        index = 0;
                        continue;
                    } else { //not the same as contain start char
                        argv[arg_count][index++] = c;
                        continue;
                    }
                }
                printf("Parse Error! Bad quotes\n");
                ret = false;
                break;
            case '\\':
                if (in_container && s[i + 1] != container_start) {
                    argv[arg_count][index++] = c;
                    continue;
                }
                if (escaped) {
                    argv[arg_count][index++] = c;
                    continue;
                }
                escaped = true;
                break;
            default: //normal char
                if (!in_token) {
                    in_token = true;
                }
                argv[arg_count][index++] = c;
                if (i == len - 1) { //reach the end
                    argv[arg_count++][index++] = '\0';
                }
                break;
        }
    }
    *argc = arg_count;

    if (in_container) {
        printf("Parse Error! Still in container\n");
        ret = false;
    }
    if (escaped) {
        printf("Parse Error! Unused escape (\\)\n");
        ret = false;
    }
    return ret;
}

//模拟apk中executeCommand方法实现
int main(int argc, char *argv[])
{
	void *handle;
	//1.解析参数
	char* cmd = argv[1];
  int argc1 = 0;
  char temp[ARGC_MAX][ARGV_LEN_MAX];
  char *argv1[ARGC_MAX];
  if (!str2args(cmd, temp, &argc1)) {
      return 7;
  }
  printf("argc:%d\n", argc);
  printf("argc1:%d\n", argc1);
  for (int i = 0; i < argc1; i++) {
    	argv1[i] = temp[i];
  		printf("arg[%d]:[%s]\n", i, argv1[i]);
  }
  //2.获取可执行函数地址并进行调用
  handle = dlopen("/data/local/tmp/libp7zip.so", RTLD_NOW|RTLD_LOCAL);
	if(!handle)
	{
		printf("errors:%s\n", dlerror());
		printf("error1!");
		return -1;
	}
	else
		printf("library is loaded!");

	int(*pfun)(int, const char**);
	printf("handle:%p\n", handle);
	pfun = dlsym(handle,"_Z5Main2iPPc");
	if(!pfun)
	{
		printf("errors:%s\n", dlerror());
		printf("error2!");
		return -1;
	}
	else
		printf("function is found!\n");
	printf("pfun:%p\n", pfun);
	int result = pfun(argc1, argv1);
	printf("exec success!\n");
	//printf("result is: %s\n", result);
	dlclose(handle);
	return 0;
}

上述代码是以_Z5Main2iPPc方法为例,该方法名是反汇编上一步得到的libp7zip.so文件得到的。在github上下载的apk源码,在executeCommand方法中调用的main方法定义在CPP/p7zip/UI/Console/MainAr.cpp文件中。整个harness.c文件就是模拟executeCommand方法实现的。接下来编写对应的Android.mk文件即可。
Android.mk文件如下:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := harness
LOCAL_SRC_FILES :=harness.c
LOCAL_ARM_MODE :=arm
LOCAL_C_INCLUDES:=include 
LOCAL_CFLAGS+= -pie -fPIE
LOCAL_LDFLAGS+= -pie -fPIE
include $(BUILD_EXECUTABLE)

接下来进入harness.c所在目录,通过命令编译:
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk APP_PLATFORM=android-19
最终即可得到可执行文件harness,然后将harness和libp7zip.so文件,一起push到手机的data/local/tmp目录下即可。
在android环境下执行命令:

/harness "7z x '/data/local/tmp/test.7z' '-o/storage/emulated/0/test.7z-ext' -aoa"

结果如下:

argc:2
argc1:5
arg[0]:[7z]
arg[1]:[x]
arg[2]:[/data/local/tmp/test.7z]
arg[3]:[-o/storage/emulated/0/test.7z-ext]
arg[4]:[-aoa]
library is loaded!handle:0xf460b44da7b790f9
function is found!
pfun:0x7793a1342c
Can't load './7z.dll' (dlopen failed gqdadd: library "./7z.so" not found)
exec success! 

通过上述结果可知,通过wrapper so的方式能实现方法调用,但是可能由于自编译的libp7zip.so存在问题,导致方法执行没有成功,至于原因还没有分析。

三、利用android-afl对目标so文件进行模糊测试

android-afl下载和编译参考https://github.com/ele7enxxh/android-afl。
因为这个过程中涉及到android源码的下载和编译,在此不进行赘述,网上资源丰富。
编译完成之后,共生成了5个可执行文件:afl-analyze, afl-fuzz, afl-gotcpu, afl-tmin和afl-showmap。
将afl-fuzz push到data/local/tmp目录下,然后进行对之前放入的harness进行模糊测试即可。
由于前面harness执行时调用方法没有成功,所以这一步无法在自己编译成功的libp7zip.so中实现。

那么此时,又回到能否利用现有apk中的libp7zip.so,避开executeCommand方法(也就是避开JNIEnv对象),重新选择一个新的方法进行调用,而这个方法就是上述harness.c中的Main2方法。但事实证明此时Main2方法只剩下地址,在so中没有相应符号。

接下来,回到p7zip源码,在其p7zip_16.02/CPP/ANDROID目录中,有多个目录,可以编译多个工具,如7z,7zr等等。一开始选择7z目录,编译出android环境下的arm64架构so,但是编译出来通过wrapper之后调用会出错,出错内容为

Can't load './7z.dll' (dlopen failed gqdadd: library "./7z.so" not found)

接下来,重新选择7zr目录,编译出的arm64架构下的lib7zr.so。通过查看7z和7zr目录下Android.mk文件,发现其实两者编译的源代码文件差不多。在lib7zr.so中同样包含有Main2函数。
构造wrapper代码如第二部分所示,只需修改dlopen中参数为/data/local/tmp/lib7zr.so即可,编译生成harness可执行文件,push到/data/local/tmp目录即可。
在samples_in目录中包含有压缩文件7zrtest.7z,解压之后输出目录即为当前目录
执行fuzz命令如下:

./afl-fuzz -d -n -i samples_in/ -o afl_out/ ./harness "7zr x @@ -aoa"

四、利用android-afl对可执行文件进行模糊测试

在上述测试过程中,编译出来的so,通过wrapper之后并不能执行。因此退而求其次,按照源码中原有的Android.mk文件(p7zip_16.02/CPP/ANDROID目录下),编译出可执行文件。尝试选择子目录7z,以编译出7z可执行文件,但是编译之后放入手机环境,执行失败,与编译so并wrapper之后的可执行程序错误相同。
接下来,选择7zr子目录编译可执行文件,其中ndk-build版本为16

cd p7zip_16.02/CPP/ANDROID/7zr/jni
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk APP_PLATFORM=android-21

将可执行文件7zr(在当前目录的libs目录下)push进/data/local/tmp,将测试文件p7zip_16.02/check/test目录下的后缀为7z的文件也导入到/data/local/tmp/sample_in目录,然后执行模糊测试命令。包括的命令

./afl-fuzz -m 2G -d -n -i samples_in/ -o afl_out/ ./7zr x @@ -o/data/local/tmp -aoa

其中sample_in目录下包含有两个后缀为7z的压缩包,上述命令需要再其后添加-aoa,意思是设置为覆写模式。


总结

通过android-afl实现了在android环境中,对so进行fuzz的测试。不足之处:
1.一个可执行程序可能只能fuzz一个方法
2.执行环境在android环境下,不知道是否会受硬件资源限制
3.android-afl已经多年没有更新,目前流行的是afl++(https://github.com/AFLplusplus/AFLplusplus),而这个也是支持android环境运行的,后续应该是编译出afl++在android环境下的可执行文件。
在android下的fuzz效果还没有运行出来,不好评价。下一步,应该是考虑往pc环境迁移。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐