Android APP脱壳的本质就是对内存中处于解密状态的dex的dump。
首先要区分这里的脱壳和修复的区别。这里的脱壳指的是对加固apk中保护的dex的整体的dump,不管是函数抽取、dex2c还是vmp壳,首要做的就是对整体dex的dump,然后再对脱壳下来的dex进行修复。
要达到对apk的脱壳,最为关键的就是准确定位内存中解密后的dex文件的起始地址和大小。那么这里要达成对apk的成功脱壳,就有两个最为关键的要素:
  1. 内存中dex的起始地址和大小,只有拿到这两个要素,才能够成功dump下内存中的dex
  1. 脱壳时机,只有正确的脱壳时机,才能够dump下明文状态的dex。否则,时机不对,即使是正确的起始地址和大小,dump下来的也可能只是密文。
 

脱壳点的选择

脱壳点其实有很多:
  1. 直接查找法,就是指以DexFile为关键字,在庞大的源码库中检索定位可能的脱壳点。
  1. 间接法,就是指以DexFile为出发点,寻找能够间接获取到DexFile对象的。
注意,一般情况下,这里脱下来的壳是没有进行指令还原的,指令抽取壳需要我们进行主动调用,vmp壳需要其他形式的修复。

FART主动调用

脱壳点

FART使用的是通过运行过程中ArtMethod来使用GetDexFile()函数从而获取到DexFile对象引用进而达成dex的dump。

ClassLoader 的选取

由于主动调用需要遍历APP所有的类,所以需要找到隐藏类的 ClassLoader 才行。
对于一个正常的应用来说,最终都要由一个个的Activity来展示应用的界面并和用户完成交互,那么我们就可以选择在ActivityThread中的performLaunchActivity函数作为时机,来获取最终的应用的Classloader。选择该函数还有一个好处在于该函数和应用的最终的application同在ActivityThread类中,可以很方便获取到该类的成员。
有了ClassLoader 之后,再获取到Classloader中的mCookie,即Native层中的DexFile(不同版本的源码有不同的逻辑,需要自行查看)。

类函数的主动调用

通过 JNI 调用来实现类函数的调用:
这里的线程 self 参数传递的是 null,是为了区分是我们脱壳主动调用的,还是app自己执行逻辑调用的。
最后在 ArtMethod 执行的时候:
这里就可以dump方法的指令了,因为方法要准备执行了,所以指令肯定已经被替换成真实的指令了,否则App会报错了。
dump 方法指令完成后,需要回填到 dex 中,这需要脚本处理,就不展开了。

一些更深的主动调用

虽然上面的主动调用很好用,但是实际上是可以被针对的:
一些加固会将方法指令再藏一层:
这个抽取壳,必须在函数执行了之后,才会还原出真实的函数。回想一下前面说的FART的主动调用深度。发现函数真正执行前就已经被我们直接结束掉了。所以我们需要更深的主动调用才能够解决这个抽取壳。
首先,仍然进行参数模拟,走到 ArtMethod 的 invoke 方法,强制走解释器模式,走到解释器的 invoke 方法,这里我们需要对指令进行判断:
 
下文进行具体的代码分析。

dex2oat

dex2oat完成了对抽取的dex进行编译生成了oat文件,后续的函数运行中,从oat中取出函数编译生成的二进制代码来执行,因此函数对dex填充后,如果时机不对,时机在dex2oat后,自然从dex2oat后那么我们动态修改的dex中的smali指令流就不会生效,因为后面app运行调用的真正的代码就会从dex2oat编译生成的oat文件,和以前的dex无关了。
因此如果希望填充回去smali指令生效要么禁用dex2oat实现阻止编译,这样对加载到内存中的dex文件进行填充始终会保持生效,要么保持dex2oat编译,但是还原代码时机要早于dex2oat就ok了,保证dex2oat再次对dex编译的时候,dex已经是一个完整dex,不会影响我们填充的代码,但是肯定dex文件存在完整的时候,可以利用dex2oat编译的流程进行脱壳,一般加壳厂商都是牺牲掉app一部分的运行效率,干掉dex2oat的过程,因为google本身提倡dex2oat就是为了提升app运行效率。
 
Loading...