来源
本文要实验的方案同样来源于CSDN大牛Jack_Jia的一篇翻译博文:
Android4.0内存Dex数据动态加载技术
原文的地址是 http://2013.hackitoergosum.org/presentations/Day1-05.Nifty%20stuff%20that%20you%20can%20still%20do%20with%20Android%20by%20Xavier%20Martin.pdf
着重参考了看雪论坛的两篇帖子:
【求助】Dex内存加载的Native实现过程中出现的问题
【原创】DEX文件内存加载实现中的数据构造(C部分)
方案
从上一篇,基础加壳的思路最后得出的结果是方案还不够完善。因为使用的系统DexClassLoader提供的接口必须要求源程序保存在文件系统中,对手一旦过了莱茵河马其诺防线就没啥意义了。所以在前一篇的基础上,又有上面来源方案中的思路,即通过jni调用底层接口,在内存中加载dex文件。步骤如下:
- 获取Dalvik_dalvik_system_DexFile_openDexFile_bytearray方法指针;
- 调用Dalvik_dalvik_system_DexFile_openDexFile_bytearray方法解析Dex数据;
- 实现JAVA层Dex ClassLoader完成类的加载;
方案本身是译文,而且没有介绍细节上的实现。不能像上一篇那样直接copy代码,那就只能老老实实的先搞清楚原理。通过短短的几百字译文,可以总结出一下几点:
该方案只是针对实现内存加载dex文件,对于加壳来说这只是其中的一部分、最重要的一部分;
方案的技术点在于通过dlopen、dlsym方法,拿到系统动态库libdvm.so中的内存加载dex文件的方法,该方法位于源码 :dalvik/vm/native/ dalvik_system_DexFile.cpp 类中,名称是:Dalvik_dalvik_system_DexFile_openDexFile_bytearray;并且只在4.0以上版本开放;
底层加载dex文件后,会得到一个int型的cookie值,java层的自定义DexClassLoader需要根据该值能够拿到已加载好的dex内容才能把整个流程拼接起来;
实现
虽然从方案分析上看,这个加载实现是有系统版本局限性的,不过通过dlsym方法拿到系统动态库函数指针然后来使用的思路对一个中间层认识有限的土锤来说还从来没尝试过,并且,通用的方法应该也离不开这种模式,所以完全有理由去实现它,作为一个中间过程。
所有尝试都是基于上一篇的基础班加壳的实现上,不要忘记我们的最终目的是实现APK加壳,内存加载dex文件只是其中的一部分。
壳工程的迭代
本地代码
Jni关键代码基本都在译文博客中了,我们要做的是让它通过编译、得到so库。本地代码当然要有与之对应的java代码去加载才能用,通过上面对因为的总结,可以先这样定义本地方法:
static native int loadDex(byte[] dex,long dexlen);
生成好对应的.h、.c文件之后把译文中给出的核心代码填上,下面才是难题,许多类型都是unknown的,ndk编译器会告诉你它不认识这些乱七八糟的玩意儿。接下来就是挨个补充定义了。
看着u4、u1这些从java程序猿眼中怪怪的类型我不禁长出一口气——幸亏当年是C出身的。溯本清源,在源码 /dalvik/vm/Common.h 类中找到了这群货的宏定义,于是照葫芦画瓢,在jni目录里弄了一个伪造版的Common.h,搜刮了一下所有需要定义的类型之后,这个文件基本上是这个样子的:
|
|
这里面还有个大小端的问题,不过为求实验先通过就先定义死,过了再说。
还有个值得一提的结构就是最后面的ArrayObject,这玩意定义在源码的/dalvik/vm/oo/Object.h 中,原本的定义是这样的:
|
|
如果还实实在在的去弄一个ClassObject,那就是java中毒已深的表现,根据看雪里面的相关讨论(就是文首提到的两篇),直接如上定义了。得到最后的C代码如下:
|
|
ArrayObject之后的数据拷贝是从看雪上抄来的,刚开始不求甚解,后来看了源码中的调用方法就慢慢明白了:
|
|
Java层
底层代码基本了然,也就是说译文提供的思路基本实现,剩下其他加壳的事儿还要自己动脑筋补上。现在java层我们有一个可以使用的以byte数组为参数的加载dex的接口了:
static native int loadDex(byte[] dex,long dexlen);
要知道我们花这么大力气实现的这个方法,实际意义在于让源程序的dex数据在内存中传递,而不是必须保存在某个地方、以文件的方式。也就是说,我们需要一个新的DexClassLoader,去替换在上一篇提到的基础加壳方案中自定义Application—— ProxyApplication 类,通过反射设置到”android.app.LoadedApk”中mClassLoder属性的那个系统DexClassLoader,即至少那一段应该改成这样:
|
|
没错,DynamicDexClassLoder 它的构造参数中应当去接收源程序的dex数据,以byte数组的形式,这样、相关把dex数组保存为文件那段代码可以删除,/data/data 中相关目录就找不到缓存dex文件的身影了;
替换DexClassLoader,要知道相对于系统版本的加载器我们的少了什么,又多出了什么,在一一对接上,就没问题了。少了什么呢?是dex文件路径、多出了什么呢?是dex byte数组,考虑到已经实现的jni库,那就是多了一个加载好的dex文件对应的cookie值。那么,这个
Cookie 是否能够完成替换呢?这需要到源码中找答案。
源码路径:libcore/dalvik/src/main/java/dalvik/system ,生成类图,取出DexClassLoader相关的一部分:
走读几遍代码基本就能了解,对于dex文件加载而言,DynamicDexClassLoder需要做的实际上只有一件事,复写findClass方法,使APK运行时能够找到和加载源程序dex中的类,至于如何实现,从类图上就可以看出,最后实际上追溯到DexFile类,可以利用到jni加载到的cookie,通过反射DexFile中的方法,实现我们的预期,具体实现如下:
|
|
}
加密工具的跟进
加密工具需要变化的是,加入壳程序dex的加密数据不再是整个源程序的APK,而是源程序中的dex文件。这一点修改加密代码中的目标文件、并修改操作脚本即可,无需多说。
小结
结合译文方案,实现了内存加载dex文件,并通过自定义DexClassLoader的方式,巩固了之前的加壳方案,使源程序不在以文件的形式出现。壳的意义也在于此,至于防止内存中获取dex这种高级的破解方法,壳似乎略显无力,所以先放到后面考虑。目前的问题是,内存加载dex所依赖的底层方法,只在4.0以上几个版本存在,5.0没有查询还是未知数,还没能满足通用性的要求,要需要进一步寻找方案。