安卓逆向|019 Android加固之APK加固的原理和实现


文章目录

    • 前言
    • 加载Activity遇到的问题
      • APK的启动过程
      • 替换ClassLoader流程
        • 获取ActivityThread类对象
        • 获取AppBindData类对象mBoundApplication
        • 获取LoadedApk类对象info
        • 获取info对象中的ClassLoader
    • 设计傀儡dex文件
    • 手工加固APK
    • 代码实现APK加固
      • 实现步骤
    • 总结

前言 动态加载dex之后,我们会想说,能不能将整个程序的dex都进行动态加载。如果将加载的dex事先加密,加载前解密,这样就完成了对程序完整的解密了。但这里面遇到一个问题,那就是Android中很多组件其实是事先在清单文件中注册过的,我们需要在不多修改清单文件的前提下,完成对藏匿在资源中加密的dex文件。完成了这个过程,也就完成了apk的加固
那做到在不过多修改清单文件的前提下,完成对藏匿在资源中加密的dex文件,我们将分为以下几步完成:
  1. 完成对已注册的Activity的加载
  2. 找寻apk启动时最开始启动的代码,插入自己的代码
  3. 设计傀儡dex文件,启动apk之后,将傀儡dex代码替换为目标dex代码
接下来就是按照这个思路,开始加载Activity
加载Activity遇到的问题 我们已经学会了如何动态加载一个类,那动态加载一个Activity呢?为了能让Activity免除资源困扰,我们在资源中先创建一个Activity类,资源同样也建立好。
先看Activity类代码
package com.example.apkdemo2; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; public class MainActivity2 extends AppCompatActivity {@Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); } }

再看资源文件

接着将这个Activity反编译后再转成dex文件
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

然后将生成的dex放到assets目录下。
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

然后删除MainActivity2这个类。
我们在这个项目中去加载Activity,因为当前项目已经有Activity2的资源文件了,这样就可以不受资源的困扰,难度会低很多。
接着完成动态加载dex的步骤:
  1. 拷贝自定义资源中的dex到程序中
  2. 创建一个DexClassLoader,加载dex
  3. 调用加载dex中的class方法
这个步骤我们之前已经完成了
区别在于第三步,这里需要获取类类型,设置Intent信息,启动Activity,代码如下:
Class clz=null; try {clz=dexClassLoader.loadClass("com.example.apkdemo2.MainActivity2"); } catch (ClassNotFoundException e) {e.printStackTrace(); }Intent intent=new Intent(this,clz); startActivity(intent);

完成后我们在界面中增加按钮,然后在按钮中调用上面的方法。
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

运行程序,点击按钮,发现出现了错误,报错信息如下:
unable to instantiate activity

无法实例化Activity。
我们使用创建了DexClassLoader加载器,加载了我们需要的类,但有一个问题,那就是程序当前的ClassLoader是哪个?答案是PathClassLoader,但是PathClassLoader没有我们加载的类。
针对这个问题有两个解决方案
  1. 直接使用PathClassLoader加载我们需要的类
  2. 使用我们自己的ClassLoader替换掉PathClassLoader
