性能优化06-多dex加密
dex加密是为了提高apk的安全性,保护源码。
一、dex加密的原理
使用加密库对apk的所有dex文件加密,然后把加密库打包成dex文件,把它和apk的dex文件放在一起重新打包成apk文件。这样apk的原dex文件就无法被反编译了。
apk的原dex文件加密后,Android系统也无法解析,所以需要先对其解密。由于app运行时最先启动Applicaiton,所以需要设置一个代理Applicaiton,在代理Applicaiton做解密操作。解密完成后,使用hook技术,替换真正的Applicaiton即可。
下面介绍本人设计的dex加密框架。
二、使用说明
使用时,需要先使用加密框架加密dex:
- 在app的build.gradle中引入加密库Tool
- 编译加密库Tool,获得加密库Tool的aar文件和app的apk文件
- 在EncryptAndSigin的main()中手动修改initKeyStore(),来配置签名文件。注意:配置环境变量后,需要重启AS。
- 执行EncryptAndSigin的main().会在app/build/outputs/apk/debug/下生成签名过的apk,即为加密的apk
然后,配置代理的Applicaiton:
- 在app的清单文件注册ProxyApp
- 添加额外数据:app_name(表示真实Applicaiton的全类名)和app_version(表示app的版本,使用int值)
三、dex加密的实现
dex加密的实现分为四步:dex分包、加密apk、替换Applicaiton、解密apk。
1.dex分包
当APP的方法数超过65536时,一般采用dex分包来解决。如果不需要分包,就跳过此步骤。
系统支持使用multidex分包。Android5.0以下没有multidex库,所以需要手动引入。
android {
defaultConfig {
multiDexEnabled true
}
}
dependencies {
compile 'com.android.support:multidex:1.0.0'
}
2.加密apk
加密apk,不仅要对apk的dex文件加密,还需要将加密ku打包成dex文件,并与apk文件一起打包成新的apk。
制作加密库的dex 文件
先将加密库编译成aar文件,然后对其解压得到classes.jar,再通过dx命令将其打包成dex文件。
private static File unZipAar() throws IOException, InterruptedException {
File aarFile = new File(dexPath + "/build/outputs/aar/" + dexPath + "-debug.aar");
File aarTemp = new File(dexPath + "/temp");
//解压aar 获得classes.jar
Zip.unZip(aarFile, aarTemp);
File classesJar = new File(aarTemp, "classes.jar");
//执行dx命令 将jar变成dex文件
File classesDex = new File(aarTemp, "classes.dex");
String cmd = "cmd /c dx --dex --output " + classesDex.getAbsolutePath() + " " + classesJar.getAbsolutePath();
//执行cmd命令。1.windows中需要以cmd /c开头,linux/mac不需要。2.需要把dx添加环境变量,并重启AS。
exec(cmd);
return classesDex;
}
加密apk中所有dex文件
解压apk,得到所有的dex文件,然后使用AES加密。
private static File encryptDex() throws Exception {
//2.1 解压apk 获得所有的dex文件
File apkFile = new File(appPath + "/build/outputs/apk/debug/app-debug.apk");
File apkTemp = new File(appPath + "/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 dex : dexFiles) {
//读取文件数据
byte[] bytes = getBytes(dex);
//加密
byte[] encrypt = AES.encrypt(bytes);
//写到指定目录
FileOutputStream fos = new FileOutputStream(new File(apkTemp, "secret-" + dex.getName()));
fos.write(encrypt);
fos.flush();
fos.close();
dex.delete();
}
return apkTemp;
}
把classes.dex 放入 apk解压目录 在压缩成apk
将加密库的dex文件和apk的dex文件打包成新的apk,即为加密后的apk。
private static File zipDex(File classesDex, File apkTemp) throws Exception {
classesDex.renameTo(new File(apkTemp, "classes.dex"));
File unSignedApk = new File(appPath +"/build/outputs/apk/debug/app-unsigned.apk");
Zip.zip(apkTemp, unSignedApk);
return unSignedApk;
}
对齐并签名apk
先对加密后的apk对齐,压缩其体积,然后再进行签名。
private static void siginApk(File unSignedApk) throws IOException, InterruptedException {
//对齐apk文件:压缩apk体积。26.0.2不认识-p参数 zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
File alignedApk = new File(appPath + "/build/outputs/apk/debug/app-unsigned-aligned.apk");
String cmd = "cmd /c zipalign -f 4 " + unSignedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath();
exec(cmd);
//签名:apksigner sign --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out out.apk in.apk
File signedApk = new File(appPath + "/build/outputs/apk/debug/app-signed-aligned.apk");
File jks = new File(dexPath + "/src/main/java/gsw/dex/jks/proxy.jks");
//注意:apksigner工具在build-tools26.0.0中没有,可以用26.0.3的
String cmd2 = "cmd /c apksigner sign --ks " + jks.getAbsolutePath() + " --ks-key-alias lance --ks-pass pass:p123456 --key-pass " +
"pass:p654321 --out" + " " + signedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath();
exec(cmd2);
//删除临时文件
File aarTemp = new File(dexPath + "/temp");
Utils.deleteDir(aarTemp);
File apkTemp = new File(appPath + "/build/outputs/apk/debug/temp");
Utils.deleteDir(apkTemp);
//删除未签名的文件
Utils.deleteDir(unSignedApk);
//删除对齐的文件
if (signedApk.exists()) {
Utils.deleteDir(alignedApk);
}
}
3.替换Applicaiton
由于使用了代理的Applicaiton,所以需要先从清单文件从获取真实Applicaiton的全类名,然后通过hook技术替换真实的Applicaiton。
获取真实Applicaiton的信息
通过在清单配置的meta-data,获取app_name(表示真实Applicaiton的全类名)和app_version(表示app的版本)。
public void getMetaData() {
try {
ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo
(getPackageName(), PackageManager.GET_META_DATA);
Bundle metaData = applicationInfo.metaData;
//是否设置app_name 与 app_version
if (null != metaData) {
//是否存在name为app_name的meta-data数据
if (metaData.containsKey("app_name")) {
app_name = metaData.getString("app_name");
}
if (metaData.containsKey("app_version")) {
app_version = metaData.getString("app_version");
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
替换真实的Applicaiton
首先根据全类名生成真实Applicaiton的实例,然后通过hook技术替换真实的Applicaiton。由于ContextImpl、ActivityThread、LoadedApk都持有Applicaiton的引用,所以需要进行3个替换。
private void bindRealApplication() throws Exception {
if (isBindReal) {
return;
}
//如果用户(使用这个库的开发者) 没有配置Application 就不用管了
if (TextUtils.isEmpty(app_name)) {
return;
}
//这个就是attachBaseContext传进来的 ContextImpl
Context baseContext = getBaseContext();
//反射创建出真实的 用户 配置的Application
Class<?> delegateClass = Class.forName(app_name);
delegate = (Application) delegateClass.newInstance();
//反射获得 attach函数
Method attach = Application.class.getDeclaredMethod("attach", Context.class);
//设置允许访问
attach.setAccessible(true);
attach.invoke(delegate, baseContext);
/**
* 1.替换ContextImpl:ContextImpl -> mOuterContext ProxyApp->MyApplication
*/
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
//获得 mOuterContext 属性
Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
mOuterContextField.setAccessible(true);
mOuterContextField.set(baseContext, delegate);
/**
* 2.替换ActivityThread: mAllApplications 与 mInitialApplication
*/
//获得ActivityThread对象 ActivityThread 可以通过 ContextImpl 的 mMainThread 属性获得
Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
mMainThreadField.setAccessible(true);
Object mMainThread = mMainThreadField.get(baseContext);
//替换 mInitialApplication
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field mInitialApplicationField = activityThreadClass.getDeclaredField
("mInitialApplication");
mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(mMainThread, delegate);
//替换 mAllApplications
Field mAllApplicationsField = activityThreadClass.getDeclaredField
("mAllApplications");
mAllApplicationsField.setAccessible(true);
ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
mAllApplications.remove(this);
mAllApplications.add(delegate);
/**
* 3.替换LoadedApk:LoadedApk -> mApplication ProxyApp
*/
//LoadedApk 可以通过 ContextImpl 的 mPackageInfo 属性获得
Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
mPackageInfoField.setAccessible(true);
Object mPackageInfo = mPackageInfoField.get(baseContext);
Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
mApplicationField.setAccessible(true);
mApplicationField.set(mPackageInfo, delegate);
//修改ApplicationInfo className LoadedApk
Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
mApplicationInfoField.setAccessible(true);
ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
mApplicationInfo.className = app_name;
delegate.onCreate();
isBindReal = true;
}
4.解密dex
解密dex
先把加密后的dex使用AES进行解密,然后使用hook技术添加到系统的dexElements中,让其生效。
private void decryptApk() {
//获得当前的apk文件
File apkFile = new File(getApplicationInfo().sourceDir);
//apk zip 解压到 appDir这个目录 /data/data/packagename/
File versionDir = getDir(app_name + "_\\" + app_version, MODE_PRIVATE);
File appDir = new File(versionDir, "app");
//提取apk中 需要解密的所有dex放入到这个目录
File dexDir = new File(appDir, "dexDir");
//需要我们加载的dex
List<File> dexFiles = new ArrayList<>();
//需要解密 (MD5 文件校验)
if (!dexDir.exists() || dexDir.list().length == 0) {
//把apk解压 到 appDir
Zip.unZip(apkFile, appDir);
//获取目录下的所有文件
File[] files = appDir.listFiles();
for (File file : files) {
String name = file.getName();
//文件名是 .dex结尾, 并且不是主dex 放入 dexDir 目录
if (name.endsWith(".dex") && !TextUtils.equals(name, "classes.dex")) {
try {
//从文件中读取 byte数组 加密后的dex数据
byte[] bytes = Utils.getBytes(file);
//将dex 文件 解密 并且写入 原文件file目录
AES.init(AES.DEFAULT_PWD);
bytes = AES.decrypt(bytes);
FileOutputStream fos = new FileOutputStream(file);
fos.write(bytes);
fos.close();
dexFiles.add(file);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//已经解密过了
} else {
for (File file : dexDir.listFiles()) {
dexFiles.add(file);
}
}
try {
loadDex(dexFiles, versionDir);
} catch (Exception e) {
e.printStackTrace();
}
}
加载dex
先获取系统 classloader中的dexElements数组,然后把解密后的dex添加到前面。
private void loadDex(List<File> dexFiles, File optimizedDirectory) throws
NoSuchFieldException, IllegalAccessException, NoSuchMethodException,
InvocationTargetException {
getClassLoader();
/**
* 1.获得 系统 classloader中的dexElements数组
*/
//1.1 获得classloader中的pathList => DexPathList
Field pathListField = Utils.findField(getClassLoader(), "pathList");
Object pathList = pathListField.get(getClassLoader());
//1.2 获得pathList类中的 dexElements
Field dexElementsField = Utils.findField(pathList, "dexElements");
Object[] dexElements = (Object[]) dexElementsField.get(pathList);
/**
* 2.创建新的 element 数组 -- 解密后加载dex
*/
//5.x 需要做版本兼容
Method makeDexElements = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
makeDexElements = Utils.findMethod(pathList, "makeDexElements", List.class, File.class, List.class);
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
makeDexElements = Utils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
makeDexElements = Utils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);
}
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
Object[] addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,
optimizedDirectory,
suppressedExceptions);
/**
* 3.合并两个数组
*/
//创建一个数组
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);
/**
* 4.替换classloader中的 element数组
*/
dexElementsField.set(pathList, newElements);
}
最后
代码地址:https://gitee.com/yanhuo2008/Common/tree/master/ToolDex
性能优化专题:https://www.jianshu.com/nb/25128595
喜欢请点赞,谢谢!
网友评论