加固与脱壳06 - 脱壳分析

418 阅读5分钟

Android APP脱壳的本质就是对内存中处于解密状态的dex的dump。

首先要区分这里的脱壳和修复的区别。这里的脱壳指的是对加固apk中保护的dex的整体的dump,不管是函数抽取、dex2c还是vmp壳,首要做的就是对整体dex的dump,然后再对脱壳下来的dex进行修复。

要达到对apk的脱壳,最为关键的就是准确定位内存中解密后的dex文件的起始地址和大小。那么这里要达成对apk的成功脱壳,就有两个最为关键的要素:

  1. 内存中dex的起始地址和大小,只有拿到这两个要素,才能够成功dump下内存中的dex
  2. 脱壳时机,只有正确的脱壳时机,才能够dump下明文状态的dex。否则,时机不对,即使是正确的起始地址和大小,dump下来的也可能只是密文。

脱壳点的选择

脱壳点其实有很多:

  1. 直接查找法,就是指以DexFile为关键字,在庞大的源码库中检索定位可能的脱壳点。
  2. 间接法,就是指以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运行效率。

qrcode_for_gh_42f185f6bcde_258 拷贝.jpg