Android APK 加固技术探究(二)

Android APK 加固技术探究(一)

Android APK 加固技术探究(二)

Android APK 加固技术探究(三)

为了保证 Android 应用的源码安全性,我们一般会对上线的应用进行代码混淆,然而仅仅做代码混淆还不够,我们还要对我们的应用加固,防止别人通过反编译获取到我们的源码。目前 apk 加固技术比较成熟完善,市面上比较流行的有“360加固”。本文就 apk 加固技术做一个技术探究,希望读者看过后能明白加固的其中原理,并也能自己实现加固方案。

Android apk 加固技术探究(一)中,大致介绍了反编译的过程及我们能够获取到源码的原因。下面就来讲解加固的基本流程。

源码地址:
Gitee https://gitee.com/openjk/apk-steady
Github https://github.com/openJK-dev/AppJiagu-Android

加固流程

  1. 新建一个 Android 工程,在其中建立一个 shell 的 module 用来生成加固的壳 arr 文件
  2. shell module 中包含一个 Application 的子类 SteadyApplication,其中包含对 dex 文件解密的逻辑
  3. 编译 shell 生成 shell.aar 文件
  4. 解压待加固 apk 到 apkUnzip 目录中,拿到其中的所有 dex 文件
  5. 修改 apkUnzip 下 AndroidManifest.xml 文件中 application 根结点下的 name 属性值为 2 中创建的 SteadyApplication。同时将原 apk 的 Application 的路径保存到 meta-data 节点下,以备在 SteadyApplication 中解析生成
  6. 使用加密算法将上一步中得到的 dex 文件加密,并删除原 dex 文件
  7. 解压 3 中生成的 aar 文件,获得到里面的 jar 文件,然后通过 SDK 中提供的 dx 工具将 jar 文件转换成 dex 文件,将生成的 dex 文件放到 apkUnzip 文件
  8. 压缩 apkUnzip 文件夹生成新的 apk
  9. 重新签名

这篇文章主要讲解上述1、2、3步骤,如何生成一个 Shell.arr(壳)文件。Shell 最终会打入到原 apk 的class.dex 中,用来解密已经加密的原 apk 中的dex和加载原来的 dex 文件

一、生成 Shell.aar(dex 解密和类加载)

1、解密加固的 dex 文件的流程

  1. 在 Application 中可以通过 getApplicationInfo().sourceDir 来获取 base APK,这个 apk 就包含了我们应用的所有代码。
  2. 通过 Application 的getDir() 方法,我们在应用的私有目录创建一个私有文件夹 SteadyDir
  3. 在 2 中创建的目录里面我们将 bask.apk 解压
  4. 解压后我们得到 apk 的所有文件,然后过滤出所有以dex为后缀的文件。其中 classes.dex 文件我们不需要因为它已经被加载进系统,所以只需要处理被我们加密的dex 文件
  5. 将解密后的 dex 文件加载到程序中
  6. 运行 apk 真实的 application,启动 app

2、如何解压 apk 文件

zip 解压主要用到了 java 中的 ZipFile 类,具体实现直接上代码,代码中包含注释就不多解释。

public static void unZip(File zip, File dir) {
    try {
        //清空存放解压文件的目录
        deleteFile(dir);
        ZipFile zipFile = new ZipFile(zip);
        //zip文件中每一个条目
        Enumeration<? extends ZipEntry> entries = zipFile.entries();
        //遍历
        while (entries.hasMoreElements()) {
            ZipEntry zipEntry = entries.nextElement();
            //zip中 文件/目录名
            String name = zipEntry.getName();
            //原来的签名文件 不需要了
            if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
                    .equals("META-INF/MANIFEST.MF")) {
                continue;
            }
            //空目录不管
            if (!zipEntry.isDirectory()) {
                File file = new File(dir, name);
                //创建目录
                if (!file.getParentFile().exists()) {
                    file.getParentFile().mkdirs();
                }
                //写文件
                FileOutputStream fos = new FileOutputStream(file);
                InputStream is = zipFile.getInputStream(zipEntry);
                byte[] buffer = new byte[2048];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    fos.write(buffer, 0, len);
                }
                is.close();
                fos.close();
            }
        }
        zipFile.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}


private static void deleteFile(File file){
    if (file.isDirectory()){
        File[] files = file.listFiles();
        for (File f: files) {
            deleteFile(f);
        }
    }else{
        file.delete();
    }
}

3、如何解密 dex 文件

