FART:ART环境下基于主动调用的自动化脱壳方案

这是我通读FART的笔记,将FART的原理用自己的理解记录下来的一篇文章。

问题一:

为了解决后续应用在加载执行解密后的dex文件中的Class和Method的问题,接下来就是通过利用java的反射修复一系列的变量。其中最为重要的一个变量就是应用运行中的Classloader,只有Classloader被修正后,应用才能够正常的加载并调用dex中的类和方法,否则的话由于Classloader的双亲委派机制,最终会报ClassNotFound异常,应用崩溃退出,这是加固厂商不愿意看到的。

为什么会有解密后的dex文件中的Class和Method问题。会有什么问题?为什么会有这样的问题?

我的理解是:

因为dex文件被加密了,而后需要进行解密。使用我们自定义的classloader进行读取解密后的dex文件,但是本来没有加壳的dex文件中的classloader被壳加密后再解密需要进行修复,只有这个应用中运行中classloader被修复后,才能正确运行原本dex里的Method和类,否则的话由于Classloader的双亲委派机制,最终会爆出ClassNotFound异常,应用崩溃退出。

由此可见Classloader是一个至关重要的变量,所有的应用中加载的dex文件最终都在应用的Classloader中。

因此,只要获取到加固应用最终通过反射设置后的Classloader,我们就可以通过一系列反射最终获取到当前应用所加载的解密后的内存中的Dex文件。

总结就是:

首先要知道一个流程,就是加壳在什么时候什么地方加,这里贴一个图。

attachBaseContext、onCreate位置加壳。

img

加固后要对dex文件进行解密,解密至关重要的是修正Classloader,然后通过获取classloader利用反射获取到当前应用所加载的解密后的在内存里的dex文件。

知识点 dex加密和hook

随着加壳技术的发展,为了对抗dex整体加固更易于内存dump来得到原始dex的问题,各加固厂商又结合hook技术,通过hook dex文件中类和方法加载执行过程中的关键流程,来实现在函数执行前才进行解密操作的指令抽取的解决方案。此时,就算是对内存中的dex整体进行了dump,但是由于其方法的最为重要的函数体中的指令被加密,导致无法对相关的函数进行脱壳。

就是说,不再仅仅把整体的dex整体加固,因为会被dump出内存中整块的dex文件。然后利用hook技术,把内存中的重要函数体进行加密,然后函数被调用的时候,再进行解密。这样,即使把整块dex文件dump出来,重要函数还是不可见。

知识点二 Fupk3

Fupk3可以针对知识点一的问题。

由此,Fupk3诞生了,该脱壳工具通过欺骗壳而主动调用dex中的各个函数,完成调用流程,让壳主动解密对应method的指令区域,从而完成对指令抽取型壳的脱壳。

原理很明显,我不再是等你运行到重要函数的时候才要你解密,而是在未运行之前,就骗壳主动调用dex中的各个函数,这样壳就帮加密内容解密出来了。

详细原理:

大概是说,Android是开源的,通过直接修改Android源码,把运行时的所有dex数据dump出来,不就可以实现一个通用的脱壳机了吗。

https://bbs.pediy.com/thread-246117.htm,真得好好拜读一下!

现有ART环境下自动化脱壳工具及优缺点

我几乎都没用过,但是提前学习下,了解它们的适用范围、原理以及优缺点,为以后使用打下基础吧。

dexhunter包含了dalvik和art环境,这两种是说Andriod的虚拟机版本。art环境较dalvik做了较大改变。

但是dexhunter树大招风。有对抗dexhunter的加固,比如:

  • 添加无效类,这些类的初始化函数加入强制退出的代码,被调用就会强制退出。
  • 检测dexhunter配置文件。

ART还有基于dex2oat编译生成oat过程的内存dex的dump技术。

也是整体dump,如果是hook+抽取加密也是没有办法。有的还会对dex的流程进行hook,这些dex就不会走dex2oat流程。

基于dex加载过程中内存中DexFile结构体的dump,比如,ART通过hook OpenMem函数来实现在壳进行加载DexFile时对内存中的dex的dump的脱壳技术,早前在17年在DEF CON25黑客大会中,有人提议修改DexFile:DexFile(),以及OpenAndReadMagic()函数来实现对加壳应用的内存中的dex的dump脱壳技术。

这些都是整体dump,无法实现对指令抽取型壳的完全脱壳。

但是随着Dalvik逐渐淘汰,Android4.4以下的系统,现在的APP渐渐不支持了,这就让基于Dalvik的fupk3走向末路,但是ART方案也已经被实现和开发出来,由于某些原因并未开源。

指令抽取和vmp加固有什么区别?

一个是抽取关键代码,另一个是附加了自己的虚拟机和指令进行执行。

FART脱壳原理以及实现

  • 内存中的DexFile结构体完整dex的dump
  • 主动调用类中的每一个方法,并实现对应Codeltem的dump
  • 通过主动调用dump下来的方法的CodeItem进行dex中被抽取方法的修复
内存中的DexFile结构体完整dex的dump

不是通过修改DexFile:DexFile(),这种做法还是有办法对抗的,比如壳自己实现一套Dex文件的内存加载机制从而绕过。

这里是通过在合适的时机点获取到应用解密后的dex文件最终依附的Classloader,进行通过java的反射机制最终获取到对应的DexFile结构体,并完成dex的dump。

思路:

1、获取Classloader时机点的选择。

APP中Application类中的attachBaseContext和onCreate函数是app中最先执行的方法,壳也是通过替换APP的Application类并自己实现这两个函数,并在这两个函数中实现dex的解密加载,hook系统中的Class和method加载执行流程中的关键函数,最后通过反射完成关键变量如最终的Classloader,Application等的替换从而完成执行权的交付。

我们选择在任意一个在Application的onCreate函数执行之后的函数,一个应用最终都要由一个个的Activity来展示应用的界面并和用户完成交互,所以可以选择ActivityThread中的performLaunchActivity函数作为时机,来获取最终的应用的Classloader。选择该函数还有一个好处在于该函数和应用最终的application同在ActivityThread类中,可以很方便获取到该类的成员。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent){
  ......
  Activity activity = null;
  try{
    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    //下面通过application的getClassLoader()获取最终的Classloader,并开启线程,在新线程中完成内存中的dex的dump以及主动调用过程,由于该过程相对耗时,为了防止应用出现ANR,从而开启新线程,在新线程中进行,主要的工作都在getDexFilesByClassLoader_23
    //addstart
    packagename = r.packageInfo.getPackageName();
    if(mInitalApplication != null){
      fina java.lang.ClassLoader finalcl = mInitialApplication.getClassLoader();
      new Thread(new Runnable(){
        @Override
        public void run() {
          getDexFilesByClassLoader_23(finalcl);
        }
      }).start()
    }
  }
}

getDexFilesByClassLoader_23()函数的主要流程就是通过一系列的反射,最终获取到当前Classloader中的mCookie,即Native层中的DexFile。

Classloader中的mCookie和Native层的DexFile是什么关系?

我感觉Fupk3也提到了这个变量。

为了在C/C++中在文件完成对dex的dump操作,这里我们在framework层的DexFile类中添加两个Native函数供调用:

libcore/dalvik/src/main/java/dalvik/system/DexFile.java中

private static native void dumpDexFile(String dexfilepath, Object cookie);
private static native void dumpMethodCode(String eachclassname, String methodname, Object cookie, Object method);

这两个函数分别用于完成内存中dex的dump以及构造主动调用链,完成方法体的dump,在对应的C++文件中添加这两个Native函数的实现并完成注册:

Art/runtime/native/dalvik_system_DexFile.cc文件中

static void DexFile_dumpDexFile(JNIEnv* env, jclass, jstring filepath jobject cookie){
  std::unique_ptr<std:vector<const DexFile*>> dex_files = ConvertJavaArrayToNative(env, cookie);
  if (dex_files.get() == nullptr){
    DCHECK(env->ExceptionCheck());
    return;
  }
  int dexnum = 0;
  char dexfilepath[1000];
  for (auto&dex_file : *dex_files){
    const unit8_t* begin_ = dex_file->getbegin(); //Start of data.
    size_t size = dex_file->getsize(); //length of data;
    int dexfilesize = (int)size_;
    const char *filepathcstr = env->GetStringUTFChars(filepath, nullptr);
    memset(dexfilepath,0,1000);
    sprintf(dexfilepath, "%s_%d_%d", filepathcstr, dexfilesize, dexnum);
    dexnum++;
    //对抗抽取指令型加固壳,由于这类壳会通过hook libc中的关键文件读写函数来防止dump,这里直接使用系统调用完成dex文件的dump
    int fp = open(dexfilepath, O_CREATE|O_APPEND|O_RDWR, 0666);
    write(fp,(void*)begin_,size_);
    fsync(fp);
    close(fp);
  }
}

