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

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函数的具体实现,源码在:

dalvik2\vm\native\dalvik_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的加固技术吧。


ALICTF2014 EvilAPK4脱壳分析的更多相关文章

  1. 脱壳入门----脱ASPack壳保护的DLL

    前言 结合脱dll壳的基本思路,对看雪加密解密里的一个ASPack壳保护的dll进行脱壳分析. 脱壳详细过程 寻找程序的OEP 先将目标DLL拖入OD,来到壳的入口处. 然后利用堆栈平衡原理在push ...

  2. 天草(初级+中级+高级)VIP和黑鹰VIP破解教程(全部iso下载地址)

    以下就是我收集的教程地址,之前我收集到的都是一课一课下载的,虽然这样,我也下载完了天草的全部课程.这里分享的是在一起的iso文件,比起一课课下载爽多了.~~ 还有这些教程都是从零起点开始教的,不用担心 ...

  3. 手动脱UPX 壳实战

    作者:Fly2015 Windows平台的加壳软件还是比較多的,因此有非常多人对于PC软件的脱壳乐此不彼,本人菜鸟一枚,也学习一下PC的脱壳.要对软件进行脱壳.首先第一步就是 查壳.然后才是 脱壳. ...

  4. 手动脱FSG壳实战

    作者:Fly2015 对于FSG壳.之前没有接触过是第一次接触.这次拿来脱壳的程序仍然是吾爱破解论坛破解培训的作业3的程序.对于这个壳折腾了一会儿,后来还是被搞定了. 1.查壳 首先对该程序(吾爱破解 ...

  5. 手动脱ORiEN壳实战

    作者:Fly2015 ORiEN这种壳之前没有接触,到底是压缩壳还是加密壳也不知道,只能试一试喽.需要脱壳的程序是吾爱破解脱壳练习第7期的题目. 首先对加壳程序进行查壳,这一步也是程序脱壳的必要的一步 ...

  6. 手动脱Mole Box V2.6.5壳实战

    作者:Fly2015 这个程序是吾爱破解脱壳练习第8期的加壳程序,该程序的壳是MoleBox V2.6.5壳,之前也碰过该种壳但是这个程序似乎要复杂一点. 首先对加壳程序进行侦壳处理. Exeinfo ...

  7. IDA分析脱壳后丢失导入表的PE

    1. 问题 一些程序经过脱壳后(如用OD的dump插件),一些导入表信息丢失了,导致拖入IDA后看不到API的信息(如右图所示,第一个红圈处实际是GetCurrentProcessId),给分析造成极 ...

  8. UPX脱壳全程分析(转)

    [文章标题]: UPX脱壳全程分析 [保护方式]: 本地验证 [使用工具]: OllyDBG [作者声明]: 只是感兴趣,没有其他目的.失误之处敬请诸位大侠赐教! ------------------ ...

  9. DexHunter脱壳神器分析

    0x00 这篇文章我们分析Android脱壳神器DexHunter的源码. DexHunter作者也写了一篇介绍它的文章从Android执行时出发.打造我们的脱壳神器.DexHunter源码位于htt ...

随机推荐

  1. Linux下Jenkins与GitHub自动构建NetCore与部署

    今天我们来谈谈NetCore在Linux底下的持续集成与部署.NetCore我就不多介绍了,持续集成用的是Jenkins,源代码管理器用的是GitHub.我们就跟着博文往下走吧. 1.Linux环境 ...

  2. Jquery动态添加多行,返回数据至每一行中

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="sys_channel_ed ...

  3. 零基础快速入门SpringBoot2.0教程 (四)

    一.JMS介绍和使用场景及基础编程模型 简介:讲解什么是小写队列,JMS的基础知识和使用场景 1.什么是JMS: Java消息服务(Java Message Service),Java平台中关于面向消 ...

  4. Java continue break 制作简单聊天室程序,屏蔽不文明语言,显示每句话聊天时间 for(;;) SimpleDateFormat("yyyy-MM-dd hh:mm:ss") equalsIgnoreCase

    package com.swift; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Scanne ...

  5. dom节点获取文本的方式

    1. innerHTML innerHTML可以作为获取文本的方法也可以作为修改文本内容的方法 element.innerHTML 会直接返回element节点下所有的HTML化的文本内容 <b ...

  6. webSocket使用心跳包实现断线重连

    首先new一个webscoket的连接 let noticeSocketLink = new WebSocket(‘webSocket的地址’) 这里是连接成功之后的操作 linkNoticeWebs ...

  7. 第30题:LeetCode155. Min Stack最小栈

    设计一个支持 push,pop,top 操作,并能在O(1)时间内检索到最小元素的栈. push(x) -- 将元素 x 推入栈中. pop() -- 删除栈顶的元素. top() -- 获取栈顶元素 ...

  8. 【NTT】bzoj3992: [SDOI2015]序列统计

    板子题都差点不会了 Description 小C有一个集合S,里面的元素都是小于M的非负整数.他用程序编写了一个数列生成器,可以生成一个长度为N的数 列,数列中的每个数都属于集合S.小C用这个生成器生 ...

  9. 14.3-ELK重难点总结和整体优化配置

    本文收录在Linux运维企业架构实战系列 做了几周的测试,踩了无数的坑,总结一下,全是干货,给大家分享~ 一.elk 实用知识点总结 1.编码转换问题(主要就是中文乱码) (1)input 中的cod ...

  10. php中关于empty()函数是否为真的判断

    <?php// $a = 0;  ==> 符合empty,empty($a)为true// $a = '0';  ==> 符合empty,empty($a)为true// $a = ...