Android脱壳之整体脱壳原理与实践

Android脱壳之整体脱壳原理与实践这里所说的壳都是指dex加壳,不涉及到so的加壳。 涉及到的代码分析基于AOSP 8.0版本。 一. Dex壳基础 壳分为几种类型,或者说分代: 整体加壳,整体保护 : 即将原始Apk里边的class

这里所说的壳都是指dex加壳,不涉及到so的加壳。
涉及到的代码分析基于AOSP 8.0版本。


一. Dex壳基础

壳分为几种类型,或者说分代:

  1. 整体加壳,整体保护 : 即将原始Apk里边的classes*.dex文件隐藏起来,使用jadx这种静态反编译工具只能看到壳dex,看不到App应用逻辑所在的dex,运行的时候壳dex负责将App的dex解密还原并加载到虚拟机中。加载的方式可以采用文件加载或者内存加载。
  2. 函数粒度的保护 : 也就是函数抽取型壳,保护粒度降到了方法级别,将App dex中某些方法抽空,使用jadx这种静态反编译工具查看方法体全是nop指令,由壳代码在合适的时机将方法体还原回去,显而易见方法体的还原必须在函数执行前进行。函数抽取型壳需要禁用掉dex2oat过程。
  3. 指令粒度的保护,保护效果最强 : 主要为VMP壳和dex2C,显著的特征是Java的方法native化了,VMP壳将保护级别缩小到了每条指令级别,通过自定义解释器来执行smail指令。而dex2C则将Java的方法等价转换为对应的native函数。

dex整体加壳是基础防护,基本上主流的加固厂商都会对dex整体加壳,因此必须先脱整体壳,才能进一步判断到底是不是函数抽取型壳/VMP壳/dex2C。

现有的一些开源加壳方案很多都是实验性质的,因为加固需要解决性能和稳定性的问题,需要投入许多人力才能实现。不过可以研究这些代码学习加壳的思路。
1.整体壳:
github.com/guanchao/ap…
2.dex2C:
github.com/amimo/dcc
3.VMP :
github.com/chago/ADVMP


二.判断壳的类型:

判断App是不是加壳很简单,使用jadx打开Apk以后,查看App的四大组件类,如果组件类找不到实现,那么就是整体加壳了。
比如这里只能看到qihoo相关的壳代码,App的代码被隐藏了:

2021-05-14_10-54.png

判断是不是函数抽取型壳就是看函数体是不是有意义的代码,比如下面就是一个抽取型壳,可以看到函数体都是nop指令:

2021-05-14_10-58_1.png

2021-05-14_10-58.png

判断是不是dex2C/VMP壳,就是要看是不是有Java方法被native化了,至于到底是dex2C还是VMP则需要更一步的判断,比如native函数注册地址是否相同:

2021-05-14_11-02.png


三. 加壳角度来看整体型壳

整体壳需要解决如下几个问题:
1.如何将原始的dex文件加密隐藏起来
2.壳代码如何在执行前获取控制权并可访问到被隐藏加密的dex从而可以解密dex文件
3.如何动态加载解密后的dex文件并且让虚拟机可以识别到dex文件中的四大组件

问题1:
可以将原始的dex文件加密添加到壳dex文件的末尾,并且更新壳dex文件的checksum,signature和file_size反映出这些变化,因为在dex文件的头部信息中会保留这些值来检查dex文件格式的正确性:

2021-05-14_11-16.png 修改了这三个值以后,壳dex文件的末尾就可以添加一些和dex格式定义中无关的信息,比如原始dex文件的个数,大小以及加密以后原始dex文件的内容。然后将壳dex替换掉原始的dex文件对Apk进行重打包即可。

问题2:
壳代码会将自己的Application类替换掉原来的Application类,因此壳的Application类的onCreate()attachBaseContext()会在App进程启动的时候率先执行拿到控制权。而访问被隐藏加密的dex可以通过getApplicationInfo().sourceDir来获取apk的路径,解压apk得到壳dex文件从而解密出壳dex文件里边附带的原始dex文件。

问题3:
通过Android中的类加载器动态加载解密后的dex文件,为了让四大组件可以正常走生命周期函数,还需要对类加载器进行修正,修正的方式大概有两种方式,下面的内容中会详细的描述。


四. Android类加载器:

