什么是android apk加固
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是android apk加固相关的知识,希望对你有一定的参考价值。
加固的过程中需要三个对象:1、需要加密的Apk(源Apk)2、壳程序Apk(负责解密Apk工作)3、加密工具(将源Apk进行加密和壳Dex合并成新的Dex)主要步骤:我们拿到需要加密的Apk和自己的壳程序Apk,然后用加密算法对源Apk进行加密在将壳Apk进行合并得到新的Dex文件,最后替换壳程序中的dex文件即可,得到新的Apk,那么这个新的Apk我们也叫作脱壳程序Apk.他已经不是一个完整意义上的Apk程序了,他的主要工作是:负责解密源Apk.然后加载Apk,让其正常运行起来。 参考技术A混淆代码?
签名
以上两项保护自己的开发成果
1、防逆向:通过DEX 文件加壳以及DEX 虚拟化等技术,防止代码被反编译和逆向分析。
2、防篡改:通过校验 APK 开发者签名,防止被二次打包,植入广告或恶意代码。
3、防调试:防止应用被 IDA、JEB 等工具调试,动态分析代码逻辑。
VirboxProtector对android apk加固的核心技术有:
DEX 虚拟机保护:对 DEX 中的 dalvik 字节码进行虚拟化,转换为自定义的虚拟机指令,最后由 native 层虚拟机解释执行,防止逆向分析。
DEX 文件加密隐藏:对 DEX 文件加壳保护,防止代码被窃取和反编译。
SO 区段压缩加密:对 SO 库中的代码段和数据段压缩并加密,防止被 IDA 等工具反编译。
单步断点检测:在混淆的指令中插入软断点检测暗桩,防止native层run trace和单步调试。
防动态调试:防止应用被 IDA、JEB 等工具调试,动态分析代码逻辑。
开发者签名校验:对 APK 中的开发者签名做启动时校验,防止被第三方逆向和二次打包。
SO 内存完整性校验:在 SO 库加载时校验内存完整性,防止第三方对 SO 库打补丁。
SO 代码混淆:对 SO 库中指定的函数混淆,通过指令切片、控制流扁平化、立即加密等技术手段,将 native 指令转换为难以理解的复杂指令,无法被 IDA 反编译,并且无法被还原。
SO 代码虚拟化:对 SO 库中指定的函数虚拟化,可以将 x86、x64、arm32、arm64 架构的机器指令转换为随机自定义的虚拟机指令,安全强度极高,可通过工具自定义配置,调整性能与安全性。 参考技术C 这个当然可以的啊
019 Android加固之APK加固的原理和实现
文章目录
前言
动态加载dex之后,我们会想说,能不能将整个程序的dex都进行动态加载。如果将加载的dex事先加密,加载前解密,这样就完成了对程序完整的解密了。但这里面遇到一个问题,那就是Android中很多组件其实是事先在清单文件中注册过的,我们需要在不多修改清单文件的前提下,完成对藏匿在资源中加密的dex文件。完成了这个过程,也就完成了apk的加固
那做到在不过多修改清单文件的前提下,完成对藏匿在资源中加密的dex文件,我们将分为以下几步完成:
- 完成对已注册的Activity的加载
- 找寻apk启动时最开始启动的代码,插入自己的代码
- 设计傀儡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);
}
}
再看资源文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity2">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第二个Activity"
></TextView>
</LinearLayout>
接着将这个Activity反编译后再转成dex文件
然后将生成的dex放到assets目录下。
然后删除MainActivity2这个类。
我们在这个项目中去加载Activity,因为当前项目已经有Activity2的资源文件了,这样就可以不受资源的困扰,难度会低很多。
接着完成动态加载dex的步骤:
- 拷贝自定义资源中的dex到程序中
- 创建一个DexClassLoader,加载dex
- 调用加载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);
完成后我们在界面中增加按钮,然后在按钮中调用上面的方法。
运行程序,点击按钮,发现出现了错误,报错信息如下:
unable to instantiate activity
无法实例化Activity。
我们使用创建了DexClassLoader加载器,加载了我们需要的类,但有一个问题,那就是程序当前的ClassLoader是哪个?答案是PathClassLoader,但是PathClassLoader没有我们加载的类。
针对这个问题有两个解决方案
- 直接使用PathClassLoader加载我们需要的类
- 使用我们自己的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启动过程
另外一种分析的方法就是在onCreate函数下断,然后查看调用堆栈
替换ClassLoader流程
获取ActivityThread类对象
这个源码分析起来还是比较吃力的,这里直接看结论。替换ClassLoader的流程如下
首先获取ActivityThread类类型,然后调用这个类的currentActivityThread方法,目的是为了拿到sCurrentActivityThread对象
private static ActivityThread sCurrentActivityThread;
sCurrentActivityThread是ActivityThread类的类对象,拿到了这个类的类对象,我们就可以操作整个类的数据
获取AppBindData类对象mBoundApplication
然后.通过类对象获取成员变量mBoundApplication
获取LoadedApk类对象info
获取到了AppBindData的类对象以后,就可以拿到AppBindData的类对象内的成员变量info,也就是LoadedApk类对象
获取info对象中的ClassLoader
接着就能获取到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=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
至此,傀儡dex文件生成完毕
手工加固APK
有了以上的准备,我们基本上可以手工完成对一个APK的加固,大致步骤:
- 获取待加固的apk,随便写一个hello world
- 使用apktool反编译apk,修改AndroidManifest.xml,添加DummyApplication类信息,让程序运行的第一个类为DummyApplication类
- 将源classes.dex放入assets目录,修改文件名为src.dex
- 使用apktool重打包修改后的信息
- 替换重打包后的classes.dex为傀儡dex
- 为新打包的apk进行签名
实际操作如下:
java -jar .\\apktool.jar d .\\test.apk
首先用apktool将需要加固的apk进行反编译,反编译后会生成文件夹
然后修改清单文件,在application中添加name属性,类名为dummydex的完整包名+类名
修改完成之后 新建assets文件夹,将apk原本的dex文件放到assets文件夹下,然后修改名称为src.dex
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目录
然后打开压缩包,将原来的classes.dex删除,然后把dummy.dex放到apk里面
并修改为classes.dex,最近将apk打上签名,整个加固过程就完成了
最后安装,运行成功。那么整个加固过程就是成功的。
此时我们再用AndroidKiller进行反编译,MainActivity已经无法找到
工程文件中只有DummyApplication.smali文件。到此就完成了整个手工加固的过程
代码实现APK加固
实现步骤
根据手工加固APK的步骤得出将其转出代码的步骤:
- 获取待加密的apk路径
- 调用apktool,反编译目标apk
- 修改AndroidManifest.xml添加DummyApplication的信息
- 将源classes.dex复制到assets目录,修改文件名为src.dex
- 调用apktool重新打包,生成新的apk
- 修改新的apk中的calsses.dex将其替换为傀儡dex或者将反编译后的smali代码替换为傀儡dex的smali代码
- 为新的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);
}
运行效果如图:
运行完成之后,
APK加固也成功了。完整工程代码如下:
https://download.csdn.net/download/qq_38474570/23560575?spm=1001.2014.3001.5503
总结
通过回顾思路,写代码对APK加固有了一定的认识,在完全实现自动化的那一刻,感叹程序的魅力。不过这只是一个Demo,还有很多可以完善的地方,比如内存加载dex文件,合并源dex和傀儡dex等等。
以上是关于什么是android apk加固的主要内容,如果未能解决你的问题,请参考以下文章