第一种方法显然不行。除非我们能先将dex文件安装到apk中,既然都能安装到apk中了,那就不能做到我们想要做的隐藏效果了。所以排除第一种方案
第二种方法,替换掉PathClassLoader,这种方法应该是可行的,因为只有ClassLoader换了,用对应的ClassLoader肯定能加载对应的类
那怎么才能替换掉ClassLoader呢?需要我们进一步分析APK的启动过程,找到保存ClassLoader的变量,变量所在的类,使用反射修改。
接下来从分析APK的启动过程开始
APK的启动过程
关于APK的启动过程,这里推荐一篇文章:
《Android应用程序启动过程源代码分析》https://blog.csdn.net/Luoshengyang/article/details/6689748
这篇文章详细的分析了Android应用程序的启动过程。这里就不再赘述,做一个简单总结
Step 1. Launcher.startActivitySafely Step 2. Activity.startActivity Step 3. Activity.startActivityForResult Step 4. Instrumentation.execStartActivity Step 5. ActivityManagerProxy.startActivity Step 6. ActivityManagerService.startActivity Step 7. ActivityStack.startActivityMayWait Step 8. ActivityStack.startActivityLocked Step 9. ActivityStack.startActivityUncheckedLocked Step 10. Activity.resumeTopActivityLocked Step 11. ActivityStack.startPausingLocked Step 12. ApplicationThreadProxy.schedulePauseActivity Step 13. ApplicationThread.schedulePauseActivity Step 14. ActivityThread.queueOrSendMessage Step 15. H.handleMessage Step 16. ActivityThread.handlePauseActivity Step 17. ActivityManagerProxy.activityPaused Step 18. ActivityManagerService.activityPaused Step 19. ActivityStack.activityPaused Step 20. ActivityStack.completePauseLocked Step 21. ActivityStack.resumeTopActivityLokced Step 22. ActivityStack.startSpecificActivityLocked Step 23. ActivityManagerService.startProcessLocked Step 24. ActivityThread.main Step 25. ActivityManagerProxy.attachApplication Step 26. ActivityManagerService.attachApplication Step 27. ActivityManagerService.attachApplicationLocked Step 28. ActivityStack.realStartActivityLocked Step 29. ApplicationThreadProxy.scheduleLaunchActivity Step 30. ApplicationThread.scheduleLaunchActivity Step 31. ActivityThread.queueOrSendMessage Step 32. H.handleMessage Step 33. ActivityThread.handleLaunchActivity Step 34. ActivityThread.performLaunchActivity Step 35. MainActivity.onCreate

apk的启动过程相对比较复杂,我们的分析目的是为了找到PathClassLoader,所以不需要对每一个步骤进行详细了解,整个启动过程可以简化为下面的步骤
Step 2. Activity.startActivity--->CreateProcess ...... Step 24. ActivityThread.main----->入口点 ...... Step 35. MainActivity.onCreate--->main函数

用Windows系统来对比参照的话,Step 2可以看作是创建进程,一直到Step 24中间的步骤就等于是创建进程的准备工作,而真正的程序入口则是ActivityThread.main,相当于Windows的程序入口,接着Step 35才是真正执行用户代码的地方,相当于是main函数了
然后就可以进入Android源码网站http://androidxref.com/,开始分析app启动过程
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

另外一种分析的方法就是在onCreate函数下断,然后查看调用堆栈
替换ClassLoader流程
获取ActivityThread类对象 这个源码分析起来还是比较吃力的,这里直接看结论。替换ClassLoader的流程如下
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

首先获取ActivityThread类类型,然后调用这个类的currentActivityThread方法,目的是为了拿到sCurrentActivityThread对象
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

private static ActivityThread sCurrentActivityThread;

sCurrentActivityThread是ActivityThread类的类对象,拿到了这个类的类对象,我们就可以操作整个类的数据
获取AppBindData类对象mBoundApplication 安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

然后.通过类对象获取成员变量mBoundApplication
获取LoadedApk类对象info 安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

获取到了AppBindData的类对象以后,就可以拿到AppBindData的类对象内的成员变量info,也就是LoadedApk类对象
获取info对象中的ClassLoader 安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

