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函数的具体实现,源码在:
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 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脱壳分析的更多相关文章
- 脱壳入门----脱ASPack壳保护的DLL
前言 结合脱dll壳的基本思路,对看雪加密解密里的一个ASPack壳保护的dll进行脱壳分析. 脱壳详细过程 寻找程序的OEP 先将目标DLL拖入OD,来到壳的入口处. 然后利用堆栈平衡原理在push ...
- 天草(初级+中级+高级)VIP和黑鹰VIP破解教程(全部iso下载地址)
以下就是我收集的教程地址,之前我收集到的都是一课一课下载的,虽然这样,我也下载完了天草的全部课程.这里分享的是在一起的iso文件,比起一课课下载爽多了.~~ 还有这些教程都是从零起点开始教的,不用担心 ...
- 手动脱UPX 壳实战
作者:Fly2015 Windows平台的加壳软件还是比較多的,因此有非常多人对于PC软件的脱壳乐此不彼,本人菜鸟一枚,也学习一下PC的脱壳.要对软件进行脱壳.首先第一步就是 查壳.然后才是 脱壳. ...
- 手动脱FSG壳实战
作者:Fly2015 对于FSG壳.之前没有接触过是第一次接触.这次拿来脱壳的程序仍然是吾爱破解论坛破解培训的作业3的程序.对于这个壳折腾了一会儿,后来还是被搞定了. 1.查壳 首先对该程序(吾爱破解 ...
- 手动脱ORiEN壳实战
作者:Fly2015 ORiEN这种壳之前没有接触,到底是压缩壳还是加密壳也不知道,只能试一试喽.需要脱壳的程序是吾爱破解脱壳练习第7期的题目. 首先对加壳程序进行查壳,这一步也是程序脱壳的必要的一步 ...
- 手动脱Mole Box V2.6.5壳实战
作者:Fly2015 这个程序是吾爱破解脱壳练习第8期的加壳程序,该程序的壳是MoleBox V2.6.5壳,之前也碰过该种壳但是这个程序似乎要复杂一点. 首先对加壳程序进行侦壳处理. Exeinfo ...
- IDA分析脱壳后丢失导入表的PE
1. 问题 一些程序经过脱壳后(如用OD的dump插件),一些导入表信息丢失了,导致拖入IDA后看不到API的信息(如右图所示,第一个红圈处实际是GetCurrentProcessId),给分析造成极 ...
- UPX脱壳全程分析(转)
[文章标题]: UPX脱壳全程分析 [保护方式]: 本地验证 [使用工具]: OllyDBG [作者声明]: 只是感兴趣,没有其他目的.失误之处敬请诸位大侠赐教! ------------------ ...
- DexHunter脱壳神器分析
0x00 这篇文章我们分析Android脱壳神器DexHunter的源码. DexHunter作者也写了一篇介绍它的文章从Android执行时出发.打造我们的脱壳神器.DexHunter源码位于htt ...
随机推荐
- Linux下Jenkins与GitHub自动构建NetCore与部署
今天我们来谈谈NetCore在Linux底下的持续集成与部署.NetCore我就不多介绍了,持续集成用的是Jenkins,源代码管理器用的是GitHub.我们就跟着博文往下走吧. 1.Linux环境 ...
- Jquery动态添加多行,返回数据至每一行中
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="sys_channel_ed ...
- 零基础快速入门SpringBoot2.0教程 (四)
一.JMS介绍和使用场景及基础编程模型 简介:讲解什么是小写队列,JMS的基础知识和使用场景 1.什么是JMS: Java消息服务(Java Message Service),Java平台中关于面向消 ...
- 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 ...
- dom节点获取文本的方式
1. innerHTML innerHTML可以作为获取文本的方法也可以作为修改文本内容的方法 element.innerHTML 会直接返回element节点下所有的HTML化的文本内容 <b ...
- webSocket使用心跳包实现断线重连
首先new一个webscoket的连接 let noticeSocketLink = new WebSocket(‘webSocket的地址’) 这里是连接成功之后的操作 linkNoticeWebs ...
- 第30题:LeetCode155. Min Stack最小栈
设计一个支持 push,pop,top 操作,并能在O(1)时间内检索到最小元素的栈. push(x) -- 将元素 x 推入栈中. pop() -- 删除栈顶的元素. top() -- 获取栈顶元素 ...
- 【NTT】bzoj3992: [SDOI2015]序列统计
板子题都差点不会了 Description 小C有一个集合S,里面的元素都是小于M的非负整数.他用程序编写了一个数列生成器,可以生成一个长度为N的数 列,数列中的每个数都属于集合S.小C用这个生成器生 ...
- 14.3-ELK重难点总结和整体优化配置
本文收录在Linux运维企业架构实战系列 做了几周的测试,踩了无数的坑,总结一下,全是干货,给大家分享~ 一.elk 实用知识点总结 1.编码转换问题(主要就是中文乱码) (1)input 中的cod ...
- php中关于empty()函数是否为真的判断
<?php// $a = 0; ==> 符合empty,empty($a)为true// $a = '0'; ==> 符合empty,empty($a)为true// $a = ...