脱壳原理以及如何实现脱壳机【通过】

前言

Android兴起以来,应用数量越来越多;当厂商或者个人开发者发现应用本身也可能存在被破解的可能时,他们就需要找一种解决方案来防止应用被破解,应用加固这个产品就是用来解决这个问题。Android的加固大致分为:dex整体加固、指令抽取和虚拟指令(VMP)。保护强度依次递增。我们这里只分析整体加固。

厂商对app做了加固之后想要对app做渗透测试,或者探索app某些功能的实现就会难很多,所以我们有这些需求的时候就需要去脱壳。一般比较多的教程是教你如何用ida动态调试,然后在关键函数下断点脱壳,这种方式确实可以增强动手能力。我们这里教你如何自己写一个脱壳机,免去绕过反调试的烦恼。

环境及工具

    1. Android Studio
    1. Android sdk
    1. ndk
    1. cmake
    1. Android设备(5.0 - 7.0)
    1. IDA
    1. jadx-gui

寻找脱壳点

对于整体加固,他要执行代码,在代码被执行之前,他肯定会做解密操作,把真实的dex或者指令恢复出来。所以只要找到传入参数有dex并且已经在解密之后都可以作为脱壳点。

以Android5.1为例分析dex加载流程

Dex加载流程(基于Android5.1源码)

DexClassloader

