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

**URL:** <https://forum.90sec.com/t/topic/842>\
**Category:** 账号审核\
**Created:** [2020 年3 月 5 日 10:32 UTC](https://forum.90sec.com/t/topic/842 "2020-03-05T10:32:36Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

作者： ![gangzhilian](https://forum.90sec.com/letter_avatar_proxy/v4/letter/g/6a8cbe/32.png) [@gangzhilian](https://forum.90sec.com/u/gangzhilian)\
发布日期： [2020 年3 月 5 日 10:32 UTC](https://forum.90sec.com/t/topic/842/1 "2020-03-05T10:32:36Z")

</div>

## 前言

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`，而`DexClassloader`是`BaseDexClassLoader`的子类，`BaseDexClassLoader`还有另外一个子类`PathClassloader`，`PathClassloader`是系统默认用来加载apk文件dex的类，而我们要动态加载一个dex的话，需要使用`DexClassloader`来加载。使用很简单，直接new一个`DexClassloader`对象就行了。其代码如下  
[http://androidxref.com/5.1.0\_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java：](http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java%EF%BC%9A)

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

```

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

### BaseClassloader

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

> **[Cross Reference:...](http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java)**

在`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(下面对照源码讲解其核心逻辑)

> **[Cross Reference: /libcore/dalvik/src/main/java/dalvik/system/DexPathList.java](http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java)**

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

### DexFile

DexFile的源码

> **[Cross Reference: /libcore/dalvik/src/main/java/dalvik/system/DexFile.java](http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexFile.java)**

下面对照源码看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.cc`的`OpenDexFilesFromOat`来实现的。

### class\_linker.cc

源码

> **[Cross Reference: /art/runtime/class\_linker.cc](http://androidxref.com/5.1.0_r1/xref/art/runtime/class_linker.cc#827)**

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

### dex\_file.cc

源码

> **[Cross Reference: /art/runtime/dex\_file.cc](http://androidxref.com/5.1.0_r1/xref/art/runtime/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 studio`的`Preferences`里面的`Android SDK`中勾选安装CMake、LLDB以及NDK，其中CMake是构建工具，LLDB是调试工具，NDK是编译工具链。所需下载安装的如下图所示如下图所示。

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/c0336eb3223dca85e7996e188116b955431153a9.png)

克隆

> **[asLody/VirtualApp](https://github.com/asLody/VirtualApp)**
>
> Virtual Engine for Android(Support 10.0 in business version) - asLody/VirtualApp

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

 ![image](https://forum.90sec.com/uploads/default/original/2X/e/eb5b602eab4fd3c819e0afb0ce84d4ba791a579a.png)

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

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/c7c36197e59e1d9c728484a86e114cecdb290793.png)

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

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

```

 ![image](https://forum.90sec.com/uploads/default/original/2X/a/a1d437e2d063cdd2479d93b8e74f714d073c8a02.png)

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

![image](https://forum.90sec.com/uploads/default/original/2X/a/a3e67c1dea2d2b7b8981a64d05c8c30815b158c9.png)

在`buildscript`下的`repositories`和`allprojects`下的`repositories`都加上`google()`和`mavenCentral()`，修改之后如下图：

 ![image](https://forum.90sec.com/uploads/default/original/2X/9/98f7a92266e7d60a245ff6bd8c25569da6b27ebe.png)

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

 ![image](https://forum.90sec.com/uploads/default/original/2X/0/0f715d017fc1a23525dded23c0a6f1c356edc0ff.jpeg)

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

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

在`VirtualApp`中`VirtualApp/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](https://forum.90sec.com/uploads/default/original/2X/f/f3703b999c60455a13d686fabe165af1130a07a4.png)

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

```
_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_

```

 ![image](https://forum.90sec.com/uploads/default/original/2X/7/7b58f3487634af07d3d1bd9b3ad328d0466f0bc4.png)

有了符号就可以开始写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就可以看到我们自己打的日志。

 ![image](https://forum.90sec.com/uploads/default/original/2X/2/2c4d521ed042fd420c7051aa8a667b1007badffb.png)

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

 ![image](https://forum.90sec.com/uploads/default/original/2X/2/2ee7750dc4ae3f6fd335422db31a6cf6bb7e2128.png)

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

![image](https://forum.90sec.com/uploads/default/original/2X/6/6548762f7699ba4d1e8d05664fb25ccb60b0783b.png)

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

 ![image](https://forum.90sec.com/uploads/default/original/2X/1/1331909434192b69ff41d33bfe5fa04aa70cfd54.png)

## 推荐阅读

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

> **[Android运行时ART加载OAT文件的过程分析\_老罗的Android之旅-CSDN博客](https://blog.csdn.net/Luoshengyang/article/details/39307813)**

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

> **[Android运行时ART加载类和方法的过程分析\_老罗的Android之旅-CSDN博客](https://blog.csdn.net/luoshengyang/article/details/39533503)**

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

> **[Android动态加载DEX文件流程分析\_朱小南的博客-CSDN博客](https://blog.csdn.net/zhu929033262/article/details/78281592)**

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

> **[dvm，art模式下的dex文件加载流程\_葱花炒蛋的博客-CSDN博客](https://blog.csdn.net/m0_37344790/article/details/78523147?utm_source=blogkpcl0)**

_Poll ([view on site](https://forum.90sec.com/t/topic/842/1))_

---

<div class="post-metadata">

作者： ![k4jar](https://forum.90sec.com/user_avatar/forum.90sec.com/k4jar/32/2091_2.png) [@k4jar](https://forum.90sec.com/u/k4jar)\
发布日期： [2020 年3 月 6 日 12:58 UTC](https://forum.90sec.com/t/topic/842/3 "2020-03-06T12:58:37Z")

</div>

有修复的办法吗