接着就能获取到LoadedApk类对象内的ClassLoader。最后需要将这一段替换ClassLoader的逻辑转换成代码,就解决了这个问题。
完整代码如下:
//替换app启动时的classloader为加载的dexclassloader private void replaceClassLoader(DexClassLoader dexClassLoader) {try {//1.获取ActivityThread类对象 //获取类类型 Class clzActivityThread=Class.forName("android.app.ActivityThread"); //获取类方法 Method methodcurrentActivityThread=clzActivityThread.getDeclaredMethod("currentActivityThread"); //调用方法 Object objectActivityThread = methodcurrentActivityThread.invoke(null,new Object[]{ }); //2.通过类对象获取成员变量mBoundApplication//获取字段 Field fieldmBoundApplication=clzActivityThread.getDeclaredField("mBoundApplication"); //取消访问检查 fieldmBoundApplication.setAccessible(true); //获取字段的值 Object objBoundApplication= fieldmBoundApplication.get(objectActivityThread); //3.获取mBoundApplication对象中的成员变量info //获取类类型 Class clzAppBindData=https://www.it610.com/article/Class.forName("android.app.ActivityThread$AppBindData"); //获取字段 Field fieldInfo=clzAppBindData.getDeclaredField("info"); fieldInfo.setAccessible(true); //获取字段的值 Object objInfo = fieldInfo.get(objBoundApplication); //4.获取info对象中的mClassLoader //获取类类型 Class clzLoadedApk=Class.forName("android.app.LoadedApk"); //获取字段 Field fieldmClassLoader=clzLoadedApk.getDeclaredField("mClassLoader"); fieldmClassLoader.setAccessible(true); //设置字段 替换ClassLoader fieldmClassLoader.set(objInfo,dexClassLoader); } catch (Exception e) {e.printStackTrace(); } }