类加载器在dex整体加壳与脱壳中起到了重要的作用。
网上有很多的文章介绍Java中的类加载器,因此我这里就不写了,这里主要说的是Android里边关于dex加载的”类”加载器。
类加载器可以在运行的时候加载在编译时未知的类,对于c/c++来说也有类似的功能dlopen与dlsym,而Android里边的BaseDexClassLoader可以实现在运行的时候加载在编译时未知的dex文件,经过此加载器的加载,ART虚拟机内存中会形成相应的数据结构,对应的dex文件也会由mmap映射到虚拟内存当中,通过此加载器的loadClass(String className, boolean resolve)方法就可以得到类的Class对象,从而可以使用该类。
对于壳来说,它解密出来的dex文件也是编译时未知的,因此需要通过类加载器来加载dex文件,系统提供的BaseDexClassLoader可以满足要求,也可以自定义ClassLoader来实现。

通过打印测试可以看到JDK API中的类如java.lang.String,以及Android系统提供的核心类如android.app.Activity(位于/system/framework/framework.jar中)都是由同一个类加载器java.lang.BootClassLoader所加载的。
而由应用程序开发者自己所编写的代码由dalvik.system.PathClassLoader加载器加载,打印出来的信息如下:

dalvik.system.PathClassLoader[DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /vendor/lib64, /system/lib64, /vendor/lib64]]]

查看源码可以看到PathClassLoader是继承自BaseDexClassLoader的,而PathClassLoader还有另外两个兄弟: InMemoryDexClassLoader以及DexClassLoader,而壳程序很多都使用了这两个类加载器来加载解密后的dex文件。其中InMemoryDexClassLoader是Android8.0以后新增的类,可以实现所谓的”不落地加载”。
总结一下,主要有如下几个ClassLoader:

  1. BootClassLoader: 加载系统核心类的加载器,由系统创建
  2. BaseDexClassLoader: 加载dex文件的基类加载器
  3. PathClassLoader: 加载应用程序开发者自己编写的代码的加载器,比如四大组件类,它继承自BaseDexClassLoader
  4. DexClassLoader: 从源码中可以看到几乎和BaseDexClassLoader没有区别,它继承自BaseDexClassLoader,一般用于实现插件化和加壳
  5. InMemoryDexClassLoader: 通过ByteBuffer数组来加载dex的加载器,它继承自BaseDexClassLoader
  6. 自定义ClassLoader: 可以实现想实现的任何功能

前面说过壳加载完原始的dex以后还需要对ClassLoader进行修正,否则加载组件类运行的时候会报ClassNotFoundException,为什么会报这种错误呢? 这就涉及到了组件类的创建过程,比如对于Activity来说,应用程序的Activity对象是在ActivityThread类的performLaunchActivity()方法中通过调用mInstrumentation.newActivity()创建出来的,这个函数的实现逻辑为:

2021-05-14_16-36.png 可以看到是通过ClassLoader先加载Activity类,再通过newInstance()来实现化类对象。
这个方法传递进来的ClassLoader是加载应用程序的ClassLoader,它所加载的dex为应用程序的主体的dex,对应的DexPathList是没有原始的dex路径的,因此会报ClassNotFoundException。这里也可以看出,一个BaseClassLoader对应着其实是一个Dex文件的列表,如果尝试让BaseClassLoader加载不在这个列表中的类,就会报ClassNotFoundException
为了解决上面的问题,可以有两种解决方案:

  1. 替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoader的parent为系统组件类加载器
  2. 打包原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入我们的DexClassLoader,即加载原始dex的DexClassLoader作为PathClassLoader的parent

第一种解决方案是将系统组件类替换,这样通过mInstrumentation.newActivity()试图加载类的时候,就能找到相应的类。
第二种解决方案将ClassLoader的继承关系修改为: BootClassLoader --> DexClassLoader --> PathClassLoader,由于双亲委派机制的存在,当PathClassLoader找不到动态加载的原始dex时,它会请求parent即DexClassLoader加载,因此可以加载成功。
还有一些壳是通过对PathClassLoader中的Elements数组进行合并来达到目的,这样的好处是当你试图用Frida枚举ClassLoader对象的时候,看不到额外的ClassLoader对象,也就无法轻易的得到Dex文件加载的路径。


五. 通用整体脱壳方法:

从上面的内容可以得知,加载Dex有很多种实现方式,每种壳的实现都不一定相同,如果想实现一个比较通用的整体脱壳方法,就必须寻找一个壳绕不开的方式来实现。这个关键的数据结构就是art虚拟机中定义的类art::DexFile,它的定义位于art/runtime/dex_file.h文件中。
这个类有两个成员变量,分别代表着dex文件加载到内存当中的起始地址以及大小,得到这两个信息以后通过内存dump的方式就可以轻轻松松的将dex文件dump下来。
这就表示一个事实: 在某个时机点内存当中一定会有解密后的完整dex存在。

