Android APP脱壳的本质就是对内存中处于解密状态的dex的dump。
首先要区分这里的脱壳和修复的区别。这里的脱壳指的是对加固apk中保护的dex的整体的dump,不管是函数抽取、dex2c还是vmp壳,首要做的就是对整体dex的dump,然后再对脱壳下来的dex进行修复。
要达到对apk的脱壳,最为关键的就是准确定位内存中解密后的dex文件的起始地址和大小。那么这里要达成对apk的成功脱壳,就有两个最为关键的要素:
- 内存中dex的起始地址和大小,只有拿到这两个要素,才能够成功dump下内存中的dex
- 脱壳时机,只有正确的脱壳时机,才能够dump下明文状态的dex。否则,时机不对,即使是正确的起始地址和大小,dump下来的也可能只是密文。
脱壳点的选择
脱壳点其实有很多:
- 直接查找法,就是指以DexFile为关键字,在庞大的源码库中检索定位可能的脱壳点。
- 间接法,就是指以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 调用来实现类函数的调用:
static void DexFile_dumpMethodCode(JNIEnv* env, jclass, jstring eachclassname, jstring methodname, jobject cookie, jobject method) {
ScopedFastNativeObjectAccess soa(env);
ArtMethod* called_method = ArtMethod::FromReflectedMethod(soa, method);
method ->myfartInvoke(method );
return;
}
void ArtMethod::myfartInvoke(ArtMethod* artmethod)
{ JValue *result=nullptr;
Thread *self=nullptr;
uint32_t temp=6;
uint32_t* args=&temp;
uint32_t args_size=6;
artmethod->Invoke(self, args, args_size, result, "fart");
}
这里的线程 self 参数传递的是 null,是为了区分是我们脱壳主动调用的,还是app自己执行逻辑调用的。
最后在 ArtMethod 执行的时候:
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
const char* shorty) {
// unsigned int tempresult=(unsigned int)self;
if (self== nullptr) {
LOG(INFO) <<"art_method.cc::Invoke is invoked by myfartinvoke";
dumpArtMethod(this);
return;
}
......
}
这里就可以dump方法的指令了,因为方法要准备执行了,所以指令肯定已经被替换成真实的指令了,否则App会报错了。
dump 方法指令完成后,需要回填到 dex 中,这需要脚本处理,就不展开了。
一些更深的主动调用
虽然上面的主动调用很好用,但是实际上是可以被针对的:
一些加固会将方法指令再藏一层:
.method public constructor <init>()V
.registers 2
goto :goto_c
:goto_1
nop
nop
return-void
:goto_c
const v0, 0x1669
invoke-static {v0}, Ls/h/e/l/l/H;->i(I)V
goto :goto_1
.end method
这个抽取壳,必须在函数执行了之后,才会还原出真实的函数。回想一下前面说的FART的主动调用深度。发现函数真正执行前就已经被我们直接结束掉了。所以我们需要更深的主动调用才能够解决这个抽取壳。
首先,仍然进行参数模拟,走到 ArtMethod 的 invoke 方法,强制走解释器模式,走到解释器的 invoke 方法,这里我们需要对指令进行判断:
第一个指令是goto的处理
第二个指令是const的处理
第三个指令是 Instruction::INVOKE_STATIC || Instruction::INVOKE_STATIC_RANGE,就可以脱壳并结束了
下文进行具体的代码分析。
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运行效率。