通过第二步中的解压方式,我们可以很轻松的将 base.apk 解压到私有目录下。然后我们通过文件的后缀名.dex 过滤出所有 dex 文件(排除 classes.dex),接着读取每个 dex 到字节数组中,然后对字节数组进行解密操作。
这里加解密使用的是 AES 的方式,为了增加安全性这里将解密的方式用 jni 方式完成。解密方式如下:

jbyteArray decrypt(JNIEnv *env,jbyteArray srcData) {
    jstring type = (*env).NewStringUTF("AES");
    jstring cipher_mode = (*env).NewStringUTF("AES/ECB/PKCS5Padding");
    jbyteArray pwd = (*env).NewByteArray(16);
    char *master_key = (char *) "huangdh'l,.AMWK;";
    (*env).SetByteArrayRegion(pwd,0,16,reinterpret_cast<jbyte *>(master_key));

    jclass secretKeySpecClass = (*env).FindClass("javax/crypto/spec/SecretKeySpec");
    jmethodID secretKeySpecMethodId = (*env).GetMethodID(secretKeySpecClass,"<init>", "([BLjava/lang/String;)V");
    jobject secretKeySpecObj = (*env).NewObject(secretKeySpecClass,secretKeySpecMethodId,pwd,type);

    jclass cipherClass = (*env).FindClass("javax/crypto/Cipher");
    jmethodID cipherInitMethodId = (*env).GetMethodID(cipherClass,"init", "(ILjava/security/Key;)V");
    jmethodID cipherInstanceMethodId = (*env).GetStaticMethodID(cipherClass,"getInstance", "(Ljava/lang/String;)Ljavax/crypto/Cipher;");
    jobject cipherObj = (*env).CallStaticObjectMethod(cipherClass,cipherInstanceMethodId,cipher_mode);

    jfieldID decryptModeFieldId = (*env).GetStaticFieldID(cipherClass,"DECRYPT_MODE", "I");
    jint mode = (*env).GetStaticIntField(cipherClass,decryptModeFieldId);
    (*env).CallVoidMethod(cipherObj,cipherInitMethodId,mode,secretKeySpecObj);

    jmethodID doFinalMethodId = (*env).GetMethodID(cipherClass,"doFinal", "([B)[B");
    jbyteArray text = (jbyteArray)(*env).CallObjectMethod(cipherObj,doFinalMethodId,srcData);
    return text;
}

4、加载 dex 文件

通过上面的解压和解密操作我们得到了原始的 dex 文件,我们将这些dex文件放进一个集合中,接下来使用类加载机制加载已经解密后的 dex 文件。关于类加载机制会在后续文章中讲解。

public static void loadDex(Application application,List<File> dexFiles, File versionDir) throws Exception{
    //1.先从 ClassLoader 中获取 pathList 的变量
    Field pathListField = ProxyUtils.findField(application.getClassLoader(), "pathList");
    //1.1 得到 DexPathList 类
    Object pathList = pathListField.get(application.getClassLoader());
    //1.2 从 DexPathList 类中拿到 dexElements 变量
    Field dexElementsField= ProxyUtils.findField(pathList,"dexElements");
    //1.3 拿到已加载的 dex 数组
    Object[] dexElements=(Object[])dexElementsField.get(pathList);

    //2. 反射到初始化 dexElements 的方法,也就是得到加载 dex 到系统的方法
    Method makeDexElements= ProxyUtils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);
    //2.1 实例化一个 集合  makePathElements 需要用到
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    //2.2 反射执行 makePathElements 函数,把已解码的 dex 加载到系统,不然是打不开 dex 的,会导致 crash
    Object[] addElements=(Object[])makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions);

    //3. 实例化一个新数组,用于将当前加载和已加载的 dex 合并成一个新的数组
    Object[] newElements= (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+addElements.length);
    //3.1 将系统中的已经加载的 dex 放入 newElements 中
    System.arraycopy(dexElements,0,newElements,0,dexElements.length);
    //3.2 将解密后已加载的 dex 放入新数组中
    System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);

    //4. 将合并的新数组重新设置给 DexPathList的 dexElements
    dexElementsField.set(pathList,newElements);
}

5、加载真实的 application 类,运行 app

1、首先从 AndroidManifest.xml 文件中获取到原 application 的类名。(在下一篇文章中会讲解我们如何将 apk 的原来的 application 类名放到 AndroidManifest.xml 的meta-data 标签下)

/**
 * 解析项目中原来的 Application 名称
 */
