Android 一二三代壳加固原理分析

简介

  • 所有的加固代码 都需要通过Classloader加载然后才可以执行

classloader介绍

  • 双亲委派机制

    双亲委派模式的工作原理的是;如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
    如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
    如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
    即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这个就是双亲委派。

  • Android中的classloader

    ClassLoader为抽象类;
    BootClassLoader预加载常用类,单例模式。
    BaseDexClassLoader是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父类,类加载的主要逻辑都是在BaseDexClassLoader完成的。
    SecureClassLoader继承了抽象类ClassLoader,拓展了ClassLoader类加入了权限方面的功能,加强了安全性,其子类URLClassLoader是用URL路径从jar文件中加载类和资源。
    其中重点关注的是PathClassLoader和DexClassLoader。
    PathClassLoader是Android默认使用的类加载器,一个apk中的Activity等类便是在其中加载。
    DexClassLoader可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现插件化、热修复以及dex加壳的重点。
    Android8.0新引入InMemoryDexClassLoader,从名字便可看出是用于直接从内存中加载dex。

596742716226444.png

APP 的启动流程

534413116246610.png

APP类加载过程

  • BootClassLoader加载系统核心库
  • PathClassLoader加载APP自身dex
  • 进入APP自身组件开始执行,调用声明Application的attachBaseContext,onCreate
    • AppThread中有LoadedApk对象。
    • AppThread handledBindApplication(应该是AMS 回调的ApplicationThread)中,第一次进入到App自身的代码中去。
    • Application attach -> oncreate最先被执行,如果加壳的话,此时的classloader只有壳的代码 没有自身代码
    • 如果不处理classloader的话,只有壳代码,是无法启动注册的Activity Service等的。
      * 所以壳代码需要在这里加载真正的代码

组件生命周期的处理

DexClassLoader加载的类是没有组件生命周期的,也就是说即使DexClassLoader通过对APK的动态加载完成了对组件类的加载,当系统启动该组件时,依然会出现加载类失败的异常。
需要替换系统组件的classloader才可以。加固厂商必然饶不过去
两种解决方案:
1、替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoader的parent为系统组件类加载器;

