来源
Andorid APK反逆向解决方案:梆梆加固原理探寻
CSDN 作者Jack_Jia
该篇博文中的:“3. 如何使DexClassLoader加载加密的dex文件? ”这部分。
方案
上一篇实现的内存加载dex方案,具有Android系统版本的局限性。为了克服这个问题,在不断的百度、google下,找到了本文的来源博文,该文章是分析梆梆加固的,作为一个以APK安全为业的公司级产品,实现的加密当然是全面的。在对梆梆加固实现的猜想部分,在“如何使DexClassLoader加载加密的dex文件?”这个技术点下,作者提出了这样一个方案猜想,步骤如下:
读取dexFileName文件内容并解密到byte数组。
调用dexFileParse函数解析byte数组为DexFile:\dalvik\libdex\DexFile.cDexFile* dexFileParse(const u1* data, size_t length, int flags)//dlsym(handle, "dexFileParse");
调用allocateAuxStructures转换DexFile为DvmDex(由于该方法为static方法,因此需要按照其逻辑自行实现)。\dalvik\vm\DvmDex.c
static DvmDex* allocateAuxStructures(DexFile* pDexFile)
添加DvmDex到gDvm.userDexFiles \dalvik\vm\Init.c
struct DvmGlobals gDvm; //gDvm = dlsym(handle, "gDvm");
修改MyDexClassLoader中的mDexs对象的mCookie值。mCookie主要用于映射底层DvmDex数据——DexClassLoader.mDexs[0].mCookie值。
分析与实验
对于一个对底层代码了解有限的菜鸟而言,对于这个猜想方案只能表示一头雾水。好在有了之前方案实现的基础,仔细分析起来还是有迹可循的。在开始实验之前,针对这个方案提出一个想不明白的问题,然后通过走读、对比源码来找到答案,以期能够快速的吃透这个思路的原理。
- 需要证明 //dlsym(handle, “dexFileParse”); 这种加载方式,是否能拿到函数指针,这个调用与方案二还是有形式上的区别的;
- 第3步为什么C++中的静态方法要自行实现逻辑,如果需要在jni中自行实现,难度有多大;
- 把解析出的DvmDex设置进init.cpp的一个全局变量中,是不是可以理解为注册到底层系统中,原理是什么?这需要对系统加载apk的整个流程有所了解;
- 返回到上层一个cookie值,这一点同方案二是一致的,java层需要根据这个cookie取到底层对应的类。问题是在前两步的操作中哪里能得到这个cookie呢?
- 是不是可以理解Dalvik_dalvik_system_DexFile_openDexFile_bytearray 该函实际上完成了方案一中第2到第4三步的操作呢,该方法所在的动态库和本方案中的动态库在系统中是什么样的关系?
- 不同版本源码是否有涉及到的函数接口?
这些是在动手实践之前想不通的问题,那么带着问题,就开始在源码、搜索引擎、前两个方案的实例中寻找答案。在不断摸索中,慢慢的还是可以形成一些看法的。对整个方案的认识也会更进一步。挨个回答吧。
第一题,答案是同样的方法根本拿不到dexFileParse这个指针。
根据同目录下的 Android.mk 文件配置可知:
LOCAL_MODULE := libdex
include $(BUILD_HOST_STATIC_LIBRARY)
实际上这货是个静态库,生成的是lib.a,所以不能使用同样的方法搞出函数指针来用;
第二题,只能捂着脸来答了,因为这属于C基础知识范围的内容,C语言中的静态函数与java中的静态函数意义完全不一样,C中static主要限定该函数只能在该文件中使用。我觉得我该考虑修改一下简历里对C语言的熟悉程度了,好久没碰果然忘得够快。其实上个方案调用的方法也是静态的:
static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args,
JValue* pResult);
不过仔细看函数指针的获取方法就知道,源码中是将这个静态函数相关信息保存在一个常量数组里面的,而上个方案也是先获取常量数组的标号之后再获取该函数的指针。
const DalvikNativeMethod dvm_dalvik_system_DexFile[] = {
{ "openDexFile", "(Ljava/lang/String;Ljava/lang/String;I)I",
Dalvik_dalvik_system_DexFile_openDexFile },
{ "openDexFile", "([B)I",
Dalvik_dalvik_system_DexFile_openDexFile_bytearray },
…
}
此外,对于静态函数allocateAuxStructures,除了需要模拟相关联到的结构体之外,抠出来通过编译并不难;
第三题,去指定的Init.cpp文件看看那个全局变量,基本上能猜测一二。每一个APK的启动与运行,在底层都会对应一个Android DVM,同时也就是有一个全局变量gDvm,通过dlsym获取全局变量的地址是没问题的。问题是不同版本的DVM,拥有不同版本的DvmGlobals结构,只有指针的情况下无法拿到正确的gDvm值。不过这个问题可以先放一放,大不了使用最笨的方法——根据版本选用不同的结构体去解析也可实现;
第四题,cookie。实际上到这一步的时候基本上可以脱离原猜想方案来看问题了。想知道如何获取cookie,最简单的办法就是通过已有的代码找找这货究竟是个什么东西。那么,很明显上个方案中Dalvik_dalvik_system_DexFile_openDexFile_bytearray——它就返回过一个cookie。
static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args,
JValue* pResult)
{
…
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = true;
pDexOrJar->pRawDexFile = pRawDexFile;
pDexOrJar->pDexMemory = pBytes;
pDexOrJar->fileName = strdup("<memory>"); // Needs to be free()able.
…
RETURN_PTR(pDexOrJar);
}
或许到这里还不甚清晰,因为返回类型是void,那么返回的任务肯定是落在输入参数pResult上了,看看这个RETURN_PTR宏定义基本也就清楚了:
#define RETURN_PTR(_val)do { pResult->l = (Object*)(_val); return; } while(0)
好,所谓cookie其实是个DexOrJar类型结构体的指针。JAVA层需要这个cookie值,到最后也就是C/C++中需要这个结构体,有了这个结构体中的信息就能拿到加载dex的内容。反观猜想方案中的步骤:
通过dexFileParse拿到一个DexFile指针(虽然动态加载无法实现,我们先假设能够拿到)->以DexFile指针为参数,通过allocateAuxStructures方法转换一个DvmDex 指针->找到DVM中的gDvm,将DvmDex设置进该全局变量的userDexFiles属性中(这一步没有指明具体的方法,但是我们也假设能找到合适的方法)->返回cookie到JAVA层使用。
可以看出cookie在整个方案中出现的很突兀,参照前面几步用到的函数也找不到任何有cookie的地方。上文所引函数已经很清晰的说明了cookie实际上是个DexOrJar指针(这一点需要通过不同版本的Android源码来对比验证,确实如此),指针只是地址,真正起作用的是这个结构:
struct DexOrJar {
char* fileName;
bool isDex;
bool okayToFree;
RawDexFile* pRawDexFile;
JarFile* pJarFile;
u1* pDexMemory; // malloc()ed memory, if any
};
4.0以前的版本没有最后一个变量,但这不影响我们对整个流程的理解。很关键的一点,该结构需要一个RawDexFile指针,而显然我们到目前为止只有一个DvmDex指针,这之间明显缺乏一个转换的步骤。
同时,我们再看看第五题,会不会产生一个完善的思路出来?没错,相关流程中4.0版本与之前的版本相比,除了相关的代码从C变成C++之外,它多出了实现内存读取dex的功能,就在Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数中,猜想方案中的步骤也无非应该是在2.3版本中对应可能实现该功能的一种方式,两者相差不会很大。我们把4.0中实现的方法、流程吃的透彻一点,对于实验方案的出炉一定有事半功倍的效果。于是,在文件、函数各种跳转之后,有了下面一个流程简图:
这是4.1版本源码的函数调用简易流程,去掉了各种容错、抛异常的部分,只列出了关键函数,每个虚线框中表示一个文件。通过代码可以很明确的了解,所谓内存加载dex也就做了两件事,第一是加载;第二是注册(实际操作是写入全局变量的一个属性表,我觉得用“注册”来描述这个动作也蛮贴切)。通过代码走读,也完全可以了解到猜测方案中的第三步,使用allocateAuxStructures方法得到DvmDex其实无法达到加载dex的目的,该函数只是做了一个转换,malloc了一个DvmDex结构而已。也就是说,完全按照猜想方案来,是无法正确加载dex数据的。
这个时候再看第六题就显得无足轻重,Android 版本问题固然是阻碍通用方案实现的最大障碍之一,但是重点不在猜想方案设计的诸函数中,因为猜想方案本身就略显骨感,不能实现预期的目的。
综上,我们可以根据猜想方案衍生出一个预计可行的方案。其实无论是猜想方案还是4.0之后系统中的实现,它们的目的是相同的:加载、注册。加载实现的重点在于DexPrepare文件中的rewriteDex函数;注册的实现重点则在于Init文件中的gDvm变量。打通这两个关节,应该可以算是又向前迈上了一步。抛开不同版本的DVM其相关数据结构、实现过程的差别带来的阻碍,先考虑在低版本拉通这个过程,那新的思路与实现目标就是:
仿照4.0的Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数实现,在jni中实现内存加载的方法,测试系统为Android2.3。
Jni实现的大体思路如下:
- 以2.3为准,引入相关结构的定义;
- 以上面的简易流程图为例,实现内存加载的模拟方法:涉及到底层对应的方法时,静态的方法,抠出3源码在jni代码中实现;
- 其他方法采用dlsym的方式指针调用;
- gDvm通过dlsym获取;
大致类似于:
int mock_dalvik_system_DexFile_openDexFile_bytearray(u1 * olddata, long len) {
LOGI("before make pRawDexFile !!");
DexOrJar* pDexOrJar = NULL;
RawDexFile* pRawDexFile = (RawDexFile*) calloc(1, sizeof(RawDexFile));
if (0 == dvmRawDexFileOpenArray(olddata, len, &pRawDexFile)) {
LOGV("Unable to open in-memory DEX file");
} else {
LOGV("open in-memory DEX file success!");
}
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = true;
pDexOrJar->pRawDexFile = pRawDexFile;
//pDexOrJar->pDexMemory = olddata;
pDexOrJar->fileName = strdup("<memory>"); // Needs to be free()able.
LOGI("before addToDexFileTable, and pRawDexFile %p", pRawDexFile);
addToDexFileTable(pDexOrJar);
LOGI("addToDexFileTable success return %p", pDexOrJar);
return (int) pDexOrJar;
}
抠源码和结构并不是个简单的活计,当然并不是说很难,准确的说是有点枯燥。测试好能够正常拿到gDvm和各相关函数指针之后就可以开始验证了。
临时结论
在2.3系统的验证中,依样画葫芦还是出了问题,在抠出来的rewriteDex代码实现中,最后一步验证和优化class数据的函数verifyAndOptimizeClasses报错异常。由于该函数是通过函数指针直接调用底层代码的,所以无法直接看出出错原因,项目暂停,没有时间继续探索了,暂时记录在此。
思考一下依样画葫芦的方法,依的是4.0的样在2.3上画葫芦可以预见的是版本间的差别可能会造成不可预期的谬误。具体如何在2.3中实现内存加载,估计还要从2.3本身如何加载dex入手去分析实现过程,来优化上面的实现。