Android APK 加固技术探究(二)

Android APK 加固技术探究(一)

Android APK 加固技术探究(二)

Android APK 加固技术探究(三)

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

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

源码地址:https://gitee.com/openjk/apk-steady

加固流程

  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 的解密便结束了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容