上面实现了对Classloader中加载的dex的dump,那么主动调用类中函数,如果这个函数被加密了,如何dump呢?

类函数的主动调用设计实现

可以从JNI提供的相关函数源码来对类函数主动调用链的构造。

JNI提供了一系列java层函数与Native层函数交互的接口。当需要在Native层中的c/c++函数中调用位于java层的函数时,需要先获取到该函数的jmethodid然后再通过注入jni中提供的call开头的一系列函数来完成对java层中函数的调用。我们以jni的CallObjectMethod函数为例,进行分析。下面开始源码分析:

static jobject CallObjectMethod(JNIEnv* env, jobject obj, jmethodID mid, ...){
  va_list ap;
  va_start(ap, mid);
  CHECK_NON_NULL_ARGUMENT(obj);
  CHECK_NON_NULL_ARGUMENT(mid);
  ScopeObjectAccess soa(env);
  JValue result(InvokeVirtualOrInterfaceWithVarArgs(soa, obj, mid, ap));
  va_end(ap);
  return soa.AddLocalReference<jobject>(result.GetL());
}

该函数中通过InvokeVirtualOrInterfaceWithVarArgs(soa, obj, mid, ap)函数来完成调用,下面看该函数内容:

该函数首先对jmethodid进行了转换,转换成ArtMethod对象指针,进而通过函数InvokeWithArgArray完成调用,下面再看InvokeWithArgArray函数内容

JValue InvokeVirtualOrInterfaceWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,jobject obj, jmethodID mid, va_list args) {
  ......
  ArtMethod* method = FindVirtualMethod(receiver, soa.DecodeMethod(mid));
  ......
  InvokeWithArgArray(soa, method, &arg_array, &result, shorty);
  .....
}

InvokeWithArgArray函数:

static void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,ArtMethod* method, ArgArray* arg_array, JValue* result,
                               const char* shorty)
    SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
  uint32_t* args = arg_array->GetArray();
  if (UNLIKELY(soa.Env()->check_jni)) {
    CheckMethodArguments(soa.Vm(), method->GetInterfaceMethodIfProxy(sizeof(void*)), args);
  }
  method->Invoke(soa.Self(), args, arg_array->GetNumBytes(), result, shorty);
}

最终是Invoke完成调用Java层函数的调用。于是,我们可以构造出自己的invoke函数,在该函数中再调用ArtMethod的Invoke方法从而完成主动调用,我们在传参的时候,设定一个标志位,在ArtMethod的Invoke函数中进行判断发现是主动调用时就进行方法体的dump并直接返回,从而完成对壳的欺骗。

阅读下面这块代码

libcore/dalvik/src/main/java/dalvik/system/DexFile.java

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; 
  }

对Java层传来的参数直接转成Native层的ArtMethod对象,接下来就是myfartInvoke

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");
}

void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,const char* shorty) {
                             // unsigned int tempresult=(unsigned int)self;
    //这里的self=nullptr是表示由我们主动调用的标志位
    if (self== nullptr) {
        LOG(INFO) <<"art_method.cc::Invoke is invoked by myfartinvoke";
        dumpArtMethod(this);
        return;
        } 
......

接下来的dump函数,和fupk3一样直接采用dexhunter里的。

到这里,就完成了内存中DexFile结构体中的dex的整体dump以及对抗抽取型指令的dump,下面就是修复被抽取的函数部分。

抽取类函数的修复

为什么要进行修复?

就比如一个单身女孩子,谈了恋爱有了男朋友,但是后来因为种种原因分手了,她又恢复了单身的状态,那么她在没谈和分手后肯定会有些方面会发生改变了,对吧。

所以要修复。

壳在完成对内存中加载的dex解密后,该dex索引去即stringid, typeid, methodid, classdef和对应的data区中string列表并没有加密。而对于classdef中类函数的CodeItem部分可能被加密存储或者直接指向内存中另一块区域。前面我们知道了,这里我们只需要使用dump下来的method的代码块来解析对应的被抽取方法即可。

实验验证

等我的pixel的手机到,然后整上一整。

后续…

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值