由于APK中包含了几乎所有的源代码,在最早的APK中是不对这些文件进行任何保护的,这时候通过baksmali/dex2jar/apktool等工具可以很容易反编译分析出APK代码逻辑,因此在代码防护技术上随着不断的攻防对抗发展出以下几代加固技术:
- ProGuard/DexGuard
- 整体DEX加固
- 类抽取
- VMP & Java2C
最常用也是最简单的代码保护方法:字符串混淆,通过对变量名、类名、方法名进行替换混淆,利用a、b、c甚至不可见的字符来进行替换,达到代码不易读的效果,其次还可以压缩Dex的大小。ProGuard是Google自带的免费的混淆器,DexGuard则是收费版本,同时提供了字符串加密、花指令、资源加密等功能。
初代壳的核心原理就是 DEX 整体加密然后动态加载,也有人具体划分为落地壳和不落地加载壳,两者的区别仅在于解密后的DEX是否持久化到本地,对于落地壳来说就是需要先解密文件,然后写入到另外一个文件当中,然后再调用 DexClassLoader 或者其他加载函数来加载解密后的文件,对于不落地壳来说则是直接在内存中解密内存中加载,对于加固方可能是个技术迭代,但对于脱壳方来说,都可以通过一种方法来解决即DEX DUMP。
早先的脱壳技术中主要是通过动态分析调试来进行脱壳,例如通过IDA断点调试然后DUMP内存的方案(现在可以用Frida内存漫游进行DUMP,比如葫芦娃大佬的Frida-DexDump),DUMP内存的方式一种是通过Hook某个加载DexFile的函数(不同安卓版本可能不同)获得DexFile结构体的内存起始地址和大小,另一种则是内存中搜索Dex文件的magic魔数,找到内存起始地址,然后根据Dex文件格式获取Dex文件大小,后来也有些加固为了对抗这种方式会将DEX魔数抹去。
可以看出这种加固和脱壳方式的粒度都是Dex级别,而且代码数据总是结构完整的存储在一段内存里面,这是一个致命的弱点,一旦反注入、反调试等措施被破解,这个保护就相当于是已经失败了。
二代壳的核心突破在于将类中的方法指令进行剥离,壳通过Hook在方法执行时才会进行解密填充,甚至执行完之后还会消除,因此这种壳保护下,通过第一代壳脱壳的方式在DEX加载时将DEX DUMP出来,也只能得到一个个空方法。
但由于涉及到方法指令的回填,因此也带来了一定的兼容性以及性能影响。所以一般来说,正常 APP 是不会将所有代码都进行抽取的,只会保护核心代码。
由于三代壳VMP实现难度角度,因此二代壳也是目前360、爱加密、梆梆采用的主要加固方式,对于二代壳中具体的也有很多对抗方式的小升级,例如防止通过类初始化还原方法指令的炸弹类、不回填至codeItem中、通过方法器分发、方法执行完后消除等,目前市面上也有很多针对二代壳的自动脱壳机,例如:
- DexHunter 基于类主动加载,自定义ROM
- FUPK3\FART 基于方法主动调用,自定义ROM(也有Frida版FART)
- FDex Java层的类主动加载,Xposed插件
目前大多数的主流脱壳机都是通过修改ROM来实现脱壳,基于ROM的优点是可以任意使用系统API,但是对于使用方较为麻烦需要刷机。
对于二代壳的脱壳方案主要有两种,一种为基于类主动加载,适用于在类加载和初始化时就会将方法指令回填的壳,例如DexHunter和FDex,遍历Dex中的所有类,然后模拟加载类的流程(例如调用 dvmFindClass 等系列函数),再DUMP DEX,另一种则是基于方法主动调用如FART,时机覆盖更全。为了对抗 DexHunter, 有的代码抽取方案已经不再类加载时还原代码了,而是在比 DexHunter 更后面的某个时机。因为可以做代码还原的点比较多,所以采用主动调用的方案,可以完全规避掉时机的问题。原理是对执行方法的入口函数进行插桩,在这个地方判断是否带有主动调用的标志,若属于主动调用则 Dump CodeItem 的数据,然后在进行 Dex 重建。而主动调用放在比较顶层的地方,这样就可以覆盖所有代码还原的时机。这个方案虽然理论上也可以通过注入和 Hook 来做,但是需要插桩的函数以及一些需要调用的函数有可能没有导出,所以会比较麻烦。
常说的VMP壳就是DEX虚拟机保护的缩写,这代技术的核心在于自己编写解释器执行自定义的方法指令,因此就算将指令DUMP出来也无法按照安卓虚拟机的规范进行解释还原,但现在并没有真正意义上的VMP,基本是指令操作码opcode替换,因此脱壳核心也是在于找到opcode的映射。
对于Native化技术,一种是Java2C,即将java通过一定编译原理转化成C语言,另一种则是直接使用JNI来编写核心代码逻辑,之所以要转换成Native层是因为Native层的保护更加丰富、门槛也更加高。Native层除了也有加壳技术,OLLVM更是一座难以逾越的大山,其包含的混淆手段有:
- 虚假控制流
- 控制流平坦化(核心)
- 指令替换等
可以发现,编写一个脱壳机有两个非常重要的因素,脱壳点和脱壳时机,脱壳点决定了如何获得Dex文件在内存中的位置,脱壳时机则是反应了能否获取到原始的真正的Dex文件。ART下影响脱壳的关键的一个类就是DexFile,那么我们便可以围绕这个类,实现在Android庞大的系统源码中快速定位脱壳点,从而能够找到“海量”的脱壳点。更具体可仔细阅读看雪中hanbingle讲师的相关帖子。