我们要动态加载一个dex文件,需要用到的是DexClassloader,而DexClassloaderBaseDexClassLoader的子类,BaseDexClassLoader还有另外一个子类PathClassloaderPathClassloader是系统默认用来加载apk文件dex的类,而我们要动态加载一个dex的话,需要使用DexClassloader来加载。使用很简单,直接new一个DexClassloader对象就行了。其代码如下
http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java:

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath,ClassLoader parent)
{
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

DexClassloader的代码就只有下面短短的几行,直接调用的父类的构造方法。

BaseClassloader

BaseClassloader就是DexClassloader的父类,在这里面实现了dex的加载逻辑。BaseClassloaderClassloader的子类,Classloader有两个重要的方法:findClassloadClass,其中findClass是用来实现类加载的逻辑,而loadClass是先从父Classloader里面去寻找,如果找不到就调用自己的findClass来找。也就是说当前加载dex的class是由loadClass来实现的。
BaseClassloader的源码

BaseClassloader里面我们需要关注的有两个方法,其一是构造方法,另外一个是findClass方法。请自己参看源码看下面的解释。

在构造方法中,将自己的pathList这个成员变量赋值,其值是一个新创建的DexPathList对象。在findClass方法中是调用的pathList里面的findClass方法,所以dex加载的逻辑应该是在DexPathList里面实现的。

DexPathList

DexPathList的源码http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java(下面对照源码讲解其核心逻辑)

DexPathList里面主要需要关注三个函数:构造方法、findClassmakeDexElements。我们先看findClass,这个方法是用来找类的,其代码里面是从自己的dexElements去找的类。然后我们看正好是在其构造方法中调用makeDexElements这个方法类给dexElements来赋值的。在makeDexElements这个类里面,就是将apk或者zip或者jar或者裸的dex加载起来,放到Elements对象里面。dexFile就是放到这个element里面,在makeDexElements里面是调用的自身的loadDexFile来加载dex,在loadDexFile里面判断了文件是否是zip或者apk jar,如果是就调用构造方法来加载dex,否则就使用loadDex方法来加载dex。

DexFile

DexFile的源码

下面对照源码看dexfile的主要逻辑。

DexFile这个类就是java层最终加载dex的类,在其构造方法中调用的openDexFile方法来加载dex,而openDexFile是调用的openDexFileNative方法来加载dex,这个方法是一个native方法。其实现在dalvik_system_DexFile.cc中。

dalvik_system_DexFile.cc

DexFile.java中调用的openDexFileNative的实现就在dalvik_system_DexFile.cc中的DexFile_openDexFileNative函数里面,源码http://androidxref.com/5.1.0_r1/xref/art/runtime/native/dalvik_system_DexFile.cc)。

在这里是由class_inker.ccOpenDexFilesFromOat来实现的。

class_linker.cc

源码

这里的代码很长,就不一一解释,大概就是查看当前dex文件是否被解析成oat,如果被解析成了oat文件,就直接下一步,如果没有被解析成oat就先调用dex2oat把dex解析成oat文件。

dex_file.cc

源码

dex_file.cc这个文件里面有很多关于dex的操作,比如创建新的dex对象,从内存中加载dex。这个文件中也就是我们可以找到很多脱壳点的地方了。我们前面说过只要传递的参数有dex就有可能是脱壳点,我们看有dex参数的函数有:

std::unique_ptr<const DexFile> DexFile::Open(const uint8_t* base, size_t size,
                                             const std::string& location,
                                             uint32_t location_checksum,
                                             const OatDexFile* oat_dex_file,
                                             bool verify,
                                             std::string* error_msg)
std::unique_ptr<const DexFile> DexFile::OpenMemory(const uint8_t* base,
                                                   size_t size,
                                                   const std::string& location,
                                                   uint32_t location_checksum,
                                                   MemMap* mem_map,
                                                   const OatDexFile* oat_dex_file,
                                                   std::string* error_msg)

还有DexFile的构造函数

DexFile::DexFile(const uint8_t* base, size_t size,
                 const std::string& location,
                 uint32_t location_checksum,
                 MemMap* mem_map,
                 const OatDexFile* oat_dex_file)

其中DexFile::Open这个函数直接调用的DexFile::OpenMemory,所以我们比较好的脱壳点就是DexFile::OpenMemory这个函数。当然也有修改dex2oat做脱壳的,因为会传递解密好的dex给dex2oat这个时候也能拿到dex,但是这个会改系统文件,对于没有root的手机,这个肯定是做不到的,而我们用hook这种方式借助VirtualApp可以实现在无root的手机上脱壳,适用范围更广。

编写代码实现脱壳

环境准备

我们编写的脱壳机基于VirtualApp,含有native代码,所以需要下载ndk来编译native的代码,在Android studioPreferences里面的Android SDK中勾选安装CMake、LLDB以及NDK,其中CMake是构建工具,LLDB是调试工具,NDK是编译工具链。所需下载安装的如下图所示如下图所示。

克隆

到本地,使用Android studio打开工程。在Android studio中选择打开已经存在的Android工程

然后选择克隆的目录工程的build.gradle导入

在gradle构建的过程中会报如下的错(我这里用的比较高版本的Android Studio,如果用低版本的可以直接加载工程,不会报错,不用下面修改build.gradle这些步骤):

Could not find manifest-merger.jar (com.android.tools.build:manifest-merger:26.0.0)

我们需要改一下整个工程的build.gradle文件

image

buildscript下的repositoriesallprojects下的repositories都加上google()mavenCentral(),修改之后如下图:

做了这些操作之后我们就能够成功编译VirtualApp了,连接上Android手机点击运行就可以运行在手机上了,运行结果如下图:

整体加固脱壳代码(基于Android6.0)

我们前面分析到DexFile::OpenMemory这个函数的参数里面有传递进来dex以及dex的长度,我们可以做一下hook来把传递的参数中的dex写道文件中去,如果这个时候dex已经被解密了,那么我们获取到的dex就是脱壳过的dex了。

VirtualAppVirtualApp/lib/src/main/jni/Foundation/IOUniformer.cpp这个源码文件中做了很多的hook操作。我们也可以模仿着他来将DexFile::OpenMemory函数hook上,代码如下:

IOUniformer.cpp里面有个onSoLoaded函数,当so被加载了之后,这个函数就会被调用,当libart.so被加载之后,我们就可以去hook上DexFile::OpenMemory,然后将传递参数中的dex写到文件中去。

要去hook这个函数,第一步需要找到这个函数的导出符号,把/system/lib/libart.so导出出来,用ida打开,等ida分析完成之后再左侧函数栏搜索OpenMemory,我们发现有两个结果。根据前面分析,确实是有两个。我们需要hook的是直接传递的dex那个,也就是第一个参数为char *的,所以我们应该hook搜索到的第一个函数。

image

我们双击函数跳转到他的反汇编代码,函数开头的那一长串就是他的符号,符号为

_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_

有了符号就可以开始写hook代码了,写hook代码也很简单,先定义一个函数指针,这个函数指针用来存放原来的函数(注意: 在Android5.0和5.1参数就和源码中的一样,在Android6.0和7.0上第一个参数为this指针):

void * (*old_DexFile_OpenMemory)(const uint8_t* base,
                    size_t size,
                    const std::string& location,
                    uint32_t location_checksum,
                    void * mem_map,
                    const void * oat_dex_file,
                    std::string* error_msg);
void * (*old_DexFile_OpenMemory)(const uint8_t* base,
                    size_t size,
                    const std::string& location,
                    uint32_t location_checksum,
                    void * mem_map,
                    const void * oat_dex_file,
                    std::string* error_msg);

然后创建一个新的函数来替代原始的DexFile::OpenMemory

void * new_DexFile_OpenMemory(const uint8_t* base,
                                 size_t size,
                                 const std::string& location,
                                 uint32_t location_checksum,
                                 void * mem_map,
                                 const void * oat_dex_file,
                                 std::string* error_msg){
    // 把参数传递的dex写入到文件中
    // 拼接文件名
    char file_path[128];
    // 存文件名的char数字全部置为0
    memset(file_path, 0, sizeof(file_path));
    // 拼接文件名
    sprintf(file_path, "/sdcard/%x.dex", size);
    // 写入到文件中
    writeToFile(file_path, base, size);
    // 调用原来的方法
    return (*old_DexFile_OpenMemory)(thiz, base, size, location, location_checksum, mem_map, oat_dex_file, error_msg);
}

在这个函数里面我们先把内存中的dex写入到文件中,然后调用了原来的DexFile::OpenMemory函数,这样就能保证原来的逻辑不变。在写文件的时候我们直接写入到了/sdcard/但是这么多文件我们找起来挺麻烦的,所以我们获取一下当前VirtualApp容器内运行应用的/proc/self/cmdline获取下进程名,然后写道/sdcard/{进程名}这个文件夹下面,我们需要读取当前的cmdline,然后拼接,最终的代码如下:


// 写入到文件
void writeToFile(const char *path, const uint8_t *base, size_t size) {
    int dex = open(path, O_WRONLY | O_CREAT);
    if(dex < 0) {
        __android_log_print(ANDROID_LOG_ERROR, UNTAG, "打开文件是吧 %s, %s", path, strerror(errno));
        return;
    }
    int wlen = write(dex, base, size);
    if(wlen != size) {
        __android_log_print(ANDROID_LOG_ERROR, UNTAG, "写入dex失败%s", path);
    }
    close(dex);
    __android_log_print(ANDROID_LOG_INFO, UNTAG, "写入dex成功 %s", path);

}

size_t getProcessName(char *name) {
    char buff[128];
    memset(buff, 0, sizeof(buff));
    int fp = open("/proc/self/cmdline", O_RDONLY);
    if(fp < 0){
        __android_log_print(ANDROID_LOG_ERROR, UNTAG, "读取文件失败%s, %s",name, strerror(errno));
        sprintf(name, "_default");
        return 0;
    }
    size_t len = read(fp, buff, sizeof(buff));
    if(len > 0) {
        strncpy(name, buff, len);
    }
    return len;
}

void * (*old_DexFile_OpenMemory)(const uint8_t* base,
                    size_t size,
                    const std::string& location,
                    uint32_t location_checksum,
                    void * mem_map,
                    const void * oat_dex_file,
                    std::string* error_msg);

void * new_DexFile_OpenMemory(const uint8_t* base,
                                 size_t size,
                                 const std::string& location,
                                 uint32_t location_checksum,
                                 void * mem_map,
                                 const void * oat_dex_file,
                                 std::string* error_msg){
    // 把参数传递的dex写入到文件中
    // 拼接文件名
    char file_path[256];
    // 存文件名的char数字全部置为0
    memset(file_path, 0, sizeof(file_path));
    // 拼接文件名
    char process_name[128];
    memset(process_name, 0, sizeof(process_name));
    getProcessName(process_name);
    sprintf(file_path, "/sdcard/%s", process_name);
    int ret = mkdir(file_path, 0777);
    if(ret == 0) {
        sprintf(file_path, "%s/%x.dex", file_path, size);
        // 写入到文件中
        writeToFile(file_path, base, size);
    } else {
        __android_log_print(ANDROID_LOG_ERROR, UNTAG, "创建文件夹失败 %s, %s", file_path, strerror(errno));
    }
    // 调用原来的方法
    return (*old_DexFile_OpenMemory)(base, size, location, location_checksum, mem_map, oat_dex_file, error_msg);
}

现在用来替换DexFile::OpenMemory的代码已经写好了,下面我们就需要用Substrate来让我们的hook代码生效,在IOUniformer里面有个函数叫onSoLoaded,当so被加载的时候这个函数就会被调用,在加载了libart.so之后我们就把DexFile::OpenMemory去hook上,等带壳的程序执行到这里就会进入到我们的hook函数里面,然后dex就会被写入到/sdcard/中。代码如下:


void onSoLoaded(const char *name, void *handle) {
    __android_log_print(ANDROID_LOG_INFO, UNTAG, "加载so: %s", name);
    if(strstr(name, "libart.so") != NULL) {
        void *symbol = NULL;
        if(findSymbol("_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_",
                   "libart.so", (unsigned long *) &symbol) == 0 ) {
            MSHookFunction(symbol, (void *) new_DexFile_OpenMemory, (void **)&old_DexFile_OpenMemory);
        }
    }
}

现在我们已经完成代码了,点击运行运行在手机上。然后在logcat里面把最右边选为No Filters,然后logcat里面搜索UNPACK_TEST就可以看到我们自己打的日志。

然后运行应用到手就上,将加固的应用放到/sdcard/点击Add app,在external storage里面选择加固了的应用,勾选之后点击install,等安装完成之后启动应用即可看到logcat中已经有输出写入dex/sdcard了。加固后的dex如下:

脱壳之后在sdcard下写入了几个dex

image

脱壳之后的dex用jadx打开如下:

推荐阅读

Android运行时ART加载OAT文件的过程分析。

Android运行时ART加载类和方法的过程分析。

Android动态加载DEX文件流程分析。

dvm,art模式下的dex文件加载流程

  • 通过
  • 未通过

0 投票者

有修复的办法吗