//classloader 已经加载了 自身代码 并且parent是原始的classloader
public void replaceClassloader(ClassLoader classloader){
        try {
            Class ActivityThreadClazz=classloader.loadClass("android.app.ActivityThread");
            Method currentActivityThreadMethod= ActivityThreadClazz.getDeclaredMethod("currentActivityThread");
            currentActivityThreadMethod.setAccessible(true);
            Object activityThreadObj=currentActivityThreadMethod.invoke(null);
            //final ArrayMap> mPackages = new ArrayMap<>();
            Field mPackagesField=ActivityThreadClazz.getDeclaredField("mPackages");
            mPackagesField.setAccessible(true);
            ArrayMap mPackagesObj= (ArrayMap) mPackagesField.get(activityThreadObj);
            WeakReference wr= (WeakReference) mPackagesObj.get(this.getPackageName());
            Object loadedApkObj=wr.get();

            Class LoadedApkClazz=classloader.loadClass("android.app.LoadedApk");
            //private ClassLoader mClassLoader;
            Field mClassLoaderField=LoadedApkClazz.getDeclaredField("mClassLoader");
            mClassLoaderField.setAccessible(true);
            mClassLoaderField.set(loadedApkObj,classloader);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2、打破原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入我们自己的DexClassLoader即可;

public void startTestActivitySecondMethod(Context context,String dexfilepath){

        File optfile=context.getDir("opt_dex",0);
        File libfile=context.getDir("lib_path",0);
        ClassLoader pathClassloader=MainActivity.class.getClassLoader();
        ClassLoader bootClassloader=MainActivity.class.getClassLoader().getParent();
        DexClassLoader dexClassLoader=new DexClassLoader(dexfilepath,optfile.getAbsolutePath(),libfile.getAbsolutePath(),bootClassloader);
        try {
            Field parentField=ClassLoader.class.getDeclaredField("parent");
            parentField.setAccessible(true);
            parentField.set(pathClassloader,dexClassLoader);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

dex整体加密型加固

  • 将Dex整体加密,在需要的时候解密,通过Classloader加入内存中。

关键代码

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //com.example.myapplication.TestClass.testFunc
        Context appContext=this.getApplicationContext();
        //testDexClassLoader(appContext,"/sdcard/3.dex");
        //startTestActivity(this,"/sdcard/4.dex");
        startTestActivitySecondMethod(this,"/sdcard/6.dex");
    }

    //替换LoadedApk中的mClassLoader 加载自身代码
    public void replaceClassloader(ClassLoader classloader){
        try {
            Class ActivityThreadClazz=classloader.loadClass("android.app.ActivityThread");
            Method currentActivityThreadMethod= ActivityThreadClazz.getDeclaredMethod("currentActivityThread");
            currentActivityThreadMethod.setAccessible(true);
            Object activityThreadObj=currentActivityThreadMethod.invoke(null);
            //final ArrayMap> mPackages = new ArrayMap<>();
            Field mPackagesField=ActivityThreadClazz.getDeclaredField("mPackages");
            mPackagesField.setAccessible(true);
            ArrayMap mPackagesObj= (ArrayMap) mPackagesField.get(activityThreadObj);
            WeakReference wr= (WeakReference) mPackagesObj.get(this.getPackageName());
            Object loadedApkObj=wr.get();

            Class LoadedApkClazz=classloader.loadClass("android.app.LoadedApk");
            //private ClassLoader mClassLoader;
            Field mClassLoaderField=LoadedApkClazz.getDeclaredField("mClassLoader");
            mClassLoaderField.setAccessible(true);
            mClassLoaderField.set(loadedApkObj,classloader);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }


    }

    public void startTestActivityFirstMethod(Context context,String dexfilepath){

        File optfile=context.getDir("opt_dex",0);
        File libfile=context.getDir("lib_path",0);
        ClassLoader parentClassloader=MainActivity.class.getClassLoader();
        ClassLoader tmpClassLoader=context.getClassLoader();
        DexClassLoader dexClassLoader=new DexClassLoader(dexfilepath,optfile.getAbsolutePath(),libfile.getAbsolutePath(),MainActivity.class.getClassLoader());

        replaceClassloader(dexClassLoader);

        Class clazz=null;
        try {
            clazz = dexClassLoader.loadClass("com.example.myapplication.TestActivity");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        context.startActivity(new Intent(context,clazz));
    }
    public void startTestActivitySecondMethod(Context context,String dexfilepath){

        File optfile=context.getDir("opt_dex",0);
        File libfile=context.getDir("lib_path",0);
        ClassLoader pathClassloader=MainActivity.class.getClassLoader();
        ClassLoader bootClassloader=MainActivity.class.getClassLoader().getParent();
        //加载dex 这里直接加载了未加密的,可以解密后 再加载,然后删除。也可以替换为memoryclassloader加载 ,实现不落地加载
        DexClassLoader dexClassLoader=new DexClassLoader(dexfilepath,optfile.getAbsolutePath(),libfile.getAbsolutePath(),bootClassloader);
        try {
            //插入我们自己的dexClassLoader 加入生命周期的处理
            Field parentField=ClassLoader.class.getDeclaredField("parent");
            parentField.setAccessible(true);
            parentField.set(pathClassloader,dexClassLoader);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
/*
* this:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.xyh.loaddex-8_fCxispeBuExjw1ryrRZg==/base.apk"],nativeLibraryDirectories=[/data/app/com.xyh.loaddex-8_fCxispeBuExjw1ryrRZg==/lib/arm64, /system/lib64, /vendor/lib64]]]--parent:dalvik.system.DexClassLoader[DexPathList[[dex file "/sdcard/6.dex"],nativeLibraryDirectories=[/data/user/0/com.xyh.loaddex/app_lib_path, /system/lib64, /vendor/lib64]]]
this:dalvik.system.DexClassLoader[DexPathList[[dex file "/sdcard/6.dex"],nativeLibraryDirectories=[/data/user/0/com.xyh.loaddex/app_lib_path, /system/lib64, /vendor/lib64]]]--parent:java.lang.BootClassLoader@fd4323d
root:java.lang.BootClassLoader@fd4323d*/
        ClassLoader tmpClassloader=pathClassloader;
        ClassLoader parentClassloader=pathClassloader.getParent();
        while(parentClassloader!=null){
            Log.i("xyh","this:"+tmpClassloader+"--parent:"+parentClassloader);
            tmpClassloader=parentClassloader;
            parentClassloader=parentClassloader.getParent();
        }
        Log.i("xyh","root:"+tmpClassloader);
        Class clazz=null;
        try {
            clazz = dexClassLoader.loadClass("com.example.myapplication.TestActivity");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        context.startActivity(new Intent(context,clazz));
    }
}

加固类型-函数抽取

壳与脱壳之二代壳函数抽取/#源码分析

Dalvik指令抽取原理介绍

Android中实现「类方法指令抽取方式」加固方案原理解析
Android免Root权限通过Hook系统函数修改程序运行时内存指令逻辑

  • 在函数粒度上保护代码
  • 将函数指令全部替换为Nop或无效指令,在执行时还原代码。
  • 主要原理是将原指令置为nop,然后hook,dexFindClass,找到原始的方法指令指针,DexCode->insns,替换insns。
207665618235835.png

Art 抽取壳介绍

Art下实现难点:dex2oat编译流程,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运行效率。
ART模式下抽取壳 要么阻止dex2oat 要么在dex2oat之前保证加载的dex是一个完整的dex
如果选择第一种方案 , 在 dex2oat 之前进行恢复 , 这没有任何意义 , dex2oat 编译后 , 生成的 oat 文件是完整的 , 此时 可以 完整的将 oat 文件 dump 到 SD 卡中 , 基本等于没有加固 , 还是一个一代壳 ;
因此 , 大部分加固厂商 , 选择 禁用 dex2oat 机制 ; 这样处于安全考虑 , 牺牲了应用的运行效率 ;

145113619231589.png
  • classloader加载dex的时候 会进行dex2oat的优化,将dex的解释执行,变成直接执行,提升运行效率。
  • 7.0之后会根据函数使用频率进行dex2oat的优化,并不会直接全部dex2oat

https://source.android.google.cn/devices/tech/dalvik/configure
ART 使用预先 (AOT) 编译,并且从 Android 7.0(代号 Nougat,简称 N)开始结合使用 AOT、即时 (JIT) 编译和配置文件引导型编译。所有这些编译模式的组合均可配置,我们将在本部分中对此进行介绍。例如,Pixel 设备配置了以下编译流程:
7.0之后最初安装应用时不进行任何 AOT 编译。应用前几次运行时,系统会对其进行解译,并对经常执行的方法进行 JIT 编译。
当设备闲置和充电时,编译守护程序会运行,以便根据在应用前几次运行期间生成的配置文件对常用代码进行 AOT 编译。
下一次重新启动应用时将会使用配置文件引导型代码,并避免在运行时对已经过编译的方法进行 JIT 编译。在应用后续运行期间经过 JIT 编译的方法将会添加到配置文件中,然后编译守护程序将会对这些方法进行 AOT 编译。
ART 的运作方式 在 Android O 版本中,将会生成以下文件:
.vdex:其中包含 APK 的未压缩 DEX 代码,以及一些旨在加快验证速度的元数据。
使用010editer就可以找到dex
.odex:其中包含 APK 中已经过 AOT 编译的方法代码。
oatdump 可以反编译odex文件,可以dump出函数的smali指令
.art (optional):其中包含 APK 中列出的某些字符串和类的 ART 内部表示,用于加快应用启动速度。
全部进行oat优化的话,非常耗时,所以7.0之后选取了保守的优化策略
odex 优化后,oatdump还是可以dump出原始的smali指令,只是优化后的函数 在CODE段,会有优化后的二进制汇编指令,在执行的时候会直接进入quick模式,执行汇编指令,加快运行速度。具有CODE段后,将不再进入解释执行模式。

  • art抽取壳必须在dex2oat之前还原,否则dex2oat之后,就无法还原。要么阻止dex2oat 要么提前还原
  • 首先要干掉Dex2oat的过程。可以参考github TurboDex(快速加载dex 阻止dex2oat) dex2oat 非常耗时;不进行oat的话 会影响dex的运行效率。
  • 阻止了dex2oat后 所有代码都是解释执行了。那么必然会去内存中寻找ArtMethod的code_item去执行
  • 那么在art类加载的时候还原code_item就可以正常去执行了
  • 剩下的和dalvik基本一致
    • 1、先通过010edite将一个函数全部抽取为nop
    • 2、hook art loadmethod的过程
    • 3、将原始的指令还原到artmethod中

ART的类加载执行流程

强烈建议阅读此文章
ART 在 Android 安全攻防中的应用

关键代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
//import c header
extern "C" {
#include "hook/dlfcn/dlfcn_compat.h"
#include "hook/include/inlineHook.h"
}
typedef unsigned char byte;
#define TAG "SecondShell"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
struct DexFile {
    // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
    // The class we are a part of.
    uint32_t declaring_class_;
    // Access flags; low 16 bits are defined by spec.
    void *begin;
    /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
    // Offset to the CodeItem.
    uint32_t size;
};
struct ArtMethod {
    // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
    // The class we are a part of.
    uint32_t declaring_class_;
    // Access flags; low 16 bits are defined by spec.
    uint32_t access_flags_;
    /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
    // Offset to the CodeItem.
    uint32_t dex_code_item_offset_;
    // Index into method_ids of the dex file associated with this method.
    uint32_t dex_method_index_;
};

void* *(*oriexecve)(const char *__file, char *const *__argv, char *const *__envp);

void* *myexecve(const char *__file, char *const *__argv, char *const *__envp) {
    LOGD("process:%d,enter execve:%s", getpid(), __file);
    if (strstr(__file, "dex2oat")) {
        return NULL;
    } else {
        return oriexecve(__file, __argv, __envp);
    }


}

//void ClassLinker::LoadMethod(Thread* self, const DexFile& dex_file, const ClassDataItemIterator& it,Handle klass, ArtMethod* dst)
void *(*oriloadmethod)(void *, void *, void *, void *, void *);

void *myloadmethod(void *a, void *b, void *c, void *d, void *e) {
    LOGD("process:%d,before run loadmethod:", getpid());
    struct ArtMethod *artmethod = (struct ArtMethod *) e;
    struct DexFile *dexfile = (struct DexFile *) b;
    LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d", getpid(), dexfile->begin,
         dexfile->size);//0,57344
    char dexfilepath[100] = {0};
    sprintf(dexfilepath, "/sdcard/%d_%d.dex", dexfile->size, getpid());
    int fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
    if (fd > 0) {
        write(fd, dexfile->begin, dexfile->size);
        close(fd);
    }

    void *result = oriloadmethod(a, b, c, d, e);
    LOGD("process:%d,enter loadmethod:code_offset:%d,idx:%d", getpid(),
         artmethod->dex_code_item_offset_, artmethod->dex_method_index_);

    byte *code_item_addr = static_cast(dexfile->begin) + artmethod->dex_code_item_offset_;
    LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,beforedumpcodeitem:%p", getpid(),
         dexfile->begin, dexfile->size, code_item_addr);


    if (artmethod->dex_method_index_ == 15203) {//TestClass.testFunc->methodidx
        LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,start repire method", getpid(),
             dexfile->begin, dexfile->size);
        byte *code_item_addr = (byte *) dexfile->begin + artmethod->dex_code_item_offset_;
        LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,beforedumpcodeitem:%p", getpid(),
             dexfile->begin, dexfile->size, code_item_addr);

        int result = mprotect(dexfile->begin, dexfile->size, PROT_WRITE);
        byte *code_item_start = static_cast(code_item_addr) + 16;
        LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,code_item_start:%p", getpid(),
             dexfile->begin, dexfile->size, code_item_start);
        byte inst[16] = {0x1a, 0x00, 0xed, 0x34, 0x1a, 0x01, 0x43, 0x32, 0x71, 0x20, 0x91, 0x05,
                         0x10, 0x00, 0x0e, 0x00};
        for (int i = 0; i < sizeof(inst); i++) {
            code_item_start[i] = inst[i];
        }
        //2343->i am from com.kanxue.test02.TestClass.testFunc
        code_item_start[2] = 0x43;//34ed->kanxue
        code_item_start[3] = 0x23;
        memset(dexfilepath, 0, 100);
        sprintf(dexfilepath, "/sdcard/%d_%d.dex_15203_2", dexfile->size, getpid());
        fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
        if (fd > 0) {
            write(fd, dexfile->begin, dexfile->size);
            close(fd);
        }
    }
    LOGD("process:%d,after loadmethod:code_offset:%d,idx:%d", getpid(),
         artmethod->dex_code_item_offset_, artmethod->dex_method_index_);//0,57344
    return result;

}

