IO实战一:Apk加解密
背景
为什么要进行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是如何打包的,那么将加密流程加到整个打包流程就相对清晰了。如下图
什么?还是不懂呢?那来看看我们整个加密和解密方案呢?
我们的思路如下:
- 既然我们要加密,那么必然有解密,但是这个解密又必然是整个应用的一部分,但是连这部分都加密的话,那么系统就完全无法解析我们的应用,也就是完全无法安装了。所以我们需要将解密的部分提取出来单独作为一个module,且这个module是不能够被加密的。然后最好的解密时机就是首次启动应用的时候进行,所以Application自然成了我们负责解密的首选。那么是否意味着原apk中不能有这个module呢?答案是:错啦。原apk中同样要将这个解密module打包进去,否则原apk也无法编译通过啊。
- 我们都知道,系统在加载类的时候都是从我们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加解密相关推荐
- 墨者学院-密码学加解密实训(摩斯密码第2题)
密码学加解密实训(摩斯密码第2题) 难易程度:★★ 题目类型:代码审计 使用工具:FireFox浏览器.audacity 1.打开靶场,下载文件,这题没有加密文件格式,所以我们直接用音频编辑器auda ...
- Android APK加壳技术方案----代码实现
本文章由Jack_Jia编写,转载请注明出处. 文章链接:http://blog.csdn.net/jiazhijun/article/details/8746917 作者:Jack_Jia 邮 ...
- Flutter实战一Flutter聊天应用(十六)
在上一篇文章<Flutter实战一Flutter聊天应用(十五)>中,我们完成了登陆屏幕.在用户登陆成功后,会在本地创建一个LandingInformation文件,以使应用程序在启动时可 ...
- Java 实现 RSA 非对称加密算法-加解密和签名验签
1. 非对称加密算法简介 非对称加密算法又称现代加密算法,是计算机通信安全的基石,保证了加密数据不会被破解.与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密(pr ...
- 基于安卓系统的SM4-SM2/3加解密软件开发报告
目 录 第一章需求分析 1.1软件功能需求 1.2平台需求 1.3人员分工 第二章概要设计 2.1 软件开发平台 2.2 软件基本流程 2.3 UML图 第三章 程序详细设计 3.1 程序接口设计 3 ...
- 【k8s实战一】Jenkins 部署应用到 Kubernetes
[k8s实战一]Jenkins 部署应用到 Kubernetes 01 本文主旨 目标是演示整个Jenkins从源码构建镜像到部署镜像到Kubernetes集群过程. 为了简化流程与容易重现文中效果, ...
- android cocoscreator jsc js 间加解密(六)
前言 前面 学了 aandroid cocoscreator 热更新 超详细篇(五) 这章 主要学习 cocoscreator 构建后 jsc 与js 文件 之间相互转化(加解密)并实际测试. 可以配 ...
- 关于apk加壳之动态加载dex文件
由于自己之前做了一个关于手机令牌的APK软件,在实现的过程中尽管使用了native so进行一定的逻辑算法保护,但是在自己逆向破解的过程中发现我的手机令牌关键数据能够"轻易地"暴露 ...
- Android之Apk加壳
基于ADT环境开发的的实现,请参考: Android中的Apk的加固(加壳)原理解析和实现 类加载和dex文件相关的内容,如:Android动态加载Dex机制解析 一.什么是加壳? 加壳是在二进制的 ...
最新文章
- Linux下安装Dubbo运行环境
- Bootstrap 3: 菜单居中 Center content in responsive bootstrap navbar
- 漫步最优化二十——下降函数
- 华为手机截屏怎么截长图_华为手机5种常用截屏方式,教你轻松定格屏幕精彩瞬间...
- WPF MVVM 验证
- 深度补全(一)-论文阅读-翻译(Depth Map Prediction from a Single Image using a Multi-Scale Deep Network)
- “AI”与“爱”满格下的百度地图:刻画真实世界,社会责任同行
- JDK各个版本新特性介绍及使用
- Aggressive cows题解
- OpenStack巴塞罗那峰会,比拼技术更比拼用户体验
- 移动端轮播图——网易云音乐手机端样式
- 1021.Deepest Root
- 笔记本电脑什么牌子好 世界笔记本电脑排名
- 查mysql结构_Mysql查询架构信息
- 解决文字与图片始终不并排的问题
- 802.11 协议介绍
- Cris 的 Python 数据分析笔记 06:Pandas 常见的数据预处理
- Java系列技术之Mybatis3-钟洪发-专题视频课程
- PowerDesigner15及破解补丁下载
- 解决matlab中文乱码
热门文章
- css 将标签固定在底部
- Model-Based Value Expansion for Efficient Model-Free Reinforcement Learning(mve)
- 18-03 MySQL高可用方案与选择
- npm 安装axios报错看这个
- 系统分析师真题2018试卷相关概念二
- Kubernetes--k8s---进阶--AWS托管式容器服务EKS--EKS全面介绍和安装使用
- python 出气象雷达图
- oracle数据库拼接sql语句字符串问题
- java gui 嵌入浏览器_DJNativeSwing-SWT组件-Java GUI中内嵌浏览器
- windows10 英文路径下文件显示中文名称