背景

为什么要进行apk加密?答案是避免apk被有心人反编译,窃取公司重要技术和算法等。但是要给Apk加密要如何实现呢?首先我们需要知道Android整个Apk的打包流程,然后将加密的环节插入要其中的步骤中,加密完成后apk安装后要如何解密,这时候我们又要了解应用安装时apk是如何被加载运行。这一系列的问题都需要我们除了掌握IO知识外还要掌握如:类加载、热修复、反射、hook等知识。这里我们将借这个实战来检验学习效果。

一、Android的Apk打包流程

我们先来看看官方给的apk打包流程图

还是看不懂,那我们这样一步步解析来看呢?

整个apk打包流程被分成了如下七步:

  • 1、打包资源文件,生成R.java文件

打包资源的工具是aapt(The Android Asset Packaing Tool),位于android-sdk/platform-tools目录下。在这个过程中,项目中的AndroidManifest.xml文件和布局文件XML都会编译,然后生成相应的R.java。

  • 2、处理aidl文件,生成相应的Java文件

这一过程中使用到的工具是aidl(Android Interface Definition Language),即Android接口描述语言。位于android-sdk/platform-tools目录下。aidl工具解析接口定义文件然后生成相应的Java代码接口供程序调用。如果在项目没有使用到aidl文件,则可以跳过这一步。

  • 3、编译项目源代码,生成class文件

项目中所有的Java代码,包括R.java和.aidl文件,都会变Java编译器(javac)编译成.class文件,生成的class文件位于工程中的bin/classes目录下。

  • 4、转换所有的class文件,生成classes.dex文件

dx工具生成可供Android系统Dalvik虚拟机执行的classes.dex文件,该工具位于android-sdk/platform-tools 目录下。

任何第三方的libraries和.class文件都会被转换成.dex文件。dx工具的主要工作是将Java字节码转成成Dalvik字节码、压缩常量池、消除冗余信息等。

  • 5、打包生成APK文件

所有没有编译的资源(如images等)、编译过的资源和.dex文件都会被apkbuilder工具打包到最终的.apk文件中。

打包的工具apkbuilder位于 android-sdk/tools目录下。apkbuilder为一个脚本文件,实际调用的是android-sdk/tools/lib/sdklib.jar文件中的com.android.sdklib.build.ApkbuilderMain类。

  • 6、对APK文件进行签名

一旦APK文件生成,它必须被签名才能被安装在设备上。

在开发过程中,主要用到的就是两种签名的keystore。一种是用于调试的debug.keystore,它主要用于调试,在Eclipse或者Android Studio中直接run以后跑在手机上的就是使用的debug.keystore。另一种就是用于发布正式版本的keystore。

  • 7、对签名后的APK文件进行对齐处理

如果你发布的apk是正式版的话,就必须对APK进行对齐处理,用到的工具是zipalign,它位于android-sdk/tools目录下。

对齐的主要过程是将APK包中所有的资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时的速度会更快。对齐的作用就是减少运行时内存的使用。

二、加密和解密方案

既然我们已经弄清了apk是如何打包的,那么将加密流程加到整个打包流程就相对清晰了。如下图

什么?还是不懂呢?那来看看我们整个加密和解密方案呢?

我们的思路如下:

  1. 既然我们要加密,那么必然有解密,但是这个解密又必然是整个应用的一部分,但是连这部分都加密的话,那么系统就完全无法解析我们的应用,也就是完全无法安装了。所以我们需要将解密的部分提取出来单独作为一个module,且这个module是不能够被加密的。然后最好的解密时机就是首次启动应用的时候进行,所以Application自然成了我们负责解密的首选。那么是否意味着原apk中不能有这个module呢?答案是:错啦。原apk中同样要将这个解密module打包进去,否则原apk也无法编译通过啊。
  2. 我们都知道,系统在加载类的时候都是从我们apk的dex文件中加载的。ClassLoader会去维护一个这样的dex文件数组(这个在前面的热修复章节有介绍过)。而我们要做的就是将原apk中的dex都加密,然后将解密部分的代码单独编程成dex文件(我们称这样的dex为壳dex)连带着加密的dex一起加到新apk中。这样新apk安装后系统就能够找到我们应用启动的入口Application了,不至于由于加密导致系统找不到应用程序入口。而在这个程序入口中我们要做的就是解密被加密的dex文件,然后重新插入到ClassLoader维护的dex文件数组中(这里就涉及到大量的反射知识)。

三、加密实现

方案说了那么多,到底如何实现呢?实现后到底能不能像我们说的那样正常安装运行呢?撸代码来验证!

先来看看我们加密工程未运行前的结构图

再来看看工程运行后,工程结构的变化

可以看到运行后原apk被加密和解密模块被放到一起重新打包成了新的apk。这个过程代码如何实现呢?

1、既然要加密,必然要选择加密方式,初始化加密算法

    //这里我们选择已封装好的Cipher加密。public static final String DEFAULT_PWD = "abcdefghijklmnop";//加密和解密的key要一致,所以解密模块的key也要是同样的。private static final String algorithmStr = "AES/ECB/PKCS5Padding";private static Cipher encryptCipher;//用来的加密的Cipher实例private static Cipher decryptCipher;//用来解密的Cipher实例/***  初始化加密算法* @param password  这里的password对应DEFAULT_PWD*/public static void init(String password) {try {// 生成一个实现指定转换的 Cipher 对象。encryptCipher = Cipher.getInstance(algorithmStr);decryptCipher = Cipher.getInstance(algorithmStr);// algorithmStrbyte[] keyStr = password.getBytes();SecretKeySpec key = new SecretKeySpec(keyStr, "AES");encryptCipher.init(Cipher.ENCRYPT_MODE, key);decryptCipher.init(Cipher.DECRYPT_MODE, key);} catch (NoSuchAlgorithmException e) {e.printStackTrace();} catch (NoSuchPaddingException e) {e.printStackTrace();} catch (InvalidKeyException e) {e.printStackTrace();}}

2、加密之前我们需要先创建两个目录用来存放原apk和解密模块压缩出来的源文件

                /*** 分别在apk和aar目录下生成两个temp目录用来存放加密的未打包的apk文件*/File apkTemp = new File("source/apk/temp");if(apkTemp.exists()) {File[] files = apkTemp.listFiles();for(File file:files) {if(file.exists()) {file.delete();}}}File aarTemp = new File("source/aar/temp");if(aarTemp.exists()) {File[] files = aarTemp.listFiles();for(File file:files) {if(file.exists()) {file.delete();}}}

3、解压原apk,并加密原apk中的dex文件。

        /**    * 解压原apk文件到apk/temp目录下,并加密dex文件*/File sourceApk = new File("source/apk/app-debug.apk");File newApkDir = new File(sourceApk.getParent() + File.separator + "temp");if(!newApkDir.exists()) {newApkDir.mkdirs();}//解压原apk,加密dexAESUtil.encryptAPKFile(sourceApk,newApkDir);if (newApkDir.isDirectory()) {File[] listFiles = newApkDir.listFiles();for (File file : listFiles) {if (file.isFile()) {//修改classes.dex名为classes_.dex,避免等会与aar中的classes.dex重名if (file.getName().endsWith(".dex")) {String name = file.getName();int cursor = name.indexOf(".dex");String newName = file.getParent()+ File.separator + name.substring(0, cursor) + "_" + ".dex";file.renameTo(new File(newName));}}}}

什么?没看到解压和加密的核心代码?传送门在这里,自己去看。

4、解压aar文件,并生成壳dex

