前言
为什么要加固
对APP进行加固,可以有效防止移动应用被破解、盗版、二次打包、注入、反编译等,保障程序的安全性、稳定性。
常见的加固方案有很多,本文主要介绍如果通过对dex文件进行加密来达到apk加固的目的;
APK加固整体思路
加固整体思路:
先解压apk文件,取出dex文件,对dex文件进行加密,然后组合壳中的dex文件(Android类加载机制),结合之前的apk资源(解压apk除dex以外的其他资源,如manifest、res等),打包新的apk文件,并对新的apk文件进行对齐、签名。
从上述流程可以看出,我们需要清楚的知道apk打包流程;
具体实现
理解了上述APK加固思路,我们就可以按照思路进行对应代码实现;
项目整体结构如下:
- 新建
shell
模块,编写apk壳代码;
这里主要是增加代理ProxyApplication
,重写Application中的attachBaseContext
(app运行起来最先执行的方法)方法,主要做如下几件事:
- dex文件解密;
- 反射加载解密后的dex文件;
- 反射ActivityThread流程,替换真实的Application(源dex中的application);
public class ProxyApplication extends Application {
private String applicationName;
private boolean isRestoreRealApp;
private Application realApp;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PackageManager packageManager = base.getPackageManager();
PackageInfo packageInfo = null;
try {
packageInfo = packageManager.getPackageInfo(this.getPackageName(), 0);
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
Bundle metaData = applicationInfo.metaData;
if (metaData.containsKey("application_name")) {
applicationName = metaData.getString("application_name");
}
//应用最后一次更新时间
long lastUpdateTime = packageInfo.lastUpdateTime;
// 获取当前应用的apk文件
File apkFile = new File(getApplicationInfo().sourceDir);
// 在应用私有存储空间创建一个存放解压apk后的文件地址。
File unZipFile = getDir("fake_apk", MODE_PRIVATE);
/**
* 这里根据 "app_" + 应用最后一次更新的时间 作为解压的文件目录
* 作用: 应用每更新一次时,我们都需要重新解压apk文件。
* 当应用没有更新是,如果apk已经解压就不需要再次解压,加快第二次启动的时间。
*
*/
File app = new File(unZipFile, "app_" + lastUpdateTime);
unZipAndDecryptDex(apkFile, app);
// 存放所有的dex文件
ArrayList<File> dexList = new ArrayList<>();
for (File file : app.listFiles()) {
if (file.getName().endsWith(".dex")) {
dexList.add(file);
}
}
LogUtils.i(dexList.toString());
// 注意这里通过getClassLoader()获取的ClassLoader是PathClassLoader,而PathClassLoader是
// BaseDexClassLoader的子类。
LoaderDexUtils.loader(getClassLoader(), dexList, unZipFile);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
/**
* 解压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;
}
}
}
}
}
@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;
}
}
- 打包shell模块成aar文件;
- 编写App模块代码并打包apk文件;
App模块主要修改启动application为代理ProxyApplication
,并依赖shell模块;
- 新建
encrypt
模块,编写dex加密、打包apk流程方法;
object ApkEncryptMain {
//源apk存放路径
private const val SOURCE_APK_PATH = "encrypt/source/apk/app-debug.apk"
//壳aar文件存放路径
private const val SHELL_APK_PATH = "encrypt/source/arr/shell-release.aar"
@JvmStatic
fun main(args: Array<String>) {
LogUtils.i("start encrypt")
init()
//解压源apk文件到../source/apk/temp目录下,并加密dex文件
val sourceApk = File(SOURCE_APK_PATH)
val newApkDir = File(sourceApk.parent + File.separator + "temp")
if (!newApkDir.exists()) {
newApkDir.mkdirs()
}
//解压apk并加密dex文件
EncryptUtils.getInstance().encryptApkFile(sourceApk, newApkDir)
//解压arr文件(不加密的部分),并将其中的dex文件拷贝到apk/temp目录下
val shellApk = File(SHELL_APK_PATH)
val newShellDir = File(shellApk.parent + File.separator + "temp")
if (!newShellDir.exists()){
newShellDir.mkdirs()
}
//解压aar文件,并将aar中的jar文件转换为dex文件,然后拷贝aar中的classes.dex到apk/temp目录下
DxUtils.jar2Dex(shellApk,newShellDir)
//3.打包apk/temp目录生成新的未签名的apk文件
val unsignedApk = File("encrypt/result/apk-unsigned.apk")
unsignedApk.parentFile.mkdirs()
unsignedApk.delete()
ZipUtils.zip(newApkDir,unsignedApk)
//4.对齐
val unAlignApk = File("encrypt/result/apk-unAlign.apk")
unAlignApk.parentFile.mkdirs()
unAlignApk.delete()
ZipUtils.zipalign(unsignedApk,unAlignApk)
//5.给新的apk添加签名,生成签名apk
val signedApk = File("encrypt/result/apk-signed.apk")
signedApk.parentFile.mkdirs()
signedApk.delete()
SignUtils.signature(unAlignApk,signedApk)
}
//初始化
private fun init() {
FileUtils.deleteFolder("encrypt/source/apk/temp")
FileUtils.deleteFolder("encrypt/source/arr/temp")
}
}
- 将源apk和壳aar文件拷贝到
SOURCE_APK_PATH
和SHELL_APK_PATH
对应目录,执行Main方法
,最终产物如图:
我们解压apk-signed.apk
,点开dex文件,可以看到如图:
证明我们dex加密成功!!
备注:
shell壳模块没有使用kotlin编写代码原因:kotlin项目的apk加固后安装会崩溃
代码链接
结语
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )