Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/yangkun19921001/dexencryptiondecryption
APK 加固 dex 加密,解密 学习项目
https://github.com/yangkun19921001/dexencryptiondecryption
Last synced: 2 days ago
JSON representation
APK 加固 dex 加密,解密 学习项目
- Host: GitHub
- URL: https://github.com/yangkun19921001/dexencryptiondecryption
- Owner: yangkun19921001
- License: apache-2.0
- Created: 2019-06-02T15:27:04.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2024-02-07T15:37:14.000Z (11 months ago)
- Last Synced: 2024-12-24T12:26:12.054Z (9 days ago)
- Language: Java
- Homepage:
- Size: 1.44 MB
- Stars: 227
- Watchers: 11
- Forks: 56
- Open Issues: 7
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
[文章博客地址](https://juejin.im/post/5cf3ee295188256aa76bb1e1)
## 简介现在随意在应用市场下载一个 APK 文件然后反编译,95% 以上基本上都是经过混淆,加密,或第三方加固(第三方加固也是这个原理),那么今天我们就对 Dex 来进行加密解密。让反编译无法正常阅读项目源码。
**加密后的结构**
**APK 分析**
![](https://user-gold-cdn.xitu.io/2019/6/3/16b18eeecfed7d98?w=1365&h=646&f=jpeg&s=104055)
通过 AS 工具分析加密后的 APK 文件,查看 dex 是报错的,要的就是这个效果。**反编译效果**
![](https://user-gold-cdn.xitu.io/2019/6/2/16b18dbb0cd919a2?w=1344&h=756&f=jpeg&s=152627)
## 想要对 Dex 加密 ,先来了解什么是 64 K 问题
想要详细了解 64 k 的问题可以参考[官网]()
随着 Android 平台的持续成长,Android 应用的大小也在增加。当您的应用及其引用的库达到特定大小时,您会遇到构建错误,指明您的应用已达到 Android 应用构建架构的极限。早期版本的构建系统按如下方式报告这一错误:
```java
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
```较新版本的 Android 构建系统虽然显示的错误不同,但指示的是同一问题:
```java
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
```这些错误状况都会显示下面这个数字:65,536。这个数字很重要,因为它代表的是单个 Dalvik Executable (DEX) 字节码文件内的代码可调用的引用总数。本节介绍如何通过启用被称为 *Dalvik 可执行文件分包*的应用配置来越过这一限制,使您的应用能够构建并读取 Dalvik 可执行文件分包 DEX 文件。
### 关于 64K 引用限制
#### Android 5.0 之前版本的 Dalvik 可执行文件分包支持
Android 5.0(API 级别 21)之前的平台版本使用 Dalvik 运行时来执行应用代码。默认情况下,Dalvik 限制应用的每个 APK 只能使用单个 `classes.dex` 字节码文件。要想绕过这一限制,您可以使用 [Dalvik 可执行文件分包支持库](https://developer.android.com/tools/support-library/features.html?hl=zh-cn#multidex),它会成为您的应用主要 DEX 文件的一部分,然后管理对其他 DEX 文件及其所包含代码的访问。
#### Android 5.0 及更高版本的 Dalvik 可执行文件分包支持
Android 5.0(API 级别 21)及更高版本使用名为 ART 的运行时,后者原生支持从 APK 文件加载多个 DEX 文件。ART 在应用安装时执行预编译,扫描 `classesN.dex` 文件,并将它们编译成单个 `.oat` 文件,供 Android 设备执行。因此,如果您的 `minSdkVersion` 为 21 或更高值,则不需要 Dalvik 可执行文件分包支持库。
### 解决 64K 限制
1. 如果您的 `minSdkVersion` 设置为 21 或更高值,您只需在模块级 `build.gradle` 文件中将 `multiDexEnabled` 设置为 `true`,如此处所示:
```java
android {
defaultConfig {
...
minSdkVersion 21
targetSdkVersion 28
multiDexEnabled true
}
...
}
```但是,如果您的 `minSdkVersion` 设置为 20 或更低值,则您必须按如下方式使用 [Dalvik 可执行文件分包支持库](https://developer.android.com/tools/support-library/features.html?hl=zh-cn#multidex):
- 修改模块级 `build.gradle` 文件以启用 Dalvik 可执行文件分包,并将 Dalvik 可执行文件分包库添加为依赖项,如此处所示
```java
android {
defaultConfig {
...
minSdkVersion 15
targetSdkVersion 28
multiDexEnabled true
}
...
}
dependencies {
compile 'com.android.support:multidex:1.0.3'
}
```- 当前 Application extends MultiDexApplication {...} 或者 MultiDex.install(this);
2. 通过混淆 开启 ProGuard 移除未使用的代码,构建代码压缩。
3. 减少第三方库的直接依赖,尽可能下载源码,需要什么就用什么没必要依赖整个项目。
## Dex 加密与解密
![](https://user-gold-cdn.xitu.io/2019/6/2/16b18dbfe0ee1c3c?w=960&h=720&f=jpeg&s=22617)
**流程:**
1. 拿到 APK 解压得到所有的 dex 文件。
2. 通过 Tools 来进行加密,并把加密后的 dex 和代理应用 class.dex 合并,然后重新签名,对齐,打包。
3. 当用户安装 APK 打开进入代理解密的 Application 时,反射得到 dexElements 并将解密后的 dex 替换 DexPathList 中的 dexElements .### Dex 文件加载过程
既然要查 Dex 加载过程,那么得先知道从哪个源码 class 入手,既然不知道那么我们就先打印下 ClassLoader ;
![](https://user-gold-cdn.xitu.io/2019/6/2/16b18dc27dad72a8?w=761&h=438&f=jpeg&s=58192)
下面就以一个流程图来详细了解下 Dex 加载过程吧
![](https://user-gold-cdn.xitu.io/2019/6/2/16b18dc848c69036?w=1110&h=793&f=jpeg&s=35355)
最后我们得知在 **findClass(String name,List sup)** 遍历 **dexElements** 找到 Class 并交给 Android 加载。
### Dex 解密
现在我们知道 dex 加载流程了 , 那么我们怎么进行来对 dex 解密勒,刚刚我们得知需要遍历 dexElements 来找到 Class 那么我们是不是可以在遍历之前 ,初始化 dexElements 的时候。反射得到 dexElements 将我们解密后的 dex 交给 dexElements 。下面我们就通过代码来进行解密 dex 并替换 DexPathList 中的 dexElements;
1. 得到当前加密了的 APK 文件 并解压
```java
//得到当前加密了的APK文件
File apkFile=new File(getApplicationInfo().sourceDir);
//把apk解压 app_name+"_"+app_version目录中的内容需要boot权限才能用
File versionDir = getDir(app_name+"_"+app_version,MODE_PRIVATE);
File appDir=new File(versionDir,"app");
File dexDir=new File(appDir,"dexDir");
```2. 得到我们需要加载的 Dex 文件
```java
//把apk解压到appDir
Zip.unZip(apkFile,appDir);
//获取目录下所有的文件
File[] files=appDir.listFiles();
for (File file : files) {
String name=file.getName();
if(name.endsWith(".dex") && !TextUtils.equals(name,"classes.dex")){
try{
AES.init(AES.DEFAULT_PWD);
//读取文件内容
byte[] bytes=Utils.getBytes(file);
//解密
byte[] decrypt=AES.decrypt(bytes);
//写到指定的目录
FileOutputStream fos=new FileOutputStream(file);
fos.write(decrypt);
fos.flush();
fos.close();
dexFiles.add(file);
}catch (Exception e){
e.printStackTrace();
}
}
}
```3. 把解密后的 dex 加载到系统
```java
private void loadDex(List dexFiles, File versionDir) throws Exception{
//1.获取pathlist
Field pathListField = Utils.findField(getClassLoader(), "pathList");
Object pathList = pathListField.get(getClassLoader());
//2.获取数组dexElements
Field dexElementsField=Utils.findField(pathList,"dexElements");
Object[] dexElements=(Object[])dexElementsField.get(pathList);
//3.反射到初始化dexElements的方法
Method makeDexElements=Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);
ArrayList suppressedExceptions = new ArrayList();
Object[] addElements=(Object[])makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions);
//合并数组
Object[] newElements= (Object[])Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+addElements.length);
System.arraycopy(dexElements,0,newElements,0,dexElements.length);
System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);
//替换 DexPathList 中的 element 数组
dexElementsField.set(pathList,newElements);
}
```解密已经完成了,下面来看看加密吧,这里为什么先说解密勒,因为 加密涉及到 签名,打包,对齐。所以留到最后讲。
### Dex 加密
1. 制作只包含解密代码的 dex
```java
1. sdk\build-tools 中执行下面命令 会得到包含 dex 的 jar
dx --dex --output out.dex in.jar
2. 通过 exec 执行
File aarFile=new File("proxy_core/build/outputs/aar/proxy_core-debug.aar");
File aarTemp=new File("proxy_tools/temp");
Zip.unZip(aarFile,aarTemp);
File classesJar=new File(aarTemp,"classes.jar");
File classesDex=new File(aarTemp,"classes.dex");
String absolutePath = classesDex.getAbsolutePath();
String absolutePath1 = classesJar.getAbsolutePath();
//dx --dex --output out.dex in.jar
//dx --dex --output //D:\Downloads\android_space\DexDEApplication\proxy_tools\temp\classes.dex //D:\Downloads\android_space\DexDEApplication\proxy_tools\temp\classes.jar
Process process=Runtime.getRuntime().exec("cmd /c dx --dex --output "+classesDex.getAbsolutePath()
+" "+classesJar.getAbsolutePath());
process.waitFor();
if(process.exitValue()!=0){
throw new RuntimeException("dex error");
}
```2. 加密 apk 中的 dex 文件
```java
File apkFile=new File("app/build/outputs/apk/debug/app-debug.apk");
File apkTemp=new File("app/build/outputs/apk/debug/temp");
Zip.unZip(apkFile,apkTemp);
//只要dex文件拿出来加密
File[] dexFiles=apkTemp.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.endsWith(".dex");
}
});
//AES加密了
AES.init(AES.DEFAULT_PWD);
for (File dexFile : dexFiles) {
byte[] bytes = Utils.getBytes(dexFile);
byte[] encrypt = AES.encrypt(bytes);
FileOutputStream fos=new FileOutputStream(new File(apkTemp,
"secret-"+dexFile.getName()));
fos.write(encrypt);
fos.flush();
fos.close();
dexFile.delete();
}
```3. 把 dex 放入 apk 加压目录,重新压成 apk 文件
```java
File apkTemp=new File("app/build/outputs/apk/debug/temp");
File aarTemp=new File("proxy_tools/temp");
File classesDex=new File(aarTemp,"classes.dex");
classesDex.renameTo(new File(apkTemp,"classes.dex"));
File unSignedApk=new File("app/build/outputs/apk/debug/app-unsigned.apk");
Zip.zip(apkTemp,unSignedApk);
```**现在可以看下加密后的文件,和未加密的文件**
未加密 apk:
![](https://user-gold-cdn.xitu.io/2019/6/2/16b18dce19cea368?w=1547&h=789&f=gif&s=307772)
加密后的 apk (现在只能看见代理 Application )
![](https://user-gold-cdn.xitu.io/2019/6/2/16b18ddbef398ad5?w=1547&h=789&f=gif&s=367894)
### 打包
#### [对齐](https://developer.android.google.cn/studio/command-line/zipalign.html)
```java
//apk整理对齐工具 未压缩的数据开头均相对于文件开头部分执行特定的字节对齐,减少应用运行内存。
zipalign -f 4 in.apk out.apk//比对 apk 是否对齐
zipalign -c -v 4 output.apk//最后提示 Verification succesful 说明对齐成功了
236829 res/mipmap-xxxhdpi-v4/ic_launcher.png (OK - compressed)
245810 res/mipmap-xxxhdpi-v4/ic_launcher_round.png (OK - compressed)
260956 resources.arsc (OK - compressed)
317875 secret-classes.dex (OK - compressed)
2306140 secret-classes2.dex (OK - compressed)
2477544 secret-classes3.dex (OK - compressed)
Verification succesful
```#### 签名打包 apksigner
```java
//sdk\build-tools\24.0.3 以上,apk签名工具
apksigner sign --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out out.apk in.apk
```## 总结
其实原理就是把主要代码通过命令 *dx* 生成 dex 文件,然后把加密后的 dex 合并在代理 class.dex 中。这样虽然还是能看见代理中的代码,但是主要代码已经没有暴露出来了,就已经实现了我们想要的效果。如果封装的好的话(JNI 中实现主要解密代码),基本上就哈也看不见了。ClassLoader 还是很重要的,热修复跟热加载都是这原理。学到这里 DEX 加解密已经学习完了,如果想看自己试一试可以参考我的代码
[代码传送阵]()
**如何学习本项目:**
![](https://devyk.oss-cn-qingdao.aliyuncs.com/blog/20200321010522.gif)