一、前言
Android中加固方案一直在进步,因为新的加固方案出来就会被人无情的破解脱壳了,从第一代加固方案落地加密dex文件,第二代加固方案不落地加密dex文件,在到第三代加固方案类方法抽取,以后后面的更多加固方案来说都是安全的进步,关于脱壳方案网上有很多资料,但是加固方案却没多少资料,因为有些加固方案是一些加固公司的商业机密不可泄露的,所以我们现在看到的网上加固方案还是以前的加固方案,不了解以前的加固方案同学,可以看我写的逆向大黄书「Android应用安全防护和逆向分析」,而本文就来介绍如何实现类方法抽取方案来实现应用加固。
二、指令抽取方案
首先不多说,这种方案依赖的技术就是拦截系统的加载类函数,然后进行类方法指令还原。所以这个技术必须了解,好在我们在之前一篇文章中已经详细介绍了如何在native层拦截系统函数实现应用方法指令篡改,不了解的同学可以看这篇文章:Android中Hook系统函数实现运行时态篡改方法执行逻辑;本文的技术是依赖于这篇文章,如果这篇文章没看明白,那么本文将很难看懂。一定要先看这篇文章。不多说了,我们在介绍加固方案主要从两方面入手,一方面是如何进行指令抽取,一方面是如何进行指令还原,所以下面先来介绍如何进行类方法的指令抽取功能。
我们还是先用之前的工程作为案例:
在这个类中,我们定义了一个获取密码的方法:
现在为了安全,我们将这个方法信息置空,可以先用010Editor工具查看这个方法的指令信息:
看到这个方法包含了3个指令,那么我们如何将其设置成0呢?这里就需要自己写个工具了,因为我们不能依赖于这个工具手动去置空,那就太不智能了。这个就需要借助之前介绍dex文件格式解析的时候那个源码了。我们需要做一下改动就可以实现这个功能。dex文件格式解析源码下载地址:https://github.com/fourbrother/parse_androiddex;是个java工程,下载之后直接导入Eclipse即可。代码逻辑不做分析了,不了解的同学看之前的一篇文章:Android逆向之旅---解析编译之后的Dex文件格式;我们的目的就是想置空指定方法的的指令数据,那么直接去解析方法指令的那块逻辑即可:
这里在解析代码结构的时候,对类方法和对象方法的代码结构体信息,进行保存,用到的是map结构,而key值就必须用方法的唯一签名信息,这样后面会通过具体的方法签名信息获取到该方法对应的代码结构体信息,然后进行操作:
这里通过解析完dex文件格式之后:在通过指定类的指定方法检索到其代码结构体信息,然后拿到指令个数和指令的偏移地址,构造一个空指令集,覆盖原始指令。最后重新计算文件的checksum和signature值,回写回去。这里最后一步非常重要,如果dex文件的checksum和signature值不正确,dex文件就是失效的。没任何意义了加载也是失败。下面在来详细看一下这两个值的计算方法:
这两个值的计算方法很简单,我们可以从系统源码查看[Android源码目录/dalvik/libdex/DexFile.cpp]中,首先看看checksum的计算方法:
整个dex文件去除头部中的magic和checksum共12个字节,余下的内容计算crc即可。而signature的计算方法:
对整个dex文件,去除头部信息中的magic,checksum,signature共32个字节,余下的内容做sha1计算即可。最终都要回写到文件中的头部信息,我们可以利用010Editor查看:
这样我们就一气呵成,利用自己写的工具对指定方法进行指令置空:先解析原始dex文件格式,保存所有类的所有方法的代码结构体信息,然后通过传入的方法签名信息检索到其代码结构体信息,在获取其指令个数和指令偏移地址,构造出同数量的空指令集,覆盖原始指令实现置空。最后一步非常关键,就是需要重新计算dex文件的checksum和signature信息回写到头部信息中。
下面我们就用上面的CoreDex工程编译出来的dex文件作为案例,借助这个工具,将CoreUtils.getPwd方法指令置空,然后用IDA工具进行查看:
不相信的话,我们在继续用Jadx工具进行查看:
这个方法就是个空方法了,到这里我们抽空方法指令的功能就完成了,下面继续来看如何进行还原指令。
三、指令还原
还原指令就简单了,直接借助之前那个篡改内存指令的代码即可,在native层hook系统函数dexFindClass,然后进行类的方法名过滤,获取指定方法的代码结构体,修改指令内存块为可读属性,然后把指令还原即可:
这里为了简单,就把原始指令保存在native中,然后直接还原了。到这里我们还原指令的逻辑也完成了,下面就运行项目看看日志和效果,先把之前抽取指令的dex文件拷贝到SD卡中,然后用这个工程进行动态加载运行看看效果:
这里看到了,dex中的指令的确被抽取置空了,然后我们也还原成功了,方法执行也有结果了:
四、流程总结
到这里我们就介绍完了本文内容,下面来总结一下流程:
第一、抽取指令流程
1、解析原始dex文件格式,保存所有方法的代码结构体信息。
2、通过传入需要置空指令的方法和类名,检索到其代码结构体信息。
3、通过方法的代码结构体信息获取指令个数和偏移地址,构造空指令集,然后覆盖原始指令。
4、重新计算dex文件的checksum和signature信息,回写到头部信息中。
第二、指令还原流程
1、native层hook系统函数dexFindClass,获取类结构体信息。
2、获取类中所有的方法信息,通过指定方法名进行过滤,获取该方法的代码结构体信息。
3、获取该方法被抽取的指令集,修改方法对应的内存地址为可读属性,直接进行指令还原。
在这个过程中,抽取指令需要借助编写的工具,当然工具可以进行深入优化。可以留给你们进行完善。对于还原指令中,如何保存抽取指令并没有介绍,这部分内容可以后续完善优化,比如我们可以将抽取的指令再做一次加密保存到程序中的某个地方,在指令还原的时候去这个文件进行读取解密即可。下面来看看整个流程图:
本文涉及到的代码项目可以去编码美丽小密圈自取,欢迎加入小密圈一起学习探讨技术
五、总结
在之前的两代加固方案中,我们都可以拦截系统解析dex的函数或者IDA动态调试来dump出解密后的dex文件,那个依赖的核心就是不管dex文件怎么加密,最终都是需要解密加载到内存中运行的。但是这种类方法信息抽取加固方案就可以解决dump出内存的dex文件的问题。因为我们是在方法程序运行的时候进行指令还原,所以在此之前dump出的dex文件都是被抽取指令的,其实没什么意义了。所以后面结合native层对抽取指令加密等更强的安全防护,这种加固方案还是可取的。
不过遗憾的是,这种加固方案并不是完美的,毕竟没有绝对的安全可言,我们可以看到在还原指令的时候是拦截系统函数做到的,那么破解的人也可以拦截这个系统函数,他在你已经还原成功之后在获取类信息,然后重组dex文件,这样就可以完美的还原原始dex文件了。不过这个破解难度就会增大了。