学习了F8师傅的《Android应用加固保护开发入门》,记录下笔记~
原理
本篇的加壳是Java层的加壳,即将要保护的类从dex文件里面抽离出来,编译成一个独立的dex文件后对dex文件整体进行加密(如异或加密),在被保护程序执行前,先解密被抽离的dex并将其加载到内存中,再开始执行,这样将在一定程度上提高静态分析的难度。于是我们需要的操作有:
- 将关键类抽离并进行加密处理
- 在项目源码中删除已经被抽离的类,因为它们将会在以后从其他地方加载(如被加密的文件解密后再加载)
- 在被保护程序用户逻辑执行前,对被保护的dex进行解密操作并将其加载到内存中
- 做必要的解析与修复,执行到这个阶段壳相关的代码已经执行完成,再跳转到源程序的起始点执行即可。
下面将以例子演示以上操作:
原程序
源程序主要有两个类,其他布局资源什么的根据此可以自行写出。
MainActivity
是第一个执行的Activity,它做的事如下注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package betamao.jk;
import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.widget.TextView;
public class MainActivity extends AppCompatActivity { static final String TAG = "BetaMao"; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate: MainActivity!"); setContentView(R.layout.activity_main); TextView tvWorld = (TextView) findViewById(R.id.textView2); TextView t2World = (TextView) findViewById(R.id.textView); t2World.setText("MainActivity has been loaded~"); if(getApplication() instanceof MyApplication){ tvWorld.setText("Application has been loaded"); }else{ tvWorld.setText("Application hasnt been loaded"); } } }
|
MyApplication
里面对两个方法进行了覆写,其实目前也就只添加了一条输出日志功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package betamao.jk;
import android.app.Application; import android.content.Context; import android.util.Log;
public class MyApplication extends Application { public void onCreate(){ super.onCreate(); Log.d("BetaMao","onCreate: MyApplocation"); }
protected void attachBaseContext(Context bass){ super.attachBaseContext(bass); Log.d("BetaMao","MyApplication attachBaseContext"); } }
|
现在若运行程序,tvWorld
的值其实是Application hasnt been loaded
(可以试试),因为它还需要在在AndroidManifest.xml
中添加android:name=".MyApplication"
项。之后程序运行,结果如下:
现在将会开始手动加壳,我们的目的是在不破坏原有程序功能的前提下保护代码,所以加壳后运行的结果必须应该是和上图一样的。
手动加壳
抽离被保护代码并处理
这里自己只写了上面提到的两个类,那么要保护的也就是这两个类了。
- 解压apk后反编译dex:
java -jar ShakaApktool.jar bs classes.dex -o classes
- 删除多余文件及,只保留:
MainActivity.smali
和MyApplication.smali
两个文件及目录结构
- 回编译:
java -jar ShakaApktool.jar s classes -o encrypt.dex
对项目进行处理
- 删除项目里
MainActivity
和MyApplication
两个类的源码
- 将处理后的
encrypt.dex
放到项目下的资源目录下
编写壳代码加载被保护的类
此处的壳代码的作用是在原程序代码运行前先执行,对被保护的代码进行解密及加载初始化等操作,所以我们需要找到应用程序员能够使用的最早(越早越好,否则需要手动实现很多被保护代码已经实现的功能)的执行的方法,在那个地方我们能够获取到控制权并开始执行壳代码,在最开始学习Android开发时都被告知在AndroidManifest.xml
中指定"Main"Activity
,那么程序将从这个指定的Activity开始执行:
1 2 3 4 5 6
| <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
|
实际上它并不是我们能够取得控制权的最早执行的地方,阅读源码(在app的Launcher2里),会发现Android应用在启动后执行"Main"Activity
前会先创建Application
(若存在)并执行它里面的attachBaseContext
和 onCreate
方法,当然上面的例子中已经有输出日志可以验证:
1 2 3
| 01-21 09:42:03.989 2752-2752/? D/BetaMao: MyApplication attachBaseContext 01-21 09:42:03.990 2752-2752/? D/BetaMao: onCreate: MyApplocation 01-21 09:42:04.448 2752-2752/betamao.jk D/BetaMao: onCreate: MainActivity!
|
所以思路就是:
1.新建一个Application类,将会在里面实现本部分所诉功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package betamao.jk;
import android.app.Application; import android.content.Context;
public class TkApplication extends Application { @Override public void onCreate() { super.onCreate(); }
@Override protected void attachBaseContext(Context base) {
super.attachBaseContext(base); } }
|
并且在AndroidManifest.xml
中把Application的android:name
属性值改为.TkApplication
。同时activity的android:name
的值不变化,由于它所指向的.MainActivity
已经被删除所以报错红色标注,这在低版本的Android studio或者sdk(不知道具体是哪个,后来我两者都用高版本就没出现问题了)中会报Could not identify launch activity: Default Activity not found
错误,这个鬼让我找了一天都没找到原因和解决办法
2.写解密加载代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| protected void attachBaseContext(Context base){ super.attachBaseContext(base); File cache = getDir("grege",MODE_PRIVATE); String srcDex = cache + "/encrypt.dex";
File dexFile = FileManager.releaseAssetsFile(this,"encrypt.dex",srcDex,null); DexClassLoader cl = new DexClassLoader(srcDex,getDir("BetaMao",MODE_PRIVATE).getAbsolutePath(), getApplicationInfo().nativeLibraryDir,getClassLoader());
Object currentActivityThread = JavaRef.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",new Class[]{},new Class[]{}); ArrayMap mPakages = (ArrayMap) JavaRef.getFeildObject("android.app.ActivityThread","mPackages",currentActivityThread); WeakReference wr = (WeakReference) mPakages.get(getPackageName()); JavaRef.setFeildObject("android.app.LoadedApk","mClassLoader",wr.get(),cl); }
|
3.此时类已经加载完成了,但是若继续运行会发现结果和原程序结果不一样:
上面已经提到壳代码应该尽可能早的执行,此处在application中执行已经比较早了,但是原程序也存在application,那么两者之间是存在一定的冲突的,也就是说此处的TkApplication
执行完以后,程序不会再去执行原来的MyApplication
了,那么MainActivity
里getApplication()
得到的是系统类加载器中的TkApplication
而不在是MyApplication
,同时MyApplication
也不再是由系统类加载器加载而是由新建的类加载器加载,所以还需要对application再做修复。所谓修复就是手动实现MyApplication
的创建并且让其替换掉TkApplication
所占据的数据结构,这里再次阅读源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| private void handleBindApplication(AppBindData data) { Application app = data.info.makeApplication(data.restrictedBackupMode, null); public Application makeApplication(boolean forceDefaultAppClass,Instrumentation instrumentation) { String appClass = mApplicationInfo.className; app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext); public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return newApplication(cl.loadClass(className), context);
static public Application newApplication(Class<?> clazz, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Application app = (Application)clazz.newInstance(); app.attach(context);
final void attach(Context context) { attachBaseContext(context); mLoadedApk = ContextImpl.getImpl(context).mPackageInfo; }
return app; } } appContext.setOuterContext(app); mActivityThread.mAllApplications.add(app); mApplication = app; } mInitialApplication = app; instrumentation.callApplicationOnCreate(app); public void callApplicationOnCreate(Application app) { app.onCreate(); } }
|
如上代码注释所述,只要仿照Launcher直接调用makeApplication
创建原来的Application并且替换掉相关数据即可达到目的,实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public void onCreate() { super.onCreate(); Object currentActivityThread = JavaRef.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread",new Class[]{},new Object[]{}); Object mBoundApplication = JavaRef.getFieldObject("android.app.ActivityThread", "mBoundApplication",currentActivityThread); Object loadedApkInfo = JavaRef.getFieldObject("android.app.ActivityThread$AppBindData", "info",mBoundApplication); JavaRef.setFieldObject("android.app.LoadedApk","mApplication",loadedApkInfo,null); String className = "betamao.jk.MyApplication"; ApplicationInfo appInfoLoadedApke = (ApplicationInfo) JavaRef.getFieldObject("android.app.LoadedApk", "mApplicationInfo",loadedApkInfo); appInfoLoadedApke.className = className;
ApplicationInfo appinfoInAppBindData = (ApplicationInfo) JavaRef.getFieldObject("android.app.ActivityThread$AppBindData", "appInfo",mBoundApplication); appinfoInAppBindData.className = className;
Application oldApplication = (Application) JavaRef.getFieldObject("android.app.ActivityThread", "mInitialApplication",currentActivityThread); ArrayList<Application> mAllApplications = (ArrayList<Application>) JavaRef.getFieldObject("android.app.ActivityThread", "mAllApplications",currentActivityThread); mAllApplications.remove(oldApplication); Application orgApp = (Application) JavaRef.invokeMethod("android.app.LoadedApk", "makeApplication",new Class[]{boolean.class,Instrumentation.class}, loadedApkInfo,new Object[]{false,null}); orgApp.onCreate(); JavaRef.setFieldObject("android.app.ActivityThread","mInitialApplication", currentActivityThread,orgApp); return; }
|
现在结果已经和原来一样了,而且日志也和原来一样说明MyApplication
完成了和原程序中一样的操作~
参考
i春秋:https://www.ichunqiu.com/course/58853
https://www.jianshu.com/p/640e956cf41c