void hooklibc() {
    LOGD("go into hooklibc");
    //7.0 命名空间限制
    void *libc_addr = dlopen_compat("libc.so", RTLD_NOW);
    void *execve_addr = dlsym_compat(libc_addr, "execve");
    if (execve_addr != NULL) {
        if (ELE7EN_OK == registerInlineHook((uint32_t) execve_addr, (uint32_t) myexecve,
                                            (uint32_t **) &oriexecve)) {
            if (ELE7EN_OK == inlineHook((uint32_t) execve_addr)) {
                LOGD("inlineHook execve success");
            } else {
                LOGD("inlineHook execve failure");
            }
        }
    }
}

void hookART() {
    LOGD("go into hookART");
    void *libart_addr = dlopen_compat("/system/lib/libart.so", RTLD_NOW);
    if (libart_addr != NULL) {
        void *loadmethod_addr = dlsym_compat(libart_addr,
                                             "_ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE");
        if (loadmethod_addr != NULL) {
            if (ELE7EN_OK == registerInlineHook((uint32_t) loadmethod_addr, (uint32_t) myloadmethod,
                                                (uint32_t **) &oriloadmethod)) {
                if (ELE7EN_OK == inlineHook((uint32_t) loadmethod_addr)) {
                    LOGD("inlineHook loadmethod success");
                } else {
                    LOGD("inlineHook loadmethod failure");
                }
            }
        }
    }


}