private void getMateData(){
    try{
        ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(),
                PackageManager.GET_META_DATA);//获取包信息
        Bundle metaData = applicationInfo.metaData;//获取 Meta-data 的键值对信息
        if(null != metaData){
            if(metaData.containsKey("app_name")){
                app_name = metaData.getString("app_name");//获取原来的包名
            }
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

2、获取到原 application 的类名后就通过反射获取到 application 的实例。

private void bindRealApplication() throws Exception{
        if(isBindReal){
            return;
        }
        if(TextUtils.isEmpty(app_name)){
            return;
        }
        //1、得到 attachBaseContext(context)传入的上下文 ContextImpl
        Context baseContext = getBaseContext();
        //2、拿到真实 APK Application 的 class
        Class<?> delegateClass = Class.forName(app_name);
        //反射实例化,
        delegate = (Application) delegateClass.newInstance();
        //得到 Application attach() 方法 也就是最先初始化的
        Method attach = Application.class.getDeclaredMethod("attach",Context.class);
        attach.setAccessible(true);
        //执行 Application#attach(Context)
        attach.invoke(delegate,baseContext);

        //        ContextImpl---->mOuterContext(app)   通过Application的attachBaseContext回调参数获取
        //4. 拿到 Context 的实现类
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        //4.1 获取 mOuterContext Context 属性
        Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
        mOuterContextField.setAccessible(true);
        //4.2 将真实的 Application 交于 Context 中。这个根据源码执行,实例化 Application 下一个就行调用 setOuterContext 函数,所以需要绑定 Context
        //  app = mActivityThread.mInstrumentation.newApplication(
        //                    cl, appClass, appContext);
        //  appContext.setOuterContext(app);
        mOuterContextField.set(baseContext, delegate);

//        ActivityThread--->mAllApplications(ArrayList)       ContextImpl的mMainThread属性
        //5. 拿到 ActivityThread 变量
        Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
        mMainThreadField.setAccessible(true);
        //5.1 拿到 ActivityThread 对象
        Object mMainThread = mMainThreadField.get(baseContext);

//        ActivityThread--->>mInitialApplication
        //6. 反射拿到 ActivityThread class
        Class<?> activityThreadClass=Class.forName("android.app.ActivityThread");
        //6.1 得到当前加载的 Application 类
        Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
        mInitialApplicationField.setAccessible(true);
        //6.2 将 ActivityThread 中的 Applicaiton 替换为 真实的 Application 可以用于接收相应的声明周期和一些调用等
        mInitialApplicationField.set(mMainThread,delegate);


//        ActivityThread--->mAllApplications(ArrayList)       ContextImpl的mMainThread属性
        //7. 拿到 ActivityThread 中所有的 Application 集合对象,这里是多进程的场景
        Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
        mAllApplicationsField.setAccessible(true);
        ArrayList<Application> mAllApplications =(ArrayList<Application>) mAllApplicationsField.get(mMainThread);
        //7.1 删除 ProxyApplication
        mAllApplications.remove(this);
        //7.2 添加真实的 Application
        mAllApplications.add(delegate);

//        LoadedApk------->mApplication                      ContextImpl的mPackageInfo属性
        //8. 从 ContextImpl 拿到 mPackageInfo 变量
        Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
        //8.1 拿到 LoadedApk 对象
        Object mPackageInfo=mPackageInfoField.get(baseContext);

        //9 反射得到 LoadedApk 对象
        //    @Override
        //    public Context getApplicationContext() {
        //        return (mPackageInfo != null) ?
        //                mPackageInfo.getApplication() : mMainThread.getApplication();
        //    }
        Class<?> loadedApkClass=Class.forName("android.app.LoadedApk");
        Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
        mApplicationField.setAccessible(true);
        //9.1 将 LoadedApk 中的 Application 替换为 真实的 Application
        mApplicationField.set(mPackageInfo,delegate);

        //修改ApplicationInfo className   LooadedApk

        //10. 拿到 LoadApk 中的 mApplicationInfo 变量
        Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
        mApplicationInfoField.setAccessible(true);
        //10.1 根据变量反射得到 ApplicationInfo 对象
        ApplicationInfo mApplicationInfo = (ApplicationInfo)mApplicationInfoField.get(mPackageInfo);
        //10.2 将我们真实的 APPlication ClassName 名称赋值于它
        mApplicationInfo.className=app_name;

        //11. 执行 代理 Application onCreate 声明周期
        delegate.onCreate();

        //解码完成
        isBindReal = true;

    }

至此 apk 的解密便结束了

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值