zoukankan      html  css  js  c++  java
  • ALICTF2014 EvilAPK4脱壳分析

    相关文件可以在下面链接中下载:

    http://pan.baidu.com/s/1sjpvFy9

    1 简述

    该apk使用libmobisec.so函数实现对dex的解密还原。真正的dex为assets目录下的cls.jar和jute.data文件。

    本分析文档主要用于讨论脱壳方面的技术,并非以获取该APK的登录密码为目的。所以虽然可以使用很多动态的Android 分析工具进行API跟踪,然后快速得到登录密码,但是我还是选择进行静态脱壳,毕竟没参加比赛,不用担心时间限制啊~

    分析文档只涉及核心逻辑,具体细节需要结合libmobisec.idb文档阅读,里面注释还是很详细的。

    2 分析

    2.1 初探

    关键函数在sub_24c84(我改名为了keyFunction),该函数进行一些准备工作后,就会在偏移值0x24eca处调用parse_dex函数(偏移值为0x26398)。此函数就是整个dex解密的核心函数!下面对它进行详细分析。

    2.1.1 openWithHeader函数解析

    首先在0x269c8调用openWithHeader函数。该函数的详细实现在0x285f0处,它的功能如下:

    ①获取真正的cr4解密key;

    ②打开并mmap  cls.jar,使用真正的key对cls.jar进行cr4解密,然后解压,解压算法为lzma,处理后的数据重新mmap;

    ③munmap掉第一次mmap的内存,将解密、解压后的cls.jar(就是一个dex文件)的首地址存放到struct1.cls_jar_mmap_addr中,将它的大小存放到struct1.umcompress_size中;

    Struc1为一个辅助结构它的内容如下:

    typedef struct struct1

    {

          int mmap_size  ; //文件mmap大小,需要注意的是,在cls.jar操作中它的大小刚好比unpacksize的大小多0x10,这是由mmap时的参数造成的!详情参见idb

          void* file_mmap_addr;  //这个文件mmap在内存中的首地址,对于cls.jar其向后偏移0x10才是dex.035的开始地址!

          void* file_path ; //file的绝对路径

    };

    ④返回struct1.cls_jar_mmap_addr值,并将上层参数r2赋值为;struct1.umcompress_size, 及r1赋值为cls_jar_mmap_addr;

    如何获取真正的cr4解密key?

    在ali::decryptRc4函数中,先获取原apk的classes.dex的crc32校验码(32bit),然后将一个硬编码的0x18字节的字符串按4字节为单位依次同crc32结果进行异或运算。运算得到的0x18字节的字符串就是真正的rc4解密密钥。

    为了以后叙诉的方便,将解密、解压后的cls.jar称作cls.dex。

    2.1.2 反射调用openDexFile函数加载cls.dex

    鉴于篇幅有限,这里就不详细说明了,大家可以直接移步到jack_jia大牛的博客:

    http://blog.csdn.net/androidsecurity/article/details/9674251

    需要提及一点,就是openDexFile加载、解析完cls.dex之后,会在内存重新生成一个经过优化后的cls.dex。之后的操作,都是针对这个优化后的cls.dex的。我们可以在libmobisec的0x27b14下断,r0的值为openDexFile加载、解析后的cls.dex的开始地址,dump出来即可。

    大家可以查看openDexFile函数的具体实现,源码在:

    dalvik2vm ativedalvik_system_DexFile.cpp

       关键函数在dvmRawDexFileOpenArray(pBytes, length, &pRawDexFile),他完成加载和解析工作。需要注意的是其中的dvmPrepareDexInMemory函数会调用rewrite函数,这又是需要重点关心的函数。这个函数完成dex的优化,认证,以及所有类的加载(loadAllClasses)和部分inline方法的加载。说个题外话,可以从loadAllClasses函数看出,对于dalvik虚拟机而言,它在解析dex文件的时候会且仅会把所有的DexclassDef结构加载到内存,而只有在使用到某个类的方法的时候才会具体地加载这个方法!这貌似就是之前有人提及过的基于方法粒度的dex加密的前提吧。

    按照正常情况,分析到此,就已经可以在openDexFile函数下断点,dump出解密后的dex了。但是,现实很残酷,我们将dump出来的dex,使用backsmali反编译为smali文件,得到的却是如下结果:

    所有类的所有方法都被替换成了上面这种形式!显然,得到的cls.dex并非一个完整的dex文件,它真正的方法都被替换掉了!

    看来脱壳过程不是想象中的那么简单啊。没办法,继续看libmobisec的代码。

    2.1.3 ali::dex_juicer.patch函数分析

    在加载完cls.dex之后,会接着执行ali::dex_juicer_patch函数。从名字就可以看出它是在对dex进行修补工作。继续分析,总结此函数的功能如下:

    ①使用同样的方式解密解压juice.data文件,得到文件decrypted_juice;

    ②通过一系列算法将decrypted_juice同cls.dex结合在一起,组合成为完整的dex.

    可以在libmobisec的0x27b60下断,此时的r5为juicer.data解密后的首地址,r3为解密后的长度,dump出来,保存为decrypted_juice即可。

    第二个功能总结起来就一句话,但实际情况很复杂,我最初就卡在了0x2a79c的函数上(函数名原来是sub_2a79c,后来直到大牛Flanker_017发了一份某个复旦大牛的解密算法后,才发现它原来就是readUleb128函数!这可能就是一个人进行分析时面临的最大问题——思维固化。听说那位大牛是初次接触android就把它给逆出来了,我......只能给他跪下~)。当然,理解了这个函数,并不意味着,就可以轻松的进行dex还原了。挑战才刚刚开始!

    如果仅仅看它的那些解密函数,是很难得到有用的信息的。我分析了一天都没理出个头绪。本着一切问题睡一觉之后都能解决的原则,我一觉睡到了第二天下午~~果然,灵感来了!

    2.2 换个角度分析问题

    回顾2.1.2,我发现所有的方法都被替换成了RuntimeException。现在,我们换个角度,不从逆向的角度来思索问题,转而从加固者的角度来思索:如何才能实现2.1.2的情况。

    首先,我们需要认真的分析2.1.2所展示出来的信息,总结一下:

    ①cls.dex包含了所有的类和方法,只是每个方法的真正代码都被替换了;

    ②所有的方法都被替换成了RuntimeException,但是,如果细心一点,就可以发现,它们并不是完全相同的!如MainActivity.onCreate方法的registers为3,而MainActivity.<init>方法的registers为2。

    如果了解dex文件结构的话,我相信大家此时已经豁然开朗:我们只需要将每个DexMehtod对应的DexCode结构体篡改为2.1.2的模式就可以了!至于修复,将DexMehtod的codeOff偏移值做个重定位,指向真正的DexCode,不就OK了?

    事实是我们猜想的那样么?这就需要获取每个method对应的DexCode进行验证了。

    2.2.1 获取DexCode

    要想获取DexCode结构体,看雪Xbalien的一篇关于dalvik自篡改的文章可以提供思路:

    http://bbs.pediy.com/showthread.php?t=176732

    总结一下,要找到A类的B方法对应的DexCode结构体(为了简便,这里省去了方法声明的匹配,原理是一样的,可根据需要自行添加)那么步骤如下:

    ①获取A类名对应的字符串ID­——A_class_stringID和B方法名对应的字符串ID——B_method_stringID;

    ②获取A_class_stringID对应的A_class_typeID;

    ③获取A_class_typeID对应的DexClassDef结构体的起始地址A_classDefAddr;

    ④根据A_class_typeID和B_method_stringID获取B_methodID;

    ⑤根据A_classDefAddr和B_methodID获取B_codeOff,它指向的就是dexCode结构体。

    为了方便大家理解和测试,我在附件中提供了读取codeOff的完整源码,这是用标准c库写的,linux和windows都可编译,只需要将main函数中的classname_string和methodname_string根据自己的需要进行赋值即可。运行效果如下:

     

    通过查询不同的method的DexCode,得出结论:我们的猜想成立!它是将所有方法的DexCode结构体都改成了如下结构:

    MainAcrivity.<init>的dexcode

    Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F

     

    0000F850                                        02 00[w1]  01 00[w2]                   

    0000F860   01 00[w3]  00 00 [w4] 00 00 00 00[w5]   06 00 00 00[w6]  22 00 17 01               "  

    0000F870   70 10 20 06 00 00 27 00[w7]   02 00 01 00 01 00 00 00   p     '        

    0000F880   00 00 00 00 06 00 00 00  22 00 17 01 70 10 20 06           "   p  

    0000F890   00 00 27 00 02 00 01 00  01 00 00 00 00 00 00 00     '            

    0000F8A0   06 00 00 00 22 00 17 01  70 10 20 06 00 00 27 00       "   p

     

     [w1]registersize = 2

     [w2]inssize = 1 参数个数

     [w3]outsSize = 1,调用其他方法时使用的寄存器个数

     [w4]triesSize

     [w5]debugInfoOff

     [w6]insnsSize = 6 指令集个数,以2字节为单位,

     [w7]紧跟着的6条指令

    registersize和inssize会根据具体地函数进行相应的变化,除此之外,都相同。

    2.2.2 修复dex

    在前面说过,将DexMehtod的codeOff偏移值做个重定位,指向真正的DexCode就可以完成修复了。libmobisec中的代码也印证了此想法:

    ali::dex_juicer_patch部分代码节选:

    注意:由于当时分析的时候,命名规则有点乱,可能会给各位观众大老爷带来困扰,这里的juicerdata就是decrypted_juice;dex就是cls.dex;ali::juiceMem就是decrypted_juice在内存中的首地址

        ........

        juiceMem[0] = *ali::juiceMem;  //0x401

        juiceMem[1] = ali::juiceMem[1]; //0x8a8

        juice_mmap_addr_Plus_0x?_ptr = (int)(ali::juiceMem + 2);

        if ( juiceMem[0] > 0x1FC00000 )

          m_0x1004 = -1;

        else

          m_0x1004 = 4 * juiceMem[0];

        juiceDexCodeOff = juiceMem[1];

        ali::orgOffset = operator new[](m_0x1004);

        while ( v9 != juiceMem[0] )

        {

          dex_offset_sum += readUleb128((int **)&juice_mmap_addr_Plus_0x?_ptr);

          data_offset = readUleb128((int **)&juice_mmap_addr_Plus_0x?_ptr);

          orgOffset_new0x1004 = ali::orgOffset;

          cur_dexaddr = dexStartAddr + dex_offset_sum;

          data_offset_sum += data_offset;

          *(_DWORD *)(orgOffset_new0x1004 + 4 * v9++) = readUleb128((int **)&cur_dexaddr);// 这个new_0x1004的数据有什么作用?我们在这里给他赋了值,但是却不知道在哪个地方需要用到他!

          changeCodeOffInDex(

            dexStartAddr,

            dex_offset_sum,

            (char *)ali::juiceMem + data_offset_sum + juiceDexCodeOff - dexStartAddr);

    /* 第三个参数很有意思,需要我们好好分析。首先它是4个变量的运算结果,但是注意,这4个变量中,ali::juiceMem, juiceDexCodeOff以及dexStartAddr均是确定的值,只有dats_offset_sum会不停变化。经过分析,发现第三个参数其实就是一个偏移值,这个偏移值表示的是juicerData中的某个DexCode数据的地址较dexStartAddr的偏移距离。*/

        }

        result = 0;

      }

      return result;

    进一步分析chageCodeOffInDex,它的代码翻译过来如下:

    ChangeCodeOffInDex(void* dexStartAddr, int dex_offset_sum, int rel_off){

        dexStartAddr [dex_off_sumd] = (rel_off & 0x7F) | 0x80

        dexStartAddr [dex_off_sumd+1] = ((rel_off >>7)&0x7F) | 0x80

        dexStartAddr [dex_off_sumd+2] = ((rel_off >>14)&0x7F) | 0x80

        dexStartAddr [dex_off_sumd+3] = ((rel_off >>21)&0x7F) | 0x80

        dexStartAddr [dex_off_sumd+4] = ((rel_off >>28)&0x7F)

    }

    很明显就是一个uleb128的数据。

    再详细解释下decrypted_juice的文件结构:

    patchCount

    constOffset

    dexOffset_1

    dataOffset_1

    dexOffset_2

    dataOffse_2

    ......

    Real_dexCode_1

    ......

    Real_dexCode_N

    这里patchCount = 0x401, constOffset = 0x8a8。这两个参数都是固定的,前者表示共有多少个dexCode需要重定位,后者为0x401个使用uleb128编码的dexOffset+ dataOffset所占用的总共字节大小(其实它们真正的大小为0x8a5,不过为了4字节对齐所以填充为0x8a8字节)。第一个dexOffset表示cls.dex中的第一个method的codeOff变量的地址较dexStartAddr的偏移值,之后的dexOffset都是相对于第一个codeOff地址的增量。同理第一个dataOffset表示decrypted_juice中的第一个real_dexCode的地址较decrypted_juice的偏移值,之后的dataOffset都是相对于第一个real_dexCode地址的增量。

    其实只要画个内存的草图就很容易理解了。虽然cls.dex与decrypted_juice在内存中并不连续,但由于decrypted_juice中存放的真正有用的信息仅仅是dexCode,所以只要将cls.dex中每个method的codeOff重定向到decrypted_juice中相应的dexCode就可以了。

    3 修复

    修复的话就很简单了,时间有限,我就不用C重写修复代码了,这里直接贴上那位牛人的python代码(为了方便理解,我做了一点点修改):

    import struct

     

    # return value, length

    def readLEB128(data,pos):

        length = 0

        tp = pos

        val = 0

        for i in xrange(5):

            val |= (data[tp] & 0x7F) << (7*i)

            length += 1

            if data[tp] & 0x80:

                tp += 1

            else:

                break

        return val, length

     

    data = bytearray(open('decrypted_juice','rb').read())

    dex = bytearray(open('cls.dex','rb').read())

     

    patch_count, data_offset = struct.unpack('<II',data[:8])  

    dexlen = len(dex)

     

    pos = 8

    dex_off_sumd = 0

    data_off_sumd = 0

    for i in xrange(patch_count):

        dex_off, skip = readLEB128(data, pos)

        dex_off_sumd += dex_off

        pos += skip

        data_off, skip = readLEB128(data, pos)

        data_off_sumd += data_off

        pos += skip

     

        reloff = dexlen + data_offset  + data_off_sumd

      

        dex[dex_off_sumd] = (reloff & 0x7F) | 0x80

        dex[dex_off_sumd+1] = ((reloff>>7)&0x7F) | 0x80

        dex[dex_off_sumd+2] = ((reloff>>14)&0x7F) | 0x80

        dex[dex_off_sumd+3] = ((reloff>>21)&0x7F) | 0x80

        dex[dex_off_sumd+4] = ((reloff>>28)&0x7F)

     

    with open('fixed.dex','wb') as fp:

        fp.write(dex)

        fp.write(data)

    马上使用dex2jar反编译fixed.dex,得到的smali代码如下:

    解密成功!

    不过,分析smali代码相对来说效率还是比较低的。最好能转换成java。果断使用dex2jar,结果悲剧了:

    从上图可以看出android.support.v4.app.qn的testdex2jarcrash方法造成了dex2jar的崩溃。凭直觉,不可能只有这一个testdex2jarcrash方法,找出smali文件中的所有testdex2jarcrash方法,删掉后(最好改为无用函数),重新生成dex文件(这里推荐使用android逆向助手,集成化逆向分析工具,很方便,需要的话大家可以自行百度)。再次使用dex2jar完美编译,然后使用jd-jui打开就可得到java源码了:

    鉴于jeb比jd-gui功能更强大,大家可直接将修复后的dex放到jeb进行分析。

    至此整个脱壳工作告一段落。剩下的获取登录密码什么的,就交给各位去练手啦。

    4 总结

    说句实话,要完成此APK的脱壳所需要的技术、知识还是很多的:动态调试,静态分析,熟悉dex文件结构,熟悉Android dex动态加载机制,甚至于了解dex2jar等逆向工具的缺陷等等等等。不过收获自不用说,对dex的加固算是正式入门了`(*∩_∩*)′。

    其实我这段时间一直研究的是so的加壳,但此APK并没有使用任何的so加固技术,可能是为了降低比赛难度吧,但还是有一种一拳打到空气上赶脚~算了,等以后有机会,再总结下so的加固技术吧。


  • 相关阅读:
    nginx-rtmp-module
    nginx搭建支持http和rtmp协议的流媒体服务器之一
    用开源nginx-rtmp-module搭建flash直播环境
    Nginx RTMP 功能研究
    开源的视频直播
    LeetCode题解之Rotate String
    LeetCode题解之 Letter Case Permutation
    LeetCode题解之 3Sum
    LeetCode 题解之 Two Sum
    LeetCode 题解之Linked List Cycle II
  • 原文地址:https://www.cnblogs.com/wanyuanchun/p/4015615.html
Copyright © 2011-2022 走看看