extern "C" JNIEXPORT jstring JNICALL
Java_com_kanxue_secondshell_180_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "ART SecondShell";
    return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT void JNICALL
Java_com_kanxue_secondshell_180_MainActivity_SecondShell(
        JNIEnv *env,
        jobject /* this */) {
    hooklibc();//hook execve 禁止执行dex2oat
    hookART();//hook LoadMethod 修复被抽取的artmethod,并在修复前修复后dumpdex
    return;
}

加固类型-VMP,Dex2C

  • VMP 自定义解释器,解释执行自定义的指令。
  • Dex2c 在编译时,将java代码编译成c代码 增加破解难度
  • 未来加固的主要方向->VMP,Dex2C
    • VMP 会把java函数native化,原理:VMP是做虚拟机解释器
      • VMP的多个native函数 很可能是一个地址。多个native函数 都是调用的一个地方
      • 破解需要找到解释器,找到smaili的映射关系即可恢复
      • github 搜索 ADVMP 可以参考 没具体研究
    • Dex2c 开源项目 dcc,原理:对java语义分析,重新生成native函数。
      • python dcc.py dcc.apk -o dcc_out.apk 使用DCC增加保护,java函数会变成native函数。 只建议部分函数增加保护,否则会严重影响性能
      • dcc可以将APK的java代码 转化为native代码,并且可以增加ollvm的混淆
      • dcc的native注册函数 基本是一一对应
      • 基本不太好破解。。

你可能感兴趣的:(Android 一二三代壳加固原理分析)