目前android app反编译工具越来越牛逼,大点的app都会有加固技术。这里简单介绍下dex加密原理.
我们可能都听说过app加固,是对我们的app加了一层壳保护,但是这个壳是神马东西呢,今天主要介绍下这个东系西。
加固分为几个部分(步骤)
1,把源apk(要加固的apk s.apk)中的dex文件加密。加密之后就不再是正常的 dex文件,那么市面上的反编译工具就不能按照
正常的dex去解析了。
2,在项目工程中实现一个application(即壳,也没有什么神秘的)用于解密dex。加密之后android系统也不认识了,要想运行就必须要 解密 。
3,把解密的dex和壳的dex合并之后替换原来BaseDexClassLoader->DexPathList->Element[] dexElements属性,dexElements可是个好东西,里面存放的是应用的dex列表。这个东西在Tinker热修复中也起了关键作用(下篇文章会详细介绍)。
4,替换application。
下面看具体实现
/** * 1、制作只包含解密代码的dex 文件 */ //1.1 解压aar 获得classes.jar File aarFile = new File("proxy-guard-core/build/outputs/aar/proxy-guard-core-debug.aar"); File aarTemp = new File("proxy-guard-tools/temp"); Zip.unZip(aarFile, aarTemp); File classesJar = new File(aarTemp, "classes.jar"); //1.2 执行dx命令 将jar变成dex文件 File classesDex = new File(aarTemp, "classes.dex"); //执行命令 windows:cmd /c linux/mac不需要(cmd /c) 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文件 */ //2.1 解压apk 获得所有的dex文件 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 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(); } /** * 3、把classes.dex 放入 apk解压目录 在压缩成apk */ classesDex.renameTo(new File(apkTemp, "classes.dex")); File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk"); Zip.zip(apkTemp, unSignedApk); /** * 4、对齐与签名 */ //4.1 对齐 // 26.0.2不认识-p参数 zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk File alignedApk = new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk"); process = Runtime.getRuntime().exec("cmd /c zipalign -f 4 " + unSignedApk .getAbsolutePath() + " " + alignedApk.getAbsolutePath()); process.waitFor(); //失败 if (process.exitValue() != 0) { throw new RuntimeException("zipalign error"); } //4.2 签名 // apksigner sign --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass // pass:别名密码 --out out.apk in.apk File signedApk = new File("app/build/outputs/apk/debug/app-signed-aligned.apk"); File jks = new File("proxy-guard-tools/proxy.jks"); process = Runtime.getRuntime().exec("cmd /c apksigner sign --ks " + jks.getAbsolutePath () + " --ks-key-alias lance --ks-pass pass:p123456 --key-pass pass:p654321 --out" + " " + signedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath()); process.waitFor(); //失败 if (process.exitValue() != 0) { throw new RuntimeException("apksigner error"); }
这样就把一个加密的apk生成了。
下边看看壳的application干了什么:
getMetaData(); //获得当前的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可以看到就是把apk中dex问价解密然后替换系统的dex列表,具体怎么做的呢: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目录 Utils.decrypt(bytes, file.getAbsolutePath()); dexFiles.add(file); } catch (Exception e) { e.printStackTrace(); } } } } else { //已经解密过了 for (File file : dexDir.listFiles()) { dexFiles.add(file); } }
private void loadDex(List和好的至此上边的1,2,3步就完成了,最后一步替换application还不知道怎么实现,网上有不少例子,也在也就之中dexFiles, File optimizedDirectory) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { /** * 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 需要适配6.x 7.x Method makeDexElements; // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT < // Build.VERSION_CODES.M) { makeDexElements = Utils.findMethod(pathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); // } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){ // makeDexElements = Utils.findMethod(pathList, "makePathElements", ArrayList.class, // File.class, ArrayList.class); // } ArrayList suppressedExceptions = new ArrayList (); 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); }
后续会补充。