乐固libshella 2.10.1分析笔记

来源:互联网 发布:惠泽了知原版资料网址 编辑:程序博客网 时间:2024/05/16 04:41
这篇文章是记录本人在学习Legu脱壳的心得,分析的样本是Legu libshella-2.10.so的版本。
本文分为几个部分:
修复So文件
第一次解密
第二次解密
解密Dex

Dalvik下加载Dex原理分析

Art下加载Dex原理分析

Dalvik下脱壳机编写
Art下脱壳机编写


修复So文件

Legu的核心加固代码都是在libshella-x.so里面,

我们用IDA打开libshella-2.10.1.so,意料之中的是,IDA打开什么也看不到。

在这里我用ThomasKing的ELF修复工具试着修复一下,修复之后已经可以看到很多函数了

但是修复后的So文件在IDA还是看不到init_array段,JNI_OnLoad函数也是加密状态。

由于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);
将手机中的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掉

我没有关注init_array的解密算法是什么样的,当它解密完成之后,我将0x2000-0x3954偏移处的代码dump出来了,然后替换掉上面修复后的libshella_fix.so对应的字节,这样生成的So就包含解密后的Jni_OnLoad函数了


第二次解密

咋一看,很奇怪的是在JNI_OnLoad函数中,还会自身再调用JNI_OnLoad函数,关键地方在于sub_1968()函数,
这个函数比较庞大,我分析了半天,没分析处到底是怎么解密的,但是这里并不影响后面的分析,

当这个函数执行完之后,通过dlsym 获取JNI_OnLoad函数的地址已经不是开始的JNI_OnLoad函数地址,我们这里成为new_JNI_Onload,而是处于动态分配的debug内存区域,其实这些debug内存区域的代码就是前面sub_1968()函数解密出来的


为了方便分析,我将libshella.so debug区域以及libc.so libdvm.so进行了内存快照拍摄


我们用上面内存快照拍摄的生成的idb来分析new_JNI_OnLoad函数,在new_OnLoad函数中,看到了久违的registerNative注册操作,注册了Java层的load,runCreate等native函数


解密Dex

在Java层的attachBaseContext函数中,首先执行的是native层的load函数,

在load函数中,首先会判断当前是dalvik还是art虚拟机,然后执行对应的加载Dex方法

在解密真正的dex之前,首先是获取解密前dex的存储位置,很简单的是,解密前的Dex存储地址=内存odex地址+DexHeade->dataOff+DexHeade->dataSize

获取到解密前dex的内存地址,就会执行decrypt函数对dex进行解密,手动脱壳的话,在这里就可以dump处真正的dex了


Dalvik下加载Dex

在我的那篇阿里早期加固代码还原的帖子中,在Dalvik下的Dex加载方式是通过Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个方式进行内存加载的,但是Legu在这里使用了一种更加高级的方法

首先Legu会通过loadDex函数加载mix.dex,从而得到一个表示mix.dex的mCookie对象

然后Legu会构造一个0x34字节的结构体DexHeaderBak,用来保存真正Dex的DexHeader信息

然后用mmap分配一段内存mmap_buffer,在其中填充一些真正Dex的信息,我画了一张图描述mmap_bufffer的内存结构


在Android4.4中,mCookie对象是指向DexOrJar结构的指针,

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;
};

Legu会通过mixdex_cookie获取pRawDexFile指针,再来获取pDvmDex指针,

最后将pDvmDex指向的内容全部替换为mmap_buffer结构体中的内容,这样mix.dex的mCookie对象已经表示为真正的Dex,而不是原本的mix.dex了,关于Legu为什么知道这么做,可能要分析Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个函数的原理了。

Dalvik下Dex的加载大部分都完成了,后面Legu加载的方法其实也就是MultiDex的多Dex加载方法,这里不做分析。


Art(Android 6.0)下加载Dex分析

Android4.4下有Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个函数加载Dex,但是Android N以上就没有了这个函数,

Legu在Art下会hook几个系统函数,并且获取libart.so中的art::dexFile::OpenFile函数的地址


让我开始很疑惑的是,Legu用OpenFile函数打开内存中的oat文件,并没有任何加载dex的操作

我分析了下Android 6.0下的OpenFile函数

OpenFile首先会call fstat函数获取location的大小,但是此时执行的却是hook后的fstat,Legu会替换真正的location的大小,而是返回真正的Dex大小,
然后根据前面的大小调用MapFile函数,MapFile最终调用了mmap函数,此时执行的还是hook后的mmap,

fake_mmap代码如下,首先会解密处真正的dex内容,然后用真正的Dex地址替换mmap返回值


OpenFile进行了mmap操作后,会进行OpenMemory加载Dex,经过上面的hook,表面上是打开base.odex oat文件,实际上OpenMemory真正的Dex文件,因此我们可以Hook OpenMemory达到dump dex的目的,所以可以看出Android Art下使用OpenMemory函数来加载dex文件。


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);
}


Art下脱壳机编写

我这里是针对Android 6.0下hook OpenMemory函数,在5.1下OpenMemory函数可能参数有所改变要另外做处理
注入zygote Hook OpenMemory函数dump dex
uint32_t new_art_dexFile_openMemory(void* DexFile_thiz,char* base,int size,void* location,
                                    void* location_checksum,void* mem_map,void* oat_dex_file,void* error_meessage )
{
    if(*((uint32_t*)base)==0x0A786564)
    {
        int pid=getpid();
        const char* proc_name=get_process_name(pid);
        if(strstr(proc_name,g_szTargetName))
        {
            LOGI("openMemory found target dex");
            char szDumpPath[256]={0};
            sprintf(szDumpPath,"/data/data/%s/dump_marsha_%x.dex",g_szTargetName,size);
            FILE* fd_dump=fopen(szDumpPath,"wb+");
            if(fd_dump==NULL)
            {
                LOGI("fopen dumpfile error:%s",strerror(errno));
                return;
            }
            fwrite(base,size,1,fd_dump);
            fflush(fd_dump);
            fclose(fd_dump);
            LOGI("dex file save at %s size:%x",szDumpPath,size);
        }
        return old_openMemory(DexFile_thiz,base,size,location,location_checksum,mem_map,oat_dex_file,error_meessage);
    }
    else
    {
        return old_openMemory(DexFile_thiz,base,size,location,location_checksum,mem_map,oat_dex_file,error_meessage);
    }
}

最后测试发现libshella 2.7-2.10版本的Dex都可以成功Dump出来