设计傀儡dex文件 经过对APK启动的分析,可知在APK启动时在创建MainActivity前,会创建Application对象,调用其attachBaseContext方法,以及onCreate方法 。所以我们设计的傀儡dex只需要先原APK清单文件中添加Application的节点,创建一个MyApplication类,实现attachBaseContext方法以及onCreate方法,在这两个方法中初始化原dex文件,加载dex文件即可
我们可以在attachBaseContext方法,加载源dex文件返回dex加载器,替换系统默认的加载器,然后剩下的交给系统处理即可
先编写傀儡Application代码,继承自Application,重写attachBaseContext方法和onCreate方法
package com.example.dummydex; import android.app.Application; import android.content.Context; public class DummyApplication extends Application {@Override protected void attachBaseContext(Context base) {super.attachBaseContext(base); //code }@Override public void onCreate() {super.onCreate(); //code } }

在attachBaseContext方法中添加加载源dex文件和替换ClassLoader的代码
protected void attachBaseContext(Context base) {super.attachBaseContext(base); //拷贝自定义资源中的dex文件到程序目录下 String path=CopyDex("src.dex"); //创建一个DexClassLoader 加载dex DexClassLoader dexClassLoader=GetLoader(path); //替换ClassLoader replaceClassLoader(dexClassLoader); }

代码中的三个方法和之前的一致,到此DummyApplication类已经写完。注意需要将DummyApplication类的完整类(带包名)写入待加密的清单文件
使用smali.jar将DummyApplication类编译成class.dex
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

至此,傀儡dex文件生成完毕
手工加固APK 有了以上的准备,我们基本上可以手工完成对一个APK的加固,大致步骤:
  1. 获取待加固的apk,随便写一个hello world
  2. 使用apktool反编译apk,修改AndroidManifest.xml,添加DummyApplication类信息,让程序运行的第一个类为DummyApplication类
  3. 将源classes.dex放入assets目录,修改文件名为src.dex
  4. 使用apktool重打包修改后的信息
  5. 替换重打包后的classes.dex为傀儡dex
  6. 为新打包的apk进行签名
实际操作如下:
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

java -jar .\apktool.jar d .\test.apk

首先用apktool将需要加固的apk进行反编译,反编译后会生成文件夹
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

然后修改清单文件,在application中添加name属性,类名为dummydex的完整包名+类名
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

修改完成之后 新建assets文件夹,将apk原本的dex文件放到assets文件夹下,然后修改名称为src.dex
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

java -jar .\apktool.jar b .\test -o app.apk

再将apk进行回编译。如果回编译的时候报了下面的错误
No resource identifier found for attribute ‘compileSdkVersion’ in package ‘android’

那么需要执行一下这条命令
java -jar apktool.jar empty-framework-dir --force

清空一下framework目录
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

然后打开压缩包,将原来的classes.dex删除,然后把dummy.dex放到apk里面
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

并修改为classes.dex,最近将apk打上签名,整个加固过程就完成了
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

最后安装,运行成功。那么整个加固过程就是成功的。
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

此时我们再用AndroidKiller进行反编译,MainActivity已经无法找到
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

工程文件中只有DummyApplication.smali文件。到此就完成了整个手工加固的过程
代码实现APK加固 实现步骤
根据手工加固APK的步骤得出将其转出代码的步骤:
  1. 获取待加密的apk路径
  2. 调用apktool,反编译目标apk
  3. 修改AndroidManifest.xml添加DummyApplication的信息
  4. 将源classes.dex复制到assets目录,修改文件名为src.dex
  5. 调用apktool重新打包,生成新的apk
  6. 修改新的apk中的calsses.dex将其替换为傀儡dex或者将反编译后的smali代码替换为傀儡dex的smali代码
  7. 为新的apk进行签名
主要流程代码如下:
public static void Pack(){//需要反编译的apk文件名 String fileName="test.apk"; //获取当前路径 String currentdir=System.getProperty("user.dir"); //构造全路径 String filepath=currentdir+ File.separator+fileName; //去掉扩展名 String NoExtenDir=StringsUtils.getFileNameNoEx(filepath); //运行apktool 反编译apk System.err.println("1.反编译apk..."); CMDUtils.runCMD("java -jar apktool.jar d "+filepath); System.err.println("1.反编译apk完成..."); //修改xml文件 String xmlPath=NoExtenDir+File.separator+"AndroidManifest.xml"; System.err.println("2.修改清单文件..."); XMLUtils.ChagenApplication(xmlPath); System.err.println("2.修改清单文件完成..."); //拷贝源dex到assets目录 String assetsDir=NoExtenDir+ File.separator+"assets"; System.err.println("3.拷贝源dex到assets目录..."); FileUtils.copyFileFromZip(filepath,assetsDir); System.err.println("3.拷贝源dex到assets目录完成..."); //删除smali文件 拷贝dummydex的smali String smaliDir=NoExtenDir+ File.separator+"smali"; System.err.println("4.删除smali文件 拷贝dummydex的smali..."); FileUtils.deleteFolder(smaliDir); String oldDir=currentdir+ File.separator+"dummySmali"; FileUtils.copyFolder(oldDir,smaliDir); System.err.println("4.删除smali文件 拷贝dummydex的smali完成..."); //重打包 System.err.println("5.重打包apk..."); CMDUtils.runCMD("java -jar apktool.jar b "+NoExtenDir+" -o pack.apk"); System.err.println("5.重打包apk完成..."); //签名 System.err.println("6.对apk进行签名..."); String outpath =currentdir+File.separator+"pack.apk"; String signPath =currentdir+File.separator+"pack_sigin.apk"; CMDUtils.runCMD("java -jar signapk.jar testkey.x509.pem testkey.pk8 "+outpath+" "+signPath,"sign"); System.err.println("6.对apk进行签名完成..."); //收尾工作 System.err.println("7.收尾工作..."); FileUtils.deleteFolder(NoExtenDir); File file=new File(outpath); file.delete(); System.err.println("7.收尾工作完成..."); //输出文件路径 System.out.println("8. 已加固文件:"+signPath); }

运行效果如图:
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

运行完成之后,
安卓逆向|019 Android加固之APK加固的原理和实现
文章图片

APK加固也成功了。完整工程代码如下:
https://download.csdn.net/download/qq_38474570/23560575?spm=1001.2014.3001.5503
总结 【安卓逆向|019 Android加固之APK加固的原理和实现】通过回顾思路,写代码对APK加固有了一定的认识,在完全实现自动化的那一刻,感叹程序的魅力。不过这只是一个Demo,还有很多可以完善的地方,比如内存加载dex文件,合并源dex和傀儡dex等等。

    推荐阅读