Android dex加密与解密原理及其代码实现
为什么要进行apk加密?答案是避免apk被有心人反编译,窃取公司重要技术和算法等。但是要给Apk加密要如何实现呢?系统在加载类的时候都是从我们apk的dex文件中加载的。ClassLoader会去维护一个这样的dex文件数组。而我们要做的就是将原apk中的dex都加密,然后将解密部分的代码单独编程成dex文件(我们称这样的dex为壳dex)连带着加密的dex一起加到新apk中。这样新apk安装后系统就能够找到我们应用启动的入口Application了,不至于由于加密导致系统找不到应用程序入口。而在这个程序入口中我们要做的就是解密被加密的dex文件,然后重新插入到ClassLoader维护的dex文件数组中(这里就涉及到大量的反射知识)。
dex解密与解密分为以下几个步骤:
1)把源 apk(要加固的apk)中的 dex 文件加密。加密之后就不再是正常的 dex 文件,那么市面上的反编译工具就不能按照正常的dex去解析了。
2)将加密后的dex文件与壳程序Apk的dex文件合并成新的dex文件,然后替换壳程序中的源dex文件,生成新的apk文件。
3)在壳程序apk的application中进行解密源apk的dex文件。
知道了原理,下面就是代码实现了。这里有三个工程:
1)原程序项目 app module(需要加密的APK)。
2)壳项目 ApkShuck module(解密源程序APK和加载APK)。
3)对原APK进行加密和壳项目的DEX的合并项目 Encrypt module。
1. 加密实现
在Android studio 中创建一个java library module(Encrypt),此module主要用来进行dex的加密工作。
1. 在开始加密之前首先建立临时目录,在 Encrypt 下建立一个source目录,然后在 source 目录建立 apk 目录和 arr 目录,最后分别在 apk 和 arr 目录下建立一个 temp 目录。apk 目录下放置原apk文件,它的 temp 目录主要用来放置原apk解压之后的文件和原apk中dex加密后的文件。arr目录放置壳apk(其实是一个arr包),它的 temp 目录主要用来放置壳apk解压之后的文件。
2. 清理临时目录的缓存文件,代码如下所示:
public class ApkEncryptMain {
public static void main(String[] args) {
init();
}
/**
* 初始化
*/
private static void init() {
// 删除缓存
FileUtils.delFolder(new File("Encrypt/source/apk/temp"));
FileUtils.delFolder(new File("Encrypt/source/arr/temp"));
}
}
3. 加密算法实现,主要采用的是AES加密算法,代码如下:
public class EncryptUtils {
private final byte[] KEY = "QUmkLrrISiud6RPU".getBytes(); // 加密使用的key
private final byte[] IV = "eh7aJlOdHCNsGNcD".getBytes(); // 偏移值
private final String ALGORITHM = "AES/CBC/PKCS5Padding"; // 加密算法
private Cipher encryptCipher; // 加密
/**
* 使用单例
*/
private EncryptUtils() {
try {
// 初始化加密算法
encryptCipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec key = new SecretKeySpec(KEY, "AES");
encryptCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(IV));
} catch (Exception e) {
e.printStackTrace();
}
}
private static class SingletonHolder {
private static final EncryptUtils INSTANCE = new EncryptUtils();
}
public static EncryptUtils getInstance() {
return SingletonHolder.INSTANCE;
}
}
4. 解压原apk,并加密原apk中的dex文件。
public class ApkEncryptMain {
private static final String SOURCE_APK_PATH = "Encrypt/source/apk/app-debug.apk";
public static void main(String[] args) {
LogUtils.i("start encrypt");
init();
/**
* 1. 解压源apk文件到 ../source/apk/temp目录下,并加密dex文件
*/
File sourceApk = new File(SOURCE_APK_PATH);
File newApkDir = new File(sourceApk.getParent() + File.separator + "temp");
if (!newApkDir.exists()) {
newApkDir.mkdirs();
}
// 解压Apk并加密dex文件
EncryptUtils.getInstance().encryptApkFile(sourceApk, newApkDir);
}
}
public class EncryptUtils {
/**
* 加密apk
*
* @param srcApkFile 源apk文件的地址
* @param dstApkFile 新apk文件的地址
*/
public void encryptApkFile(File srcApkFile, File dstApkFile) {
if (srcApkFile == null || !srcApkFile.exists()) {
LogUtils.e("srcAPKFile not exist");
return;
}
// 解压apk到指定文件夹
ZipUtils.unZip(srcApkFile, dstApkFile);
// 获取所有的dex(可能存在分包的情况,即有多个dex文件)
File[] dexFiles = dstApkFile.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
// 提取所有的.dex文件
return s.endsWith(".dex");
}
});
if (dexFiles == null || dexFiles.length <= 0) {
LogUtils.i("this apk is invalidate");
return;
}
for (File dexFile : dexFiles) {
// 读取dex中的数据
byte[] buffer = FileUtils.getBytes(dexFile);
if (buffer != null) {
// 加密
byte[] encryptBytes = encrypt(buffer);
if (encryptBytes != null) {
//修改.dex名为_.dex,避免等会与aar中的.dex重名
int indexOf = dexFile.getName().indexOf(".dex");
String newName = dexFile.getParent() + File.separator +
dexFile.getName().substring(0, indexOf) + "_.dex";
// 写数据, 替换原来的数据
FileUtils.wirte(new File(newName), encryptBytes);
dexFile.delete();
} else {
LogUtils.e("Failed to encrypt dex data");
return;
}
} else {
LogUtils.e("Failed to read dex data");
return;
}
}
}
/**
* 加密
* @param data
* @return
*/
private byte[] encrypt(byte[] data) {
try {
return encryptCipher.doFinal(data);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public class ZipUtils {
/**
* 解压zip文件
*
* @param srcFile 需要解压的zip文件
* @param dstFile 解压后的文件
*/
public static void unZip(File srcFile, File dstFile) {
if (srcFile == null) {
LogUtils.e("unZip: srcFile is null");
return;
}
try {
ZipFile zipFile = new ZipFile(srcFile);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()){
ZipEntry zipEntry = entries.nextElement();
String name = zipEntry.getName();
if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
.equals("META-INF/MANIFEST.MF")) {
continue;
}
if(!zipEntry.isDirectory()){
File file = new File(dstFile, name);
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
InputStream is = zipFile.getInputStream(zipEntry);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
is.close();
fos.close();
}
}
zipFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. 解压arr文件,并生成壳dex。
public class ApkEncryptMain {
public static void main(String[] args) {
/**
* 2. 解压arr文件(不能进行加密的部分),将其中的dex文件拷贝到apk/temp目录中。
*/
File shuckApk = new File(SHUCK_APK_PATH);
File newShuckDir = new File(shuckApk.getParent() + File.separator + "temp");
if (!newShuckDir.exists()) {
newShuckDir.mkdirs();
}
// 解压arr文件,并将arr中的jar文件转化为dex文件
DxUtils.jar2Dex(shuckApk, newShuckDir);
// 拷贝arr中的classes.dex 到 apk/temp 目录中
File copyDstFile = new File("Encrypt/source/apk/temp/classes.dex");
FileUtils.copyFile(dstDex, copyDstFile);
}
}
public class DxUtils {
/**
* 解压arr并将jar转化为 dex
*
* @param srcFile
* @param dstFile
*/
public static void jar2Dex(File srcFile, File dstFile) {
if (srcFile == null || !srcFile.exists()) {
LogUtils.e("shuck arr file not exist");
return;
}
// 解压apk到指定文件夹
ZipUtils.unZip(srcFile, dstFile);
// 获取所有的jar
File[] jarFiles = dstFile.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
// 提取所有的.dex文件
return s.endsWith(".jar");
}
});
if (jarFiles == null || jarFiles.length <= 0) {
LogUtils.i("this arr is invalidate");
return;
}
// 一般情况下这个壳arr中只会有一个classes.jar文件,这里classes_jar就是classes.jar文件
File classes_jar = jarFiles[0];
// 将classes_jar 转为为 classes.dex
File dstDex = new File(classes_jar.getParent() + File.separator + "classes.dex");
// 使用 android tools 里面的dx.bat 命令将 jar 转化为 dex
dxCommand(classes_jar, dstDex);
}
private static void dxCommand(File jarFile, File dexFile) {
// 这里使用的是dx.bat的绝对路径.请根据实际情况填写。
String command = "cmd.exe /C E:\\development_tools\\android_sdk\\android_sdk\\build-tools\\29.0.2\\dx --dex --output=" + dexFile.getAbsolutePath()
+ " " + jarFile.getAbsolutePath();
CmdUtils.execCommand(command);
}
}
public class CmdUtils {
public static void execCommand(String command) {
Runtime runtime = Runtime.getRuntime();
Process process = null;
BufferedReader success_buffer = null;
BufferedReader error_buffer = null;
try {
process = runtime.exec(command);
String line;
success_buffer = new BufferedReader(new InputStreamReader(process.getInputStream()));
while ((line = success_buffer.readLine()) != null) {
LogUtils.i(line);
}
process.waitFor();
if (process.exitValue() != 0) {
LogUtils.e("exec fail --- " + command);
error_buffer = new BufferedReader(new InputStreamReader(process.getErrorStream()));
while ((line = error_buffer.readLine()) != null) {
LogUtils.e(line);
}
} else {
LogUtils.i("exec success --- " + command);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (process != null) {
process.destroy();
process = null;
}
if (success_buffer != null) {
try {
success_buffer.close();
success_buffer = null;
} catch (IOException e) {
e.printStackTrace();
}
}
if (error_buffer != null) {
try {
error_buffer.close();
error_buffer = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
7. 打包apk/temp目录生成新的未签名的apk文件
/**
* 特别注意!!!
* 这里一定要用"/" 千万不要用File.separator
* 因为这里是java 工程,它运行在 windows环境,在Windows环境下 File.separator 获取的是 "\"
* 而在 Android 系统中 File.separator 获取的是 "/"
* 因此在这里如果使用 File.separator 时获取的 "\" 在 android中识别不了,将会导致程序运行不起来。
*
*/
public class ApkEncryptMain {
private static final String SOURCE_APK_PATH = "Encrypt/source/apk/app-debug.apk";
private static final String SHUCK_APK_PATH = "Encrypt/source/arr/ApkShuck-release.aar";
public static void main(String[] args) {
/**
* 3. 打包apk/temp目录生成新的未签名的apk文件
*/
File unsignedApk = new File("Encrypt/result/apk-unsigned.apk");
unsignedApk.getParentFile().mkdirs();
unsignedApk.delete();
ZipUtils.zip(newApkDir, unsignedApk);
}
}
public class ZipUtils {
/**
* 压缩
* @param sourceFile
* @param zipFile
*/
public static void zip(File sourceFile, File zipFile) {
if (sourceFile == null) {
LogUtils.e("The original file that needs to be compressed does not exist");
return;
}
zipFile.delete();
// 对输出文件做CRC32校验
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()));
compress(sourceFile, zos, "");
zos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (zos != null) {
try {
zos.close();
zos = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static void compress(File srcFile, ZipOutputStream zos, String dir) throws IOException {
if (srcFile.isDirectory()) {
File[] files = srcFile.listFiles();
for (File file : files) {
/**
* 特别注意!!!
* 这里一定要用"/" 千万不要用File.separator
* 因为这里是java 工程,它运行在 windows环境,在Windows环境下 File.separator 获取的是 "\"
* 而在 Android 系统中 File.separator 获取的是 "/"
* 因此在这里如果使用 File.separator 时获取的 "\" 在 android中识别不了,将会导致程序运行不起来。
*
*/
compress(file, zos, dir + srcFile.getName() + "/");
}
} else {
compressFile(srcFile, zos, dir);
}
}
private static void compressFile(File file, ZipOutputStream zos, String dir) throws IOException {
// temp/classes.dex
String fullName = dir + file.getName();
// 需要去掉temp
String[] dirNames = fullName.split("/");
// 正确的文件目录名(去掉了temp)
StringBuffer sb = new StringBuffer();
if (dirNames.length > 1) {
for (int i = 1; i < dirNames.length; i++) {
sb.append("/");
sb.append(dirNames[i]);
}
} else {
sb.append("/");
}
ZipEntry entry = new ZipEntry(sb.toString().substring(1));
zos.putNextEntry(entry);
FileInputStream fis = new FileInputStream(file);
int count;
byte[] bytes = new byte[1024];
while ((count = fis.read(bytes, 0, 1024)) != -1) {
zos.write(bytes, 0, count);
}
fis.close();
zos.closeEntry();
}
}
8. 对齐
public class ApkEncryptMain {
public static void main(String[] args) {
/**
* 4. 对齐
*/
File unAlignApk = new File("Encrypt/result/apk-unAlign.apk");
unAlignApk.getParentFile().mkdirs();
unAlignApk.delete();
ZipUtils.zipalign(unsignedApk, unAlignApk);
}
}
public class ZipUtils{
/**
* 对齐
*
* @param unsignedApk
* @param unAlignApk
*/
public static void zipalign(File unsignedApk, File unAlignApk) {
if (unsignedApk == null || !unsignedApk.exists()) {
LogUtils.e("The APK that needs to be align does not exist");
return;
}
// 这里使用的是zipalign.ext的绝对路径.请根据实际情况填写。
String command = "cmd.exe /C E:\\development_tools\\android_sdk\\android_sdk\\build-tools\\29.0.2\\zipalign -v -p 4 " + unsignedApk.getAbsolutePath()
+ " " + unAlignApk.getAbsolutePath();
CmdUtils.execCommand(command);
}
}
9. 给新的未签名并且已经对齐的apk签名
public class ApkEncryptMain {
public static void main(String[] args) {
/**
* 5 .给新apk添加签名,生成签名apk
*/
File signedApk = new File("Encrypt/result/apk-signed.apk");
signedApk.getParentFile().mkdirs();
signedApk.delete();
SignUtils.signature(unAlignApk, signedApk);
}
}
public class SignUtils {
public static void signature(File unSignApk, File signApk) {
if (unSignApk == null || !unSignApk.exists()) {
LogUtils.e("The APK that needs to be signed does not exist");
return;
}
String command = "cmd.exe /C jarsigner -sigalg SHA1withRSA -digestalg SHA1 " +
"-keystore C:/Users/Administrator/.android/debug.keystore -storepass android -keypass android " +
"-signedjar " + signApk.getAbsolutePath() + " " + unSignApk.getAbsolutePath() + " androiddebugkey";
CmdUtils.execCommand(command);
}
}
到此dex加密过程就完成了,生成的apk-signed.apk就是签名了的apk,可以直接安装使用。
2. 解密实现
要完成解密,我们需要完成如下几个步骤:
1)找到合适的解密时机。
2)壳 dex 并没有被加密,需要排除在解密的 dex 文件之外。
3)解密后的 dex 文件需要重新插入到 ClassLoader 中,这与热修复思想是一样的。
1. 解密时机
作为一个被加密的应用,安装的时候我们应用本身是无法控制。所以应用第一次启动的时候就成了我们最佳的解密时机了。 所以我们将解密的逻辑放到Application的attachBaseContext()方法中。
2. 解压apk、脱壳并解密被加密的原apk中的 dex
说明: 这里只是为了实现功能而将解密的代码用java实现了,而这部分代码没有被加密,所以还是很容易被反编译查看到解密方法,这样被加密的dex也很容易被破解,因此最好的方案就是把加密和解密代码用JNI方式实现,这样即使人家反编译壳apk的dex文件也没有办法知道加密和解密方法,也就无法破解原apk的dex文件了。
/**
* 解压apk并解密被加密了的dex文件
*
* @param apkFile 被加密了的 apk 文件
* @param app 存放解压和解密后的apk文件目录
*/
private void unZipAndDecryptDex(File apkFile, File app) {
if (!app.exists() || app.listFiles().length == 0) {
// 当app文件不存在,或者 app 文件是一个空文件夹是需要解压。
// 解压apk到指定目录
ZipUtils.unZip(apkFile, app);
// 获取所有的dex
File[] dexFiles = app.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
// 提取所有的.dex文件
return s.endsWith(".dex");
}
});
if (dexFiles == null || dexFiles.length <= 0) {
LogUtils.i("this apk is invalidate");
return;
}
for (File file : dexFiles) {
if (file.getName().equals("classes.dex")) {
/**
* 我们在加密的时候将不能加密的壳dex命名为classes.dex并拷贝到新apk中打包生成新的apk中了。
* 所以这里我们做脱壳,壳dex不需要进行解密操作。
*/
} else {
/**
* 加密的dex进行解密,对应加密流程中的_.dex文件
*/
byte[] buffer = FileUtils.getBytes(file);
if (buffer != null) {
// 解密
byte[] decryptBytes = EncryptUtils.getInstance().decrypt(buffer);
if (decryptBytes != null) {
//修改.dex名为_.dex,避免等会与aar中的.dex重名
int indexOf = file.getName().indexOf(".dex");
String newName = file.getParent() + File.separator +
file.getName().substring(0, indexOf) + "new.dex";
// 写数据, 替换原来的数据
FileUtils.wirte(new File(newName), decryptBytes);
file.delete();
} else {
LogUtils.e("Failed to encrypt dex data");
return;
}
} else {
LogUtils.e("Failed to read dex data");
return;
}
}
}
}
}
3. 将解密后的dex文件重新插入dexElements数组中。在这个过程中需要对不同的版本做处理。
public class LoaderDexUtils {
public static void loader(ClassLoader loader, ArrayList<File> dexList, File dir) {
try {
/**
* 1. 通过反射找到BaseDexClassLoader中的pathList属性,pathList是DexPathList类型的对象。
* DexPathList中维护了一个dex文件数组(dexElements数组),ClassLoader加载类的时候就会从这dex数组中去查找。
* 我们需要将解密出来的dex重新插入到这个数组里面。
*/
// 这里的loader是PathClassLoader,PathClassLoader继承自BaseDexClassLoader
Class<?> baseDexClassLoaderClass = loader.getClass().getSuperclass();
Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(loader);
/**
* 2. 创建我们自己的dex文件数组,可查看源码中的makeDexElements方法
*/
ArrayList suppressedExceptions = new ArrayList();
Class<?> dexPathListClass = pathList.getClass();
Object[] elements = null;
if (Build.VERSION.SDK_INT >= 24) {
Method makeDexElementsMethod = dexPathListClass.getDeclaredMethod("makeDexElements", List.class, File.class, List.class, ClassLoader.class);
makeDexElementsMethod.setAccessible(true);
elements = (Object[]) makeDexElementsMethod.invoke(pathList, dexList, dir, suppressedExceptions, loader);
} else if (Build.VERSION.SDK_INT >= 23) {
Method makeDexElementsMethod = dexPathListClass.getDeclaredMethod("makePathElements", List.class, File.class, List.class);
makeDexElementsMethod.setAccessible(true);
elements = (Object[]) makeDexElementsMethod.invoke(pathList, dexList, dir, suppressedExceptions);
} else {
Method makeDexElementsMethod = dexPathListClass.getDeclaredMethod("makeDexElements", ArrayList.class, File.class, ArrayList.class);
makeDexElementsMethod.setAccessible(true);
elements = (Object[]) makeDexElementsMethod.invoke(pathList, dexList, dir, suppressedExceptions);
}
if (elements == null) {
LogUtils.e("makeDexElements fail");
return;
}
/**
* 3. 将解密后的dex文件插入到DexPathList的dexElements数组中。
*/
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object[] oldDexElements = (Object[]) dexElementsField.get(pathList);
Object[] newDexElements = (Object[]) (Array.newInstance(oldDexElements.getClass()
.getComponentType(), oldDexElements.length + elements.length));
System.arraycopy(oldDexElements, 0, newDexElements, 0, oldDexElements.length);
System.arraycopy(elements, 0, newDexElements, oldDexElements.length, elements.length);
dexElementsField.set(pathList, newDexElements);
// 异常处理
if (suppressedExceptions.size() > 0) {
Iterator iterator = suppressedExceptions.iterator();
while (iterator.hasNext()) {
IOException dexElementsSuppressedExceptions = (IOException)
iterator.next();
Log.w("MultiDex", "Exception in makeDexElement",
dexElementsSuppressedExceptions);
}
Field suppressedExceptionsField = dexPathListClass.getDeclaredField("dexElementsSuppressedExceptions");
suppressedExceptionsField.setAccessible(true);
IOException[] dexElementsSuppressedExceptions = (IOException[])
suppressedExceptionsField.get(pathList);
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions = (IOException[]) suppressedExceptions
.toArray(new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined = new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
dexElementsSuppressedExceptions = combined;
}
suppressedExceptionsField.set(pathList, dexElementsSuppressedExceptions);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4. 替换Application,使用原Apk中的Application替换壳中代理Application。
public class ProxyApplication extends Application {
private String applicationName;
private boolean isRestoreRealApp;
private Application realApp;
@Override
public void onCreate() {
super.onCreate();
// 替换真实的Application,不然壳的入侵性太强,而且原apk的Application不能运行。
restoreRealApp();
}
private void restoreRealApp() {
if (isRestoreRealApp) {
return;
}
if (TextUtils.isEmpty(applicationName)) {
return;
}
try {
// 得到 attachBaseContext(context) 传入的上下文 ContextImpl
Context baseContext = getBaseContext();
// 拿到真实 APK Application 的 class
Class<?> realAppClass = Class.forName(applicationName);
// 反射实例化,其实 Android 中四大组件都是这样实例化的。
realApp = (Application) realAppClass.newInstance();
// 得到 Application attach() 方法 也就是最先初始化的
Method attach = Application.class.getDeclaredMethod("attach", Context.class);
attach.setAccessible(true);
//执行 Application#attach(Context)
//将真实的 Application 和假的 Application 进行替换。想当于自己手动控制 真实的 Application 生命周期
attach.invoke(realApp, baseContext);
// ContextImpl---->mOuterContext(app) 通过Application的attachBaseContext回调参数获取
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
// 获取 mOuterContext 属性
Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
mOuterContextField.setAccessible(true);
mOuterContextField.set(baseContext, realApp);
//拿到 ActivityThread 变量
Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
mMainThreadField.setAccessible(true);
// 拿到 ActivityThread 对象
Object mMainThread = mMainThreadField.get(baseContext);
// ActivityThread--->>mInitialApplication
// 反射拿到 ActivityThread class
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
// 得到当前加载的 Application 类
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
// 将 ActivityThread 中的 mInitialApplication 替换为 真实的 Application 可以用于接收相应的声明周期和一些调用等
mInitialApplicationField.set(mMainThread, realApp);
// ActivityThread--->mAllApplications(ArrayList) ContextImpl的mMainThread属性
// 拿到 ActivityThread 中所有的 Application 集合对象
Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
mAllApplicationsField.setAccessible(true);
ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
// 删除 ProxyApplication
mAllApplications.remove(this);
// 添加真实的 Application
mAllApplications.add(realApp);
// LoadedApk------->mApplication 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);
//将 LoadedApk 中的 mApplication 替换为 真实的 Application
mApplicationField.set(mPackageInfo, realApp);
//修改ApplicationInfo className LooadedApk
// 拿到 LoadApk 中的 mApplicationInfo 变量
Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
mApplicationInfoField.setAccessible(true);
ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
// 将我们真实的 Application ClassName 名称赋值于它
mApplicationInfo.className = applicationName;
// 执行真实 Application onCreate 声明周期
realApp.onCreate();
//解码完成
isRestoreRealApp = true;
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String getPackageName() {
if (!TextUtils.isEmpty(applicationName)) {
return "";
}
return super.getPackageName();
}
/**
* 这个函数是如果在 AndroidManifest.xml 中定义了 ContentProvider 那么就会执行此处 : installProvider,简介调用该函数
*
* @param packageName
* @param flags
* @return
* @throws PackageManager.NameNotFoundException
*/
@Override
public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
if (TextUtils.isEmpty(applicationName)) {
return super.createPackageContext(packageName, flags);
}
try {
restoreRealApp();
} catch (Exception e) {
e.printStackTrace();
}
return realApp;
}
}
3. 测试
从上面的测试过程可以看出,加密dex之后再解密dex可以正常运行,然后加密过后的dex是看不到内容的,而没有加密的dex是可以看得到里面的内容的。
1. 我们在原apk中使用Application和四大组件,验证application是否替换成功。
1)Application
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
LogUtils.i("MyApplication onCreate()");
}
}
2)Activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogUtils.i("activity:" + getApplication());
LogUtils.i("activity:" + getApplicationContext());
LogUtils.i("activity:" + getApplicationInfo().className);
startService(new Intent(this, MyService.class));
Intent intent = new Intent("com.lx.source_receiver");
intent.setComponent(new ComponentName(getPackageName(), MyBroadCastReceiver.class.getName()));
sendBroadcast(intent);
getContentResolver().delete(Uri.parse("content://com.lx.source.MyProvider"), null,
null);
}
}
3) BroadcastReceiver
public class MyBroadCastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
LogUtils.i("receiver:" + context);
LogUtils.i("receiver:" + context.getApplicationContext());
LogUtils.i("receiver:" + context.getApplicationInfo().className);
}
}
4)Service
public class MyService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
LogUtils.i("service:" + getApplication());
LogUtils.i("service:" + getApplicationContext());
LogUtils.i("service:" + getApplicationInfo().className);
}
}
5)ContentProvider
public class MyProvider extends ContentProvider {
@Override
public boolean onCreate() {
LogUtils.i("provider onCreate:" + getContext());
LogUtils.i("provider onCreate:" + getContext().getApplicationContext());
LogUtils.i("provider onCreate:" + getContext().getApplicationInfo().className);
return true;
}
...
}
运行日志如下:
由以上日志可以看出,我们将代理Application成功替换为原Apk中真是的Application。
BroadcastReceiver中的context是系统又对context进行了封装,将context封装为ReceiverRestrictedContext的对象,目的是防止在onReceive方法中不能再注册广播,源码解析如下
frameworks/base/core/java/android/app/ActivityThread.java
private void handleReceiver(ReceiverData data) {
...
try {
receiver = (BroadcastReceiver)cl.loadClass(component).newInstance(); // ... 1
} catch (Exception e) {
if (DEBUG_BROADCAST) Slog.i(TAG,
"Finishing failed broadcast to " + data.intent.getComponent());
data.sendFinished(mgr);
throw new RuntimeException(
"Unable to instantiate receiver " + component
+ ": " + e.toString(), e);
}
try {
...
sCurrentBroadcastIntent.set(data.intent);
receiver.setPendingResult(data);
receiver.onReceive(context.getReceiverRestrictedContext(),
data.intent);
} catch (Exception e) {
if (DEBUG_BROADCAST) Slog.i(TAG,
"Finishing failed broadcast to " + data.intent.getComponent());
data.sendFinished(mgr);
if (!mInstrumentation.onException(receiver, e)) {
throw new RuntimeException(
"Unable to start receiver " + component
+ ": " + e.toString(), e);
}
} finally {
sCurrentBroadcastIntent.set(null);
}
if (receiver.getPendingResult() != null) {
data.finish();
}
}
在注释1处创建BroadcastReceiver的对象receiver,在注释2处调用receiver的onReceive方法,注意调用onReceive中传递context对象时,调用了context.getReceiverRestrictedContext()方法得到就是ReceiverRestrictedContext对象。
4. 总结
市面上Apk加固中原Apk项目(app)并不会依赖于壳项目(ApkShuck),但是在上面的流程中我们在app项目是依赖了ApkShuck。
下面来说明一下如何让app不依赖ApkShuck,这样ApkShuck和Encrypt(dex加密项目)更具有通用性,能够跟市面上的apk加固软件一样使用,思路如下
apk加固流程与上面将的一样,原app的AndroidManifest.xml使用的还是app中的Application。只不过我们在解压原apk的过程中增加一个操作,就是对原app的AndroidManifest.xml进行解析,然后将其修改,
1)修改Application节点的name属性为壳apk的代理ProxyApplication。
2)在Application节点中增加 <meta-data> 标签,例如:<meta-data android:name="application_name" android:value="com.lx.source.App"/>
具体解析修改AndroidManifest.xml的操作省略,有兴趣的同学可以尝试去做一下。这里就只说明实现原理。