先解压aar文件,再利用dx工具将解压出来的classes.jar文件转换成壳dex,并拷贝到新apk的源目录下

            /*** 解压aar文件(不能进行加密的部分),再利用dx将jar转换成dex,并将dex文件拷贝到apk/temp中来*/File aarFile = new File("source/aar/mylibrary-debug.aar");File sourceAarDex = null;try {//解压aar文件,并通过dx工具将jar文件转换成dex文件sourceAarDex  = DxUtil.jar2Dex(aarFile);}catch(Exception e){e.printStackTrace();}File copyAarDex = new File(newApkDir.getPath() + File.separator + "classes.dex");if (!copyAarDex.exists()) {copyAarDex.createNewFile();}//拷贝aar/temp中的classes.dex到apk/temp中FileOutputStream fos = new FileOutputStream(copyAarDex);byte[] fbytes = ByteUtil.getBytes(sourceAarDex);fos.write(fbytes);fos.flush();fos.close();

想看如何通过dx工具将jar转换成dex的核心代码?

 public static void dxCommand(File aarDex, File classes_jar) throws IOException, InterruptedException {Runtime runtime = Runtime.getRuntime();//这里需要注意,commond中dx需要配置环境变量后才可以这样写,否则需要指定dx.bat的绝对路径String commond = "cmd.exe /C dx --dex --output=" + aarDex.getAbsolutePath() + " " +classes_jar.getAbsolutePath();Process process = runtime.exec(commond);System.out.println("runtime  dxCommand");process.waitFor();System.out.println("waitFor  dxCommand");} catch (InterruptedException e) {System.out.println("InterruptedException  dxCommand");e.printStackTrace();throw e;}if (process.exitValue() != 0) {System.out.println("getErrorStream  dxCommand");InputStream inputStream = process.getErrorStream();int len;byte[] buffer = new byte[2048];ByteArrayOutputStream bos = new ByteArrayOutputStream();while((len=inputStream.read(buffer)) != -1){bos.write(buffer,0,len);}//输出出错信息System.out.println(new String(bos.toByteArray(),"GBK"));throw new RuntimeException("dx run failed");}process.destroy();}

 5、一切就绪,打包apk/temp目录生成新的未签名apk文件

 File unsignedApk = new File("result/apk-unsigned.apk");unsignedApk.getParentFile().mkdirs();ZipUtil.zip(newApkDir, unsignedApk);

 6、给加密后组合压缩成的新apk文件重新签名

File signedApk = new File("result/apk-signed.apk");
SignatureUtil.signature(unsignedApk, signedApk);

我们继续看看这个签名是个什么黑科技:

/*** 为加密后的apk文件添加签名* @param unsignedApk* @param signedApk* @throws InterruptedException* @throws IOException*/public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {String cmd[] = {"cmd.exe", "/C","jarsigner",  "-sigalg", "MD5withRSA","-digestalg", "SHA1","-keystore", "C:/Users/cizongfa/.android/debug.keystore","-storepass", "android","-keypass", "android","-signedjar", signedApk.getAbsolutePath(),unsignedApk.getAbsolutePath(),"androiddebugkey"};Process process = Runtime.getRuntime().exec(cmd);System.out.println("start sign");try {int waitResult = process.waitFor();System.out.println("waitResult: " + waitResult);} catch (InterruptedException e) {e.printStackTrace();throw e;}System.out.println("process.exitValue() " + process.exitValue() );if (process.exitValue() != 0) {InputStream inputStream = process.getErrorStream();int len;byte[] buffer = new byte[2048];ByteArrayOutputStream bos = new ByteArrayOutputStream();while((len=inputStream.read(buffer)) != -1){bos.write(buffer,0,len);}System.out.println(new String(bos.toByteArray(),"GBK"));throw new RuntimeException("签名执行失败");}System.out.println("finish signed");process.destroy();}

什么?完全不了解这个签名的机制和这个签名命令的意思?传送门送给你。还想要源码研究研究?再送给你整个项目。

四、解密实现

要完成解密,我们需要做如下几件事

  • 找到合适的解密时机
  • 壳dex并没有被加密,需要排除在解密的dex文件之外
  • 解密后的dex文件需要重新插入到ClassLoader中(热修复思想的应用)。
  • 解密后应用是否能够正常运行?

1、解密时机

作为一个被加密的应用,安装的时候我们应用本身是无法控制。所以应用第一次启动的时候就成了我们最佳的解密时机了。

所以我们将解密的逻辑放到Application的attachBaseContext()方法中。

2、解压apk、脱壳并解密被加密的核心dex

 /*** 解压并解密dex* @param apkFile  被加密的apk文件* @param newAppDir 存放解压和解密后的apk源文件目录*/private void unZipAndAecryptDex(File apkFile,File newAppDir){if (!newAppDir.exists()) {//解压apk到指定目录ZipUtil.unZip(apkFile, newAppDir);File[] files = newAppDir.listFiles();for (File file : files) {String name = file.getName();/*** 是否还记得我们在加密的时候将不能加密的壳dex命名为classes.dex并拷贝到新apk中打包生成新的apk呢?* 所以这里我们做脱壳,壳dex不需要进行解密操作。因为它根本就没有被加密。*/if (name.equals("classes.dex")) {} else if (name.endsWith(".dex")) {/*** 对被加密的核心dex进行解密,对应加密流程中的classes_.dex*/try {byte[] bytes = getBytes(file);FileOutputStream fos = new FileOutputStream(file);byte[] decrypt = AESUtil.decrypt(bytes);fos.write(decrypt);fos.flush();fos.close();} catch (Exception e) {e.printStackTrace();}}}}}

3、将解密后的dex文件重新插入ClassLoader,实现热修复

private static void install(ClassLoader loader, List<File> additionalClassPathEntries,File optimizedDirectory) throws IllegalArgumentException,IllegalAccessException, NoSuchFieldException, InvocationTargetException,NoSuchMethodException {/*** 通过反射找到BaseDexClassLoader中的pathList属性,* 这是一个ClassLoader中存放Dex文件列表的DexPathList变量,* 其内部维护着一个dex文件数组。ClassLoader加载类的时候就会从这dex数组中去查找。* 具体逻辑请查看源码http://androidxref.com/8.0.0_r4/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java* 而我们要做的就是将解密出来的dex重新插入到这个数组里面。这个在前将类加载和热修复的时候已经有提到过。所以看源码是每个程序员必须具备的技能。*/Field pathListField = findField(loader, "pathList");Object dexPathList = pathListField.get(loader);ArrayList suppressedExceptions = new ArrayList();Log.d(TAG, "Build.VERSION.SDK_INT " + Build.VERSION.SDK_INT);/*** 这里需要区分一下版本区别。每个版本的Android源码都有相应的改变,* 这就需要我们在做这些功能的时候不得不去考虑各个版本的适配。这也是Android开发让人头疼的地方。*/if (Build.VERSION.SDK_INT >= 23) {//将解密后的dex文件插入到DexPathList的dexElements数组中。expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList, newArrayList(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));} else {expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, newArrayList(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));}if (suppressedExceptions.size() > 0) {Iterator suppressedExceptionsField = suppressedExceptions.iterator();while (suppressedExceptionsField.hasNext()) {IOException dexElementsSuppressedExceptions = (IOException)suppressedExceptionsField.next();Log.w("MultiDex", "Exception in makeDexElement",dexElementsSuppressedExceptions);}Field suppressedExceptionsField1 = findField(loader,"dexElementsSuppressedExceptions");IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[])suppressedExceptionsField1.get(loader));if (dexElementsSuppressedExceptions1 == null) {dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);} else {IOException[] combined = new IOException[suppressedExceptions.size() +dexElementsSuppressedExceptions1.length];suppressedExceptions.toArray(combined);System.arraycopy(dexElementsSuppressedExceptions1, 0, combined,suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);dexElementsSuppressedExceptions1 = combined;}suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);}
}/*** 创建我们自己的dex文件数组,可查看源码中的makeDexElements方法* 附上源码链接http://androidxref.com/8.0.0_r4/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java* @param dexPathList* @param files* @param optimizedDirectory* @param suppressedExceptions* @return* @throws IllegalAccessException* @throws InvocationTargetException* @throws NoSuchMethodException*/private static Object[] makeDexElements(Object dexPathList,ArrayList<File> files, FileoptimizedDirectory,ArrayList<IOException> suppressedExceptions) throwsIllegalAccessException, InvocationTargetException, NoSuchMethodException {Method makeDexElements = findMethod(dexPathList, "makeDexElements", newClass[]{ArrayList.class, File.class, ArrayList.class});return ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files,optimizedDirectory, suppressedExceptions}));}
}

4、检验实战成果

5、附上项目地址

到底能不能做到,还是要附上真实代码,拉出来遛一遛才能确定,传送门走你。

IO实战一:Apk加解密相关推荐

  1. 墨者学院-密码学加解密实训(摩斯密码第2题)

    密码学加解密实训(摩斯密码第2题) 难易程度:★★ 题目类型:代码审计 使用工具:FireFox浏览器.audacity 1.打开靶场,下载文件,这题没有加密文件格式,所以我们直接用音频编辑器auda ...

  2. Android APK加壳技术方案----代码实现

    本文章由Jack_Jia编写,转载请注明出处. 文章链接:http://blog.csdn.net/jiazhijun/article/details/8746917 作者:Jack_Jia    邮 ...

  3. Flutter实战一Flutter聊天应用(十六)

    在上一篇文章<Flutter实战一Flutter聊天应用(十五)>中,我们完成了登陆屏幕.在用户登陆成功后,会在本地创建一个LandingInformation文件,以使应用程序在启动时可 ...

  4. Java 实现 RSA 非对称加密算法-加解密和签名验签

    1. 非对称加密算法简介 非对称加密算法又称现代加密算法,是计算机通信安全的基石,保证了加密数据不会被破解.与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密(pr ...

  5. 基于安卓系统的SM4-SM2/3加解密软件开发报告

    目 录 第一章需求分析 1.1软件功能需求 1.2平台需求 1.3人员分工 第二章概要设计 2.1 软件开发平台 2.2 软件基本流程 2.3 UML图 第三章 程序详细设计 3.1 程序接口设计 3 ...

  6. 【k8s实战一】Jenkins 部署应用到 Kubernetes

    [k8s实战一]Jenkins 部署应用到 Kubernetes 01 本文主旨 目标是演示整个Jenkins从源码构建镜像到部署镜像到Kubernetes集群过程. 为了简化流程与容易重现文中效果, ...

  7. android cocoscreator jsc js 间加解密(六)

    前言 前面 学了 aandroid cocoscreator 热更新 超详细篇(五) 这章 主要学习 cocoscreator 构建后 jsc 与js 文件 之间相互转化(加解密)并实际测试. 可以配 ...

  8. 关于apk加壳之动态加载dex文件

    由于自己之前做了一个关于手机令牌的APK软件,在实现的过程中尽管使用了native so进行一定的逻辑算法保护,但是在自己逆向破解的过程中发现我的手机令牌关键数据能够"轻易地"暴露 ...

  9. Android之Apk加壳

    基于ADT环境开发的的实现,请参考: Android中的Apk的加固(加壳)原理解析和实现  类加载和dex文件相关的内容,如:Android动态加载Dex机制解析 一.什么是加壳? 加壳是在二进制的 ...

最新文章

  1. Linux下安装Dubbo运行环境
  2. Bootstrap 3: 菜单居中 Center content in responsive bootstrap navbar
  3. 漫步最优化二十——下降函数
  4. 华为手机截屏怎么截长图_华为手机5种常用截屏方式,教你轻松定格屏幕精彩瞬间...
  5. WPF MVVM 验证
  6. 深度补全(一)-论文阅读-翻译(Depth Map Prediction from a Single Image using a Multi-Scale Deep Network)
  7. “AI”与“爱”满格下的百度地图:刻画真实世界,社会责任同行
  8. JDK各个版本新特性介绍及使用
  9. Aggressive cows题解
  10. OpenStack巴塞罗那峰会,比拼技术更比拼用户体验
  11. 移动端轮播图——网易云音乐手机端样式
  12. 1021.Deepest Root
  13. 笔记本电脑什么牌子好 世界笔记本电脑排名
  14. 查mysql结构_Mysql查询架构信息
  15. 解决文字与图片始终不并排的问题
  16. 802.11 协议介绍
  17. Cris 的 Python 数据分析笔记 06:Pandas 常见的数据预处理
  18. Java系列技术之Mybatis3-钟洪发-专题视频课程
  19. PowerDesigner15及破解补丁下载
  20. 解决matlab中文乱码

热门文章

  1. css 将标签固定在底部
  2. Model-Based Value Expansion for Efficient Model-Free Reinforcement Learning(mve)
  3. 18-03 MySQL高可用方案与选择
  4. npm 安装axios报错看这个
  5. 系统分析师真题2018试卷相关概念二
  6. Kubernetes--k8s---进阶--AWS托管式容器服务EKS--EKS全面介绍和安装使用
  7. python 出气象雷达图
  8. oracle数据库拼接sql语句字符串问题
  9. java gui 嵌入浏览器_DJNativeSwing-SWT组件-Java GUI中内嵌浏览器
  10. windows10 英文路径下文件显示中文名称