Android安全-fart脱壳优化并迁移到Android11

前言

FART应该是近些年来比较优秀的脱壳方案了,众所周知FART是基于主动调用链的脱壳方法,原版的FART基于安卓8.1.0版本编译,为配合公司沙箱(安卓11)使用,这里尝试将fart迁移到Android 11。

FART在使用过程中存在一部分问题,这里做一下简单优化。

另外市面上针对fart的检测也非常多,所以修改一下特征,尝试过检测。

由于本人写文章的水平实在是很差,为此没少挨领导的批评,如果文章中有错误之处,感谢各位朋友指正。

一、fart脱壳原理

脱壳原理相信很多人已经比较熟悉了,大致流程图如下:

-1

主要的脱壳步骤是:

 第一步:获得类加载器,遍历dexElements获取DexFile、mCookie
 第二步:通过DexFile、mCookie获取该DexFile中所有classname
 第三步:通过类加载器loadclass所有classname,获取class对象
 第四步:遍历class对象,获取所有方法Method结构
 第五步: 转换method为ArtMethod
 第六步: 主动调用ArtMethod Invoke方法
 第七步: 通过ArtMethod 获取 DexFile* 和 CodeItem*
 第八步: 通过DexFile* 获取内存整体dex基地址和长度并dump
 第九步: 通过DexFile* 获取内存整体dex基地址和长度并dump

二、移植到安卓11

1、安卓9版本之后codeitem结构体发生了变化,需要稍加修改。

8.1.0的源码中,codeitem结构体在:/art/runtime/dex_file.h:

-2

Android11源码中codeitem结构体在: /art/libdexfile/dex/dex_file_structs.h:

-3

迁移到Android11,需修改FART 获取 codeitem长度部分代码:

-4

2、为防止高版本系统编译优化,将dex2oat编译器过滤选项修改为verify。

修改源码中的/build/make/core/main.mk:

-5

三、优化与拓展

一、去FART特征

因FART使用比较广泛,目前部分加固厂商对FART特征进行检测,导致FART无法使用。

现象:应用无法在集成FART代码的系统上运行,应用卡在空白页

FART检测方式主要是:

1)检索libart.so符号
   FART通过修改源码方式实现,添加了部分函数,其中函数 myfartInvoke 为 FART特有函数符号,爱加密通过 dlopen libart.so ,dlsym myfar          tInvoke 来判断libart.so是否集成了FART模块
2)配置文件检查
  access /data/fart 是否存在这个配置文件来判断是否是FART系统。

去特征的基本思路就是:

1、通过修改函数名,去除FART符号特征(对FART所有函数名重命名)。

2、通过修改配置文件名和路径,去除FART配置文件特征。

二、合并mCookie

解决的问题:腾讯的抽取壳在部分系统对mCookie进行自定义保存,导致脱壳不全。

出现这样的问题的原因是:

执行主动调用的前提是获取到加载的所有类,以及类对应的所有方法。
fart通过遍历dexElements获取DexFile,当mCookie被隐藏时将无法搜索到对应dexfile结构,腾讯抽取壳在Android版本小于10时,DexFile 、mCookie不加入原PathList dexElements,在自定义类加载器维护PathList。

壳处理的核心逻辑是:

在加载dex时,保存DexFile和mCookie,同时会hook(重新动态注册) defineClassNative,指向自定义函数,当findclass时,进入自定义defineClassNative函数,先调用原defineClassNative函数看是否能返回class,不能则用保存的 cookie和DexFile 替换defineClassNative的最后两个参数 cookie和DexFile再defineClassNative一次,这样达到不加入BaseDexLoader pathList 的 dexElements中也能加载class。

这里考虑获取所有的DexFile,然后合并mCookie去做处理。首先在defineClassNative插桩,收集所有DexFIle指针,将其转化为mCookie1(long[]),返回给java层,合并反射获得的mCookie2为mCookie,然后使用原有逻辑获取类列表,遍历加载类的所有函数,然后dump。

