2

[原创]乐固libshella 2.10.1分析笔记

寒号鸟二代 2017-6-26 22:44 5753

这篇文章是记录本人在学习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出来

上传的附件:
最新回复 (47)
bengou 2017-6-27 08:37
2
谢谢分享
OnlyEnd 2017-6-27 09:52
3
gugubupt 2017-6-27 10:36
4
用ThomasKing的ELF修复工具试着修复一下
这里是直接使用工具进行修复么?还是先去内存dump  so文件下来?
直接使用工具修复不了啊
2
寒号鸟二代 2017-6-27 10:49
5
用工具直接修复  不需要dump
gugubupt 2017-6-27 10:52
6
寒号鸟二代 用工具直接修复 不需要dump
我用的工具不对么。。。测试修复出来的so文件是残缺的。。
楼主附件能附上修复工具么
2
寒号鸟二代 2017-6-27 10:59
7
http://bbs.pediy.com/thread-192874.htm  这个里面有
wooyunking 2017-6-27 11:00
8
楼主6啊,写的太好了
gugubupt 2017-6-27 11:10
9
寒号鸟二代 http://bbs.pediy.com/thread-192874.htm 这个里面有
嗯,工具没错,我用错了。。。
谢谢楼主
1
Caln 2017-6-27 16:32
10
6~
龙飞雪 2017-6-27 17:24
11
不错啊,有精力
magicxss 2017-6-27 19:13
12
66666,给楼主点赞
chmlqw 2017-6-28 15:14
13
谢谢LZ分享
2
大王叫我挖坟 2017-6-28 20:47
14
谢谢分享
lulusudoku 2017-7-7 00:48
15
楼主你好,请问so文件被加密了,你是怎么修改指令绕过反调试的呢?
一xOO 2017-7-25 16:23
16
偷懒使用游客下载下不了,我擦
星星当空照 2017-7-25 17:29
17
感谢楼主提供思路参考
skyun 2017-7-27 14:22
18

大神。有一点我不明白:

  buffer2 = (char *)j_j_j_malloc(256);        // art_dexFile_OpenFile调用了fstat函数

    art_dexFile_OpenFile(buffer2, odex_handle, (int)&g_oatPath, 0, (int)&error_message);// 

                                                // DexFile::OpenFile(int fd, const char* location, bool verify,std::string* error_msg) 



这个函数原型是4个参数,为什么她传了5个。第一个参数搞不懂是怎么回事。能否告知

skyun 2017-7-30 15:48
19

大神。有一点我不明白:
    buffer2  =  (char  *)j_j_j_malloc(256);                //  art_dexFile_OpenFile调用了fstat函数
        art_dexFile_OpenFile(buffer2,  odex_handle,  (int)&g_oatPath,  0,  (int)&error_message);// 
                                                                                                //  DexFile::OpenFile(int  fd,  const  char*  location,  bool  verify,std::string*  error_msg) 

2
寒号鸟二代 2017-7-30 16:12
20
this指针
skyun 2017-7-30 16:32
21

寒号鸟二代 this指针

我不明白的地方是。这个函数是static的。应该不需要传,this。还有。我也模拟了你的代码,运行,能调用openfile这个函数,但是执行完这个函数,所有的变量地址都变了。


void ( *art_dexFile_OpenFile)(char *,int fd, const char* location,  bool verify, std::string* error_msg); // r4@71
 std::string location = "";
 std::string err_msg;
 char *OpenMemoryname = "/data/data/com.example.nativedex/files/classes.dex"
 void *handle1 = dlopen("libart.so", RTLD_NOW);

 void*xx= dlsym(  handle1, "_ZN3art7DexFile8OpenFileEiPKcbPNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEE");
 art_dexFile_OpenFile= (void (*)(char *, int, const char *, bool, string *)) xx;
 char* g_buffer1 = (char *) malloc(256);

 int fd = open("/data/data/com.example.nativedex/files/classes.dex", O_RDONLY);
 art_dexFile_OpenFile((char *)g_buffer1, fd, OpenMemoryname, 0, &err_msg);

这是这个函数执行前后。变量变化的截图

skyun 2017-7-30 17:05
22
大神。。。。。
2
寒号鸟二代 2017-7-30 17:14
23
我回去看下
skyun 2017-7-30 17:17
24
寒号鸟二代 我回去看下
好的。谢谢了。辛苦了
2
寒号鸟二代 2017-7-30 20:39
25
DexFile::OpenFile(int  fd,  const  char*  location,  bool  verify,  std::string*  error_msg)  {前面有个DexFile类的this参数,那个g_buffer1是legu第一调用OpenFile函数测试用的把
skyun 2017-7-30 21:39
26
是的,我也想测一下,但是调用失败
skyun 2017-7-30 21:42
27
我赞赏了这个帖子,我看帖子上还是说赞赏金额0
2
寒号鸟二代 2017-7-30 21:55
28
skyun 我赞赏了这个帖子,我看帖子上还是说赞赏金额0
skyun 2017-7-30 23:50
29
求帮助
2
寒号鸟二代 2017-7-31 00:40
30
留个联系方式
skyun 2017-7-31 01:00
31

QQ已删除

一xOO 2017-8-1 11:49
32
你好,我找到一个新样本,我dump时发现dex分成两个了,现在smali重新打包回dex,显示文件数超过限制,这个问题怎么搞
2
寒号鸟二代 2017-8-1 12:16
33
可以参考multidex原理
一xOO 2017-8-1 14:41
34
寒号鸟二代 可以参考multidex原理
MultiDex需要源码吧,一旦导出成java就会一大堆问题吧
a晴天安好 2017-8-1 16:57
35
学习学习!
a晴天安好 2017-8-1 17:02
36
学习学习!
ZXC傲视 2017-8-3 11:43
37
楼主请问一下,这份在Dalvik下dump  DexFile的代码中这个getOrgDexOffset函数是什么回事,似乎在代码里找不到,我在编译时就报错了,求解答(°∀°)ノ
keeslient 2017-8-3 17:38
38
学习学习! 
一xOO 2017-8-5 15:58
39
请问第一次解密,这个从哪里可以看出解密地址呀?
xiziyunqi 2017-8-7 18:36
40
谢谢
clumsybirda 2017-8-9 18:46
41
怎么拍摄部分快照了,为什么我总是拍摄整片内存空间啊!!!!!!!!!!!!!!!!求解谢谢
clumsybirda 2017-8-9 18:47
42

怎么拍摄部分快照了,为什么我总是拍摄整片内存空间啊!!!!!!!!!!!!!!!!求解谢谢求解谢谢求解谢谢求解谢谢求解谢谢

lonelykin 2017-9-2 15:40
43
感谢分享
wx_天国的声音 2017-9-17 14:16
44
new_art_dexFile_openMemory这个函数,是在哪个文件里面调用的?
2
寒号鸟二代 2017-9-17 14:38
45
hook  art_dexFile_openMemory的replace函数
永恒丶冬瓜 6天前
46
楼主,  为什么你的debug区汇编那么清晰,  我的汇编还是红色的,  而且还没有函数名~
chhzh 6天前
47
太牛逼,正在学习!
暗香沉浮 1天前
48
大佬能不能发一份你用的在ida中解析  dex  结构体的头文件,我从安卓源码拿的  dexfile.h  来解析,偏移不对
返回