dexshellerInMemory

Introduction: android APK 一键 DEX 加固脚本(内存加载 DEX)
More: Author   ReportBugs   
Tags:

本文目的:通过内存加载 DEX 文件技术,完成一键 DEX 加固脚本

使用说明

python sheller.py -f xxx.apk

加固原理

和我的另一个项目Native 层 DEX 一键加固脚本基本一样,只是多了一步,引入了内存加载 DEX 技术

一键加固脚本实现步骤

  1. 准备原 DEX 加密算法以及隐藏位置(壳 DEX 尾部)
        """
        1. 第一步:确定加密算法
        """
        inKey = 0xFF
        print("[*] 确定加密解密算法,异或: {}".format(str(inKey)))
  1. 生成壳 DEX。(壳 Application 动态加载原 application 中需要原 application 的 name 字段)
        """
        2. 第二步:准备好壳 App
        """
        # 反编译原 apk
        decompAPK(fp)
        # 获取 Applicaiton name 并保存到壳 App 源码中
        stSrcDexAppName = getAppName(fp)
        save_appName(stSrcDexAppName)
        # 编译出壳 DEX
        compileShellDex()
        print("[*] 壳 App 的 class 字节码文件编译为:shell.dex 完成")
  1. 修改原 APK 文件中的 AndroidManifest.xml 文件的 applicationandroid:name 字段,实现从壳 application 启动
        """
        3. 第三步:修改原 apk AndroidManifest.xml 文件中的 Application name 字段为壳的 Application name 字段
        """
        # 替换壳 Applicaiton name 到原 apk 的 AndroidManifest.xml 内
        replaceTag(fp, "cn.yongye.nativeshell.StubApp")
        print("[*] 原 apk 文件 AndroidManifest.xml 中 application 的 name 字段替换为壳 application name 字段完成")
  1. 加密原 DEX 到壳 DEX 尾部并将壳 DEX 替换到原 APK 中
        """
        4. 替换原 apk 中的 DEX 文件为壳 DEX
        """
        replaceSDexToShellDex(os.path.join(stCurrentPt, "result.apk"))
        print("[*] 壳 DEX 替换原 apk 包内的 DEX 文件完成")
  1. 添加脱壳 lib 库到原 apk 中
        """
        5. 添加脱壳 lib 库到原 apk 中
        """
        addLib("result.apk")
  1. apk 签名
        """
        6. apk 签名
        """
        signApk(os.path.join(stCurrentPt, "result.apk"), os.path.join(stCurrentPt, "demo.keystore"))
        print("[*] 签名完成,生成 apk 文件: {}".format(os.path.join(stCurrentPt, "result.apk")))

内存加载 DEX

了解内存加载 DEX 文件内存加载技术之前,我们需要了解 DEX 文件加载这一过程。

DEX 文件加载过程:即下面左图所示的 API 调用链就是加载过程。DEX 文件加载的核心是最后一层,通过 Native 层函数传入 DEX 文件从而获取一个 cookie 值

所以我们要实现内存加载 DEX的话,不仅需要自己重写这套 DEX 文件调用链,最主要的是寻找一个 Native 层 API 实现传入 DEX 文件字节流来获取 cookie 值,就是下面右图所示调用链,API 即 openMemory(4.3 ART 虚拟机以后, 4.3 及其以前 Dalvik 虚拟机用 openDexFile([byte...)API)即可实现这样的功能

内存加载 DEX

Android 8 以后 BaseDexClassLoader 类就有内存加载的能力,参考 Android 8 源码,已经加入了这样的接口,我们只需要将相关代码文件 copy 下来,将最底层的 Native 函数用 openMemory 替代即可

image-20200415234950548

自定义 MyDexClassLoader

通过 dlopen 和 dlsym 的方法找到 openMemory 函数实现核心函数即可

JNICALL extern "C"
JNIEXPORT jlong JNICALL
Java_cn_yongye_inmemoryloaddex_DexFile_createCookieWithArray(JNIEnv *env, jclass clazz,
                                                             jbyteArray buf, jint start, jint end) {
    // TODO: implement createCookieWithArray()
    void *artlib =  fake_dlopen("/system/lib/libart.so", RTLD_LAZY);
    if(!artlib){
        exit(0);
    }
    org_artDexFileOpenMemory22 openMemory22 = (org_artDexFileOpenMemory22)fake_dlsym(artlib,
                                                                        "_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_7OatFileEPS9_");

    if(!openMemory22){
        exit(0);
    }
    jbyte *bytes;
    char *pDex  = NULL;
    std::string location = "";
    std::string err_msg;


    bytes = env->GetByteArrayElements(buf, 0);
    int inDexLen = env->GetArrayLength(buf);
    pDex = new char[inDexLen + 1];
    memset(pDex, 0, inDexLen + 1);
    memcpy(pDex, bytes, inDexLen);
    pDex[inDexLen] = 0;
    env->ReleaseByteArrayElements(buf, bytes, 0);
    const Header *dex_header = reinterpret_cast<const Header *>(pDex);
    void *value = openMemory22((const unsigned char *)pDex, inDexLen, location, dex_header->checksum_, NULL, NULL, &err_msg);

    jlong cookie = replace_cookie(env, value, 22);
    return cookie;

}

还需要实现 DEX 加载器另外一个重要功能,即加载类的能力,这个功能本来也是需要 Native 函数实现的,这里我们可以通过反射调用 DexFile 类的 defineClass 方法(加载类的调用链是:findClass->defineClass)实现

private Class defineClass(String name, ClassLoader loader, Object cookie,
                              DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            Class clsDexFile = Class.forName("dalvik.system.DexFile");
            Method mdDefineClass = clsDexFile.getDeclaredMethod("defineClass", String.class, ClassLoader.class, long.class, List.class);
            mdDefineClass.setAccessible(true);
            result = (Class) mdDefineClass.invoke(null, name, loader, cookie, suppressed);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return result;
    }

Cmake、ninja 编译 cpp 文件

native 层代码源文件在 cpp 目录下,可以直接调用 cpp/build.bat 一键编译批处理文件

cmake 生成 ninja 编译配置文件

D:\Android\Sdk\cmake\3.10.2.4988404\bin\cmake.exe . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=D:\Android\Sdk\ndk\20.0.5594570\build\c make\android.toolchain.cmake -DANDROID_ABI=x86 -DANDROID_NDK=D:\Android\Sdk\ndk\20.0.5594570 -DANDROID_PLATFORM=android-16 -DCMAKE_ANDROID_ARCH_ABI=x86 -DCMAKE_ANDROID_NDK=D:\Android\Sdk\ndk\20.0.5594570 -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_MAKE_PROGRAM=D:\Android\Sdk\cmake\3.10.2.4988404\bin\ninja.exe -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=16 -GNinja

ninja 生成 so 文件

只是修改 cpp 文件,直接运行 ninja 命令,不用重新用 cmake 生成 ninja 配置文件 如果有新 cpp 文件的增加删除,还是需要删除配置文件,重新运行 cmake 的

D:\Android\Sdk\cmake\3.10.2.4988404\bin\ninja.exe

参考

【1】Android 中 apk 加固完善篇之内存加载 dex 方案实现原理(不落地方式加载)

【2】内存解密并加载 DEX

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools