Dalvik下加载Dex原理分析
Art下加载Dex原理分析
修复So文件
我们用IDA打开libshella-2.10.1.so,意料之中的是,IDA打开什么也看不到。
但是修复后的So文件在IDA还是看不到init_array段,JNI_OnLoad函数也是加密状态。
第一次解密
我没有关注init_array的解密算法是什么样的,当它解密完成之后,我将0x2000-0x3954偏移处的代码dump出来了,然后替换掉上面修复后的libshella_fix.so对应的字节,这样生成的So就包含解密后的Jni_OnLoad函数了
第二次解密
当这个函数执行完之后,通过dlsym 获取JNI_OnLoad函数的地址已经不是开始的JNI_OnLoad函数地址,我们这里成为new_JNI_Onload,而是处于动态分配的debug内存区域,其实这些debug内存区域的代码就是前面sub_1968()函数解密出来的
为了方便分析,我将libshella.so debug区域以及libc.so libdvm.so进行了内存快照拍摄
解密Dex
在load函数中,首先会判断当前是dalvik还是art虚拟机,然后执行对应的加载Dex方法
获取到解密前dex的内存地址,就会执行decrypt函数对dex进行解密,手动脱壳的话,在这里就可以dump处真正的dex了
Dalvik下加载Dex
首先Legu会通过loadDex函数加载mix.dex,从而得到一个表示mix.dex的mCookie对象
然后Legu会构造一个0x34字节的结构体DexHeaderBak,用来保存真正Dex的DexHeader信息
然后用mmap分配一段内存mmap_buffer,在其中填充一些真正Dex的信息,我画了一张图描述mmap_bufffer的内存结构
在Android4.4中,mCookie对象是指向DexOrJar结构的指针,
Legu会通过mixdex_cookie获取pRawDexFile指针,再来获取pDvmDex指针,
Art(Android 6.0)下加载Dex分析
Legu在Art下会hook几个系统函数,并且获取libart.so中的art::dexFile::OpenFile函数的地址
我分析了下Android 6.0下的OpenFile函数
fake_mmap代码如下,首先会解密处真正的dex内容,然后用真正的Dex地址替换mmap返回值
OpenFile进行了mmap操作后,会进行OpenMemory加载Dex,经过上面的hook,表面上是打开base.odex oat文件,实际上OpenMemory真正的Dex文件,因此我们可以Hook OpenMemory达到dump dex的目的,所以可以看出Android Art下使用OpenMemory函数来加载dex文件。
Art下脱壳机编写
最后测试发现libshella 2.7-2.10版本的Dex都可以成功Dump出来
这篇文章是记录本人在学习Legu脱壳的心得,分析的样本是Legu libshella-2.10.so的版本。
本文分为几个部分:
修复So文件
第一次解密
第二次解密
解密Dex
Dalvik下脱壳机编写
Art下脱壳机编写
Legu的核心加固代码都是在libshella-x.so里面,
在这里我用ThomasKing的ELF修复工具试着修复一下,修复之后已经可以看到很多函数了
由于So加载完之后会调用init_array函数,我们从Android源码入手来获取init_array的地址,在Android源码 linker.cpp的代码中有这么一段代码就是来调用init和init_aray函数的
CallFunction("DT_INIT", init_func);
CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);
CallFunction("DT_INIT", init_func);
CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);
将手机中的linker拖入ida,找到DT_INIT_ARRAY字符串,可以获取调用CallArray的地址,在我的linker中,0x295E处就是调用init段的地址
我用修复后的So文件替换掉原始的Libshela-2.10.1.so文件后,App还可以成功运行,在这里就直接用修复后的So来动态调试
在init_array的函数中,首先会解密出JNI_OnLoad和反调试代码等等
解密是一个while循环,会从libshella.so 0x2000处开始解密,到0x3954处结束解密,解密完成之后,就只是创建了一个反调试线程,检测到反调试就执行raise(9),最简单的方法可以将call raise函数的代码nop掉
咋一看,很奇怪的是在JNI_OnLoad函数中,还会自身再调用JNI_OnLoad函数,关键地方在于sub_1968()函数,
这个函数比较庞大,我分析了半天,没分析处到底是怎么解密的,但是这里并不影响后面的分析,
我们用上面内存快照拍摄的生成的idb来分析new_JNI_OnLoad函数,在new_OnLoad函数中,看到了久违的registerNative注册操作,注册了Java层的load,runCreate等native函数
在Java层的attachBaseContext函数中,首先执行的是native层的load函数,
在解密真正的dex之前,首先是获取解密前dex的存储位置,很简单的是,解密前的Dex存储地址=内存odex地址+DexHeade->dataOff+DexHeade->dataSize
在我的那篇阿里早期加固代码还原的帖子中,在Dalvik下的Dex加载方式是通过Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个方式进行内存加载的,但是Legu在这里使用了一种更加高级的方法
struct DexOrJar {
char* fileName;
bool isDex;
bool okayToFree;
RawDexFile* pRawDexFile;
JarFile* pJarFile;
u1* pDexMemory; // malloc()ed memory, if any
};
struct DexOrJar {
char* fileName;
bool isDex;
bool okayToFree;
RawDexFile* pRawDexFile;
JarFile* pJarFile;
u1* pDexMemory; // malloc()ed memory, if any
};
struct RawDexFile {
char* cacheFileName;
DvmDex* pDvmDex;
};
struct RawDexFile {
char* cacheFileName;
DvmDex* pDvmDex;
};
最后将pDvmDex指向的内容全部替换为mmap_buffer结构体中的内容,这样mix.dex的mCookie对象已经表示为真正的Dex,而不是原本的mix.dex了,关于Legu为什么知道这么做,可能要分析Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个函数的原理了。
Dalvik下Dex的加载大部分都完成了,后面Legu加载的方法其实也就是MultiDex的多Dex加载方法,这里不做分析。
Android4.4下有Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个函数加载Dex,但是Android N以上就没有了这个函数,
让我开始很疑惑的是,Legu用OpenFile函数打开内存中的oat文件,并没有任何加载dex的操作
OpenFile首先会call fstat函数获取location的大小,但是此时执行的却是hook后的fstat,Legu会替换真正的location的大小,而是返回真正的Dex大小,
然后根据前面的大小调用MapFile函数,MapFile最终调用了mmap函数,此时执行的还是hook后的mmap,
Dalvik下脱壳机编写
根据上面的分析,在Dalivk下可以很容易地获取到真正Dex的内存位置:真正Dex存储地址=内存odex地址+DexHeade->dataOff+DexHeade->dataSize
App通过运行之后,注入后虽然可以Dump Dex,但是dump 的Dex跟原始的字节有几个字节不同,导致重打包运行出错,Legu在加载的时候是真正的Dex文件,但是脱离Legu代码运行起来后发现有几个字节变了,刚开始老以为是Legu对方法字节码做了处理,后来调试发现是Dalik自己改变的,字节码的变化不知道是不是Dalvik对字节码进行优化导致的。
Art下Dump 得到的Dex是完成正确的,推荐大家在Art下进行Dump Dex
void DumpDex_kitkat(char* pkgName)
{
int pid=getpid();
printf("pid:%d\n",pid);
char filename[100]={0};
char dumpfilepath[256]={0};
char* s;
unsigned int startAddr=NULL;
unsigned int endAddr=NULL;
unsigned int mainDexAddr;
char* oatPath[256]={0};
errno=0;
sprintf(dumpfilepath,"/data/data/%s/dump.dex",pkgName);
sprintf(filename,"/proc/%d/maps",pid);
FILE *fp;
fp = fopen(filename, "r");
if(fp!=NULL)
{
char line [2048];
while (fgets(line, sizeof(line), fp ) != NULL ) /* read a line */
{
if (strstr(line, pkgName) != NULL)
{
if (strstr(line, "classes.dex") != NULL)
{
LOGI("dvm-found odex address");
s = strchr(line, '-');
if (s == NULL)
LOGI(" Error: string NULL");
*s++ = '\0';
//strtoul:将字符串转化成无符号整型
startAddr = (void *)strtoul(line, NULL, 16);
endAddr = (void *)strtoul(s, NULL, 16);
LOGI(" dvm classes.odex addr %x-%x", startAddr,endAddr);
break;
}
}
}
fclose ( fp);
}
else
{
LOGI("fopen maps failed");
return;
}
if(startAddr==NULL || endAddr==NULL)
{
LOGI("found odex or oat file failed");
return;
}
mainDexAddr=startAddr+0x28;
LOGI("dexAddr:%s",(unsigned char*)mainDexAddr);
int magic=*(unsigned int*)mainDexAddr;
if(magic!=0x0A786564)
{
LOGI("not find main Dex");
return ;
}
unsigned int OrgDexOffset=getOrgDexOffset(mainDexAddr);
LOGI("OrgDexOffset:%d",OrgDexOffset);
unsigned int realDexAddr=mainDexAddr+OrgDexOffset;
magic=*(unsigned int*)realDexAddr;
if(magic!=0x0A786564)
{
LOGI("not find real Dex");
return ;
}
unsigned int dexSize=*(unsigned int*)(realDexAddr+0x20);
LOGI("dexSize:%d",dexSize);
void* buffer=malloc(dexSize);
if(buffer==0)
{
LOGI("malloc dexsize buffer failed");
return;
}
memcpy(buffer,(void*)realDexAddr,dexSize);
FILE* fd_dump=fopen(dumpfilepath,"wb+");
if(fd_dump==NULL)
{
LOGI("fopen dumpfile error:%s",strerror(errno));
return;
}
fwrite(buffer,dexSize,1,fd_dump);
free(buffer);
fflush(fd_dump);
fclsoe(fd_dump);
}
void DumpDex_kitkat(char* pkgName)
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课