2021-05-14_17-26.png

art::DexFile是加载dex绕不开的类,不论是使用BaseClassLoader,还是自定义类加载器,最终都需要art::DexFile,这一结论可以通过查阅class_linker.cc文件中的函数得到,像ClassLinker::DefineClassClassLinker::LoadMethodClassLinker::LoadClassMembers这些重量级函数都需要以DexFile对象做为参数:

2021-05-14_17-34.png

2021-05-14_17-34_1.png

2021-05-14_17-34.png

通过BaseDexClassLoader来加载类会执行art/runtime/native/dalvik_system_DexFile.ccDexFile_defineClassNative函数,这个函数的实现就是遍历Java层的DexFilemCookie对象所表示的native层的art::DexFile,因为这个art::DexFile对象代表了dex文件在内存中的结构,所以可以通过类名在art::DexFile列表中查找到DexFile::ClassDef结构,才能继续调用ClassLinker::DefineClass()函数,最终才能得到一个虚拟机实现中的Class对象表示mirror::Class*,因此这个流程仍然离不开虚拟机中的art::DexFile类。

2021-05-14_17-46.png

这还会引发出一个问题:
art虚拟机执行smail指令可以解释执行,这种模式叫Interpreter模式(很显然解释模式下需要dex文件存在于内存当中才能解释执行)
也可以执行oat以后elf文件中的本地机器指令,这种模式叫quick code模式。
在quick code模式中,dex编译出来的机器指令包含在dex2oat生成出来的oat文件中(对于app来说,oat文件以.odex结尾),那么对于quick code模式是否还需要dex文件呢?如果不需要dex文件,是不是就无法在内存中dump出dex文件了?

事实上quick code模式内存中也是会有原始的dex文件存在的,具体的执行逻辑在Runtime的GetOatFileManager().OpenDexFilesFromOat()函数中,在这个函数中如果走dex2oat流程的话就会执行oat_file_assistant.MakeUpToDate()调用dex2oat命令生成odex和vdex文件,并生成OatFile类的对象,这个类就代表着dex2oat以后oat文件在内存中的映射表示,oat文件其实就是可执行文件,只不过它有特殊的几个符号:oatdata,oatlastword,oatbss,oatbsslastword等,OatFile类中的成员变量oat_dex_files_storage_是个std::vector<const OatDexFile*>类型的变量,而OatDexFile类表示的是这个oat文件所对应的dex文件的信息(列表),通过OatDexFiledex_file_pointer_即可以找到对应的art::DexFile对象的地址。
在android8.0以后,oat文件存放着dex文件编译出来的可执行指令,而原始的dex内容其实存放在vdex文件中,这一点可以在后面的dump程序中看到。
在quick code模式下也需要原始dex的存在的原因是dex文件还存放着类相关的信息,如class_def_item,method_id_item,对于类方法的执行还离不开这些信息。

六. 整体脱壳实践:

既然知道虚拟机中的art::DexFile类是dex在内存中的表示,那么得到这个对象就可以dump出dex文件。
接下来就是寻找一个合适的点可以得到art::DexFile对象,得到对象以后通过hook的方式或者修改源码的方式都可以dump下来了。
下面是脱函数抽取型壳fart的作者寒冰大佬所提出的办法:
找到libart.so文件中所有导出函数中带有art::DexFile参数或者返回值的函数,那么这就是一个可以脱壳的点
我写了一个命令可以查找满足这种条件的函数:

arm64-readelf -s libart.so -W | tr -s ' ' | cut -f9 -d ' '| c++filt  | grep "art::DexFile"

比如在art/runtime/dex_file.ccDexFile::OpenCommon或者DexFile::DexFile中添加如下代码即可脱壳:

  pid_t pid = getpid();
  char dexfilepath[100] = {0};
  sprintf(dexfilepath,"/sdcard/drdump_%d_%d_DexFile.dex",(int)size,(int) pid);
  int fd = open(dexfilepath, O_CREAT | O_RDWR , 666);
  if (fd > 0){
    int number = write(fd,base,size);
    if(number > 0){
    }
    close(fd);
  }

这种是修改源码的方式来脱壳。