static jclass DexFile_defineClassNative(JNIEnv* env,jclass,jstring javaName,jobject javaLoader,
                                        jobject cookie,
                                        jobject dexFile) {
  
......
 
  for (auto& dex_file : dex_files) {
    //add
    if(canInsert){
      const uint8_t* begin_=dex_file->Begin();  // Start of data.
      size_t size_=dex_file->Size();  // Length of data.
      LOG(ERROR) << "defineClassNative DexBaseAddr:" << (void*)begin_ << ", size:" << size_ << ", " <<class_name.c_str();
      mydex_files.insert(std::pair<const art::DexFile*,size_t>(dex_file,size_));
    }
    //addend

调用返回mCookie到java层:

static jobject DexFile_dumpWDexFile(JNIEnv* env, jclass) {
 ......
  canInsert = false;
  std::vector<std::unique_ptr<const DexFile>> xdex_files;
  for(auto it=mydex_files.begin(); it != mydex_files.end(); it++){
    const DexFile*  mDexfile = it->first;
    const uint8_t* begin_=mDexfile->Begin();
    size_t size_=it->second;
    LOG(ERROR) << "map DexBaseAddr:" << (void*)begin_ << ", size:" << size_;
    std::unique_ptr<const DexFile> dex_file(mDexfile);
    xdex_files.push_back(std::move(dex_file));
  }
  return ConvertDexFilesToJavaArray(env, nullptr, xdex_files);
......
}

java层合并mCookie后沿用原loadClassAndWInvoke逻辑:

......

    Object mycookies= (Object)dumpDexFile_method.invoke(null);
    Log.e("ActivityThread", "wltxwithDefineClass cookies = " + Arrays.toString((long[])mycookies));

    long[] cookies = (long[])mycookies;
    for(long subCookie:cookies){
        cookieHashSet.add(Long.valueOf(subCookie));
    }

    Set<Long> cookieSet = new TreeSet(cookieHashSet);
    Long[] Lcookie = cookieSet.toArray(new Long[]{});
    long[] mergeCookies = new long[Lcookie.length];
    for(int i=0; i<Lcookie.length; i++){
        mergeCookies[i] = Lcookie[i].longValue();
    }
    Log.e("ActivityThread", "mergeCookies cookies = " + Arrays.toString(mergeCookies));
......

三、静态块<client>还原

在类的加载流程中,需要调用<clinit>函数对静态变量,静态代码块执行初始化工作,fart中java反射获取不到<clinit>,初始化类加载方式会运行抛异常导致修复不全。

解决方案一:在加载class时进行初始化

public static void loadClassAndWInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) {
    .....
                resultclass = Class.forName(eachclassname, true, appClassloader); //invoke <clinit>
                //resultclass = appClassloader.loadClass(eachclassname); //old
                //Log.e("ActivityThread", "26");
            } catch (Exception e) {
                Log.e("ActivityThread", "loadClassAndInvoke exception: " + eachclassname);
                e.printStackTrace();
                return;
            }catch (Error e) { //add skip error
                Log.e("ActivityThread", "loadClassAndInvoke error: " + eachclassname);
                e.printStackTrace();
                return;
            }
    ....

interpreter过滤:

static inline JValue Execute(
    Thread* self,
    const CodeItemDataAccessor& accessor,
    ShadowFrame& shadow_frame,
    JValue result_register,
    bool stay_in_interpreter = false) REQUIRES_SHARED(Locks::mutator_lock_) {
 
     //add
    if(strstr(shadow_frame.GetMethod()->PrettyMethod().c_str(),"<clinit>")){
      dumpWArtMethod(shadow_frame.GetMethod());
    }

此方案的问题:初始化会导致应用非正常方式执行<clinit>逻辑,导致运行时异常,如空指针,导致应用进程退出无法完成脱壳,故弃用。

解决方案二:获取每个类的Class*,使用FindClassMethod函数获取该类 <clinit>的ArtMethod*

核心逻辑:在获取类的第一个函数的ARTMethod*时获取 Class* ,然后调用class->FindClassMethod(“<clinit>”, “()V”, kRuntimePointerSize);获得<clinit>的ArtMethod指针,从而获取<clinit>的CodeItem

public static void loadClassAndWInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) {
    Class resultclass = null;
    int methodidx = 0;

    try {
        if(eachclassname != null && eachclassname.startsWith("androidx.")){//skip androidx
            return;
        }
        Log.e("ActivityThread", "loadClassAndInvoke: " + eachclassname);
        //Log.e("ActivityThread", "25");
        //resultclass = Class.forName(eachclassname, true, appClassloader); //invoke <clinit>
        resultclass = appClassloader.loadClass(eachclassname);
        //Log.e("ActivityThread", "26");
    } catch (Exception e) {
        Log.e("ActivityThread", "loadClassAndInvoke exception: " + eachclassname);
        e.printStackTrace();
        return;
    }catch (Error e) { //add skip error
        Log.e("ActivityThread", "loadClassAndInvoke error: " + eachclassname);
        e.printStackTrace();
        return;
    }
....

获取Class* ,然后获取<clinit>数据:

extern "C" void mywltxInvoke(ArtMethod* artmethod,int methodidx)  REQUIRES_SHARED(Locks::mutator_lock_) {
    .....
      dumpWArtMethod(artmethod);
      if(methodidx == 0){
        mirror::Class* declaring_class = artmethod->GetDeclaringClass();
        ArtMethod* clinit = declaring_class->FindClassMethod("<clinit>", "()V", kRuntimePointerSize);
        if(clinit != nullptr){
          dumpWArtMethod(clinit);
        }else{
          LOG(ERROR) << "not found clinit";
        }
      }
    }
    //addend

四、过滤cdex、vdex

过滤cdex 、vdex结构数据,不dump

....
    const DexFile* dex_file = artmethod->GetDexFile();
    const uint8_t* begin_=dex_file->Begin();  // Start of data.
    size_t size_=dex_file->Size();  // Length of data.
    memcpy(magic,begin_,4);
    if(strcmp(magic,"cdex")!=0 && strcmp(magic,"vdex")!=0){
       
        memset(dexfilepath,0,100);
        int size_int_=(int)size_;
...

优化后的整体流程图如下:

-6

四、编译使用

涉及修改源码路径如下:

art/dex2oat/dex2oat.cc 
art/runtime/art_method.cc 
art/runtime/interpreter/interpreter.cc 
art/runtime/native/dalvik_system_DexFile.cc 
art/runtime/native/java_lang_reflect_Method.cc 
art/runtime/oat_file_assistant.cc 
frameworks/base/core/java/android/app/ActivityThread.java 
frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java 
frameworks/base/services/core/java/com/android/server/pm/PackageDexOptimizer.java 
frameworks/native/cmds/installd/dexopt.cpp 
frameworks/native//cmds/installd/otapreopt.cpp 
libcore/dalvik/src/main/java/dalvik/system/DexFile.java

第一步、编译 (替换android11对应文件编译)。

    1)export LC_ALL=C
    
    2)export WITH_DEXPREOPT=false    # 重要,必须关闭dex优化,才能迫使其走解析执行流程,保证脱壳功能除发。
    
    3)source build/envsetup.sh
    
    4)lunch
    
    5)make -j8

若编译时出现该错误: error: : DEXPREOPT must be enabled for user and userdebug builds build/make/core/dex_preopt.mk:55: error: done.

解决方法:修改build/make/core/dex_preopt.mk

 ifneq (true,$(WITH_DEXPREOPT))=> ifeq (true,$(WITH_DEXPREOPT))

第二步、进入到生成的img目录,刷机。

第三步、进入手机 (针对抽取壳需要这一步设置,默认整体dump)。

echo “要脱壳的包名” > /data/local/tmp/wltx

第四步、打开应用,等待脱壳完成。

如果是抽取壳,脱壳后的目录会存在bin文件,需要用dexfixer.jar修复重组。