还有一种方式是通过frida hook来脱壳,它的优点是简单有效,不需要重新编译rom。
它的原理如下:
对于通过使用BaseDexClassLoader来加载的程序来说,DexFile.java类的mCookie变量在native层的表现其实是一个jlong类型的指针的数组,数组的个数为此ClassLoader加载的dex文件个数 + 1,第一个元素类型为OatFile*,剩余的元素为对应的art::DexFile*,因此可以通过获取mCookie变量来得到art::DexFile*列表,并且通过art::DexFile的begin_和size_来dump。
代码如下:

function hasOwnProperty(obj, name) {
    try {
        return obj.hasOwnProperty(name) || name in obj;
    } catch (e) {
        return obj.hasOwnProperty(name)
    }
}

function getHandle(object) {
    var result = null;
    if (hasOwnProperty(object, "$handle")) {
        result = object.$handle;
        if (result) {
            return result;
        }
    }
    if (hasOwnProperty(object, "$h")) {
        return object.$h;
    }

    return null;
}

function dump_dex(packagename, dexfilebegin, dexfilesize) {
    var dexfile_path = "/sdcard/my_frida_dump_" + packagename + "_" + dexfilesize + ".dex";
    var dexfile_handle = new File(dexfile_path, "w");
    if (dexfile_handle && dexfile_handle != null) {
        var dex_buffer = ptr(dexfilebegin).readByteArray(dexfilesize);
        dexfile_handle.write(dex_buffer);
        dexfile_handle.flush();
        dexfile_handle.close();
    }
}


function dealwithClassLoader(classloaderobj, packagename) {
    if (Java.available) {
        Java.perform(function () {
            try {
                var dexfileclass = Java.use("dalvik.system.DexFile");
                var BaseDexClassLoaderclass = Java.use("dalvik.system.BaseDexClassLoader");
                var DexPathListclass = Java.use("dalvik.system.DexPathList");
                var Elementclass = Java.use("dalvik.system.DexPathList$Element");
                var basedexclassloaderobj = Java.cast(classloaderobj, BaseDexClassLoaderclass);
                var tmpobj = basedexclassloaderobj.pathList.value;
                var pathlistobj = Java.cast(tmpobj, DexPathListclass);
                console.log("pathlistobj->" + pathlistobj);
                var dexElementsobj = pathlistobj.dexElements.value;
                console.log("dexElementsobj->" + dexElementsobj);
                for (var i in dexElementsobj) {
                    var obj = dexElementsobj[i];
                    var elementobj = Java.cast(obj, Elementclass);
                    console.log("elementobj->" + elementobj);
                    tmpobj = elementobj.dexFile.value;
                    var dexfileobj = Java.cast(tmpobj, dexfileclass);
                    var mCookie = dexfileobj.mInternalCookie.value;
                    var mInternalCookie = dexfileobj.mInternalCookie.value;
                    if (mCookie != null) {
                        var jnienv = Java.vm.tryGetEnv();
                        var cookiePtr = getHandle(mCookie);
                        var arrayLength = jnienv.getArrayLength(cookiePtr);
                        var long_data = jnienv.getLongArrayElements(cookiePtr, 0);
                        console.log("arrayLength:" + arrayLength + ",long_data:" + long_data);
                        for (var i = 1; i < arrayLength; i++) {
                            var dexfileptr = Memory.readPointer(ptr(long_data).add(8 * i));
                            var dexfilebegin = Memory.readPointer(ptr(dexfileptr).add(Process.pointerSize * 1));
                            var dexfilesize = Memory.readU32(ptr(dexfileptr).add(Process.pointerSize * 2));
                            console.log("pointer:" + dexfileptr + ",dexfilebegin:" + dexfilebegin + ",dexfilesize:" + dexfilesize);
                            dump_dex(packagename, dexfilebegin, dexfilesize);
                        }
                    }
                }
            } catch (e) {
                console.log(e);
            }

        });
    }


}

function tuoke(packagename) {
    if (Java.available) {
        Java.perform(function () {
            console.log("go into enumerateClassLoaders!");
            Java.enumerateClassLoadersSync().forEach(function (loader) {
                if (loader.toString().indexOf("BootClassLoader") >= 0) {
                    console.log("this is a BootClassLoader!")
                } else {
                    try {
                        console.log("classloader : " + loader);
                        dealwithClassLoader(loader, packagename);
                    } catch (e) {
                        console.log(e);
                    }
                }
            })
        });
    }
}

这种方式的优点是简单有效,不需要费时间编译rom,不过对于自定义ClassLoader的壳可能无法处理。


还有一种是通过frida进行内存搜索dex特征进行dump的方式,是葫芦娃大佬写的: github.com/hluwa/FRIDA…

今天的文章Android脱壳之整体脱壳原理与实践分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20746.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注