简单的Android手动加壳

学习了F8师傅的《Android应用加固保护开发入门》,记录下笔记~

原理

本篇的加壳是Java层的加壳,即将要保护的类从dex文件里面抽离出来,编译成一个独立的dex文件后对dex文件整体进行加密(如异或加密),在被保护程序执行前,先解密被抽离的dex并将其加载到内存中,再开始执行,这样将在一定程度上提高静态分析的难度。于是我们需要的操作有:

  1. 将关键类抽离并进行加密处理
  2. 在项目源码中删除已经被抽离的类,因为它们将会在以后从其他地方加载(如被加密的文件解密后再加载)
  3. 在被保护程序用户逻辑执行前,对被保护的dex进行解密操作并将其加载到内存中
  4. 做必要的解析与修复,执行到这个阶段壳相关的代码已经执行完成,再跳转到源程序的起始点执行即可。

下面将以例子演示以上操作:

原程序

源程序主要有两个类,其他布局资源什么的根据此可以自行写出。
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~"); //只要MainActivity被创建(类被加载并触发初始化条件,如要执行),这个域就会被设置为此内容
if(getApplication() instanceof MyApplication){ //当此Activity的Application和系统类加载器里的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"项。之后程序运行,结果如下:

现在将会开始手动加壳,我们的目的是在不破坏原有程序功能的前提下保护代码,所以加壳后运行的结果必须应该是和上图一样的。

手动加壳

抽离被保护代码并处理

这里自己只写了上面提到的两个类,那么要保护的也就是这两个类了。

  1. 解压apk后反编译dex:java -jar ShakaApktool.jar bs classes.dex -o classes
  2. 删除多余文件及,只保留:MainActivity.smaliMyApplication.smali两个文件及目录结构
  3. 回编译:java -jar ShakaApktool.jar s classes -o encrypt.dex

对项目进行处理

  1. 删除项目里MainActivityMyApplication两个类的源码
  2. 将处理后的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(若存在)并执行它里面的attachBaseContextonCreate 方法,当然上面的例子中已经有输出日志可以验证:

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";
/*释放dex,这里FileManager.releaseAssetsFile用来解密dex
* 但是本篇为了学习方便并没有对encrypt.dex进行加密,否则可以在此处进行解密操作
*/
File dexFile = FileManager.releaseAssetsFile(this,"encrypt.dex",srcDex,null);
DexClassLoader cl = new DexClassLoader(srcDex,getDir("BetaMao",MODE_PRIVATE).getAbsolutePath(),
getApplicationInfo().nativeLibraryDir,getClassLoader()); //加载释放解密后的dex

/*仅仅这样加载还不行,需要把此处的类加载器替换掉原来的类加载器,因为双亲委派,原来的其实是此处
* 新得到的类加载器的父加载器,它们存在链接关系,所以此处替换后原来的类加载器仍然能够正常使用
*/
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了,那么MainActivitygetApplication()得到的是系统类加载器中的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
/*LoadedApk.java*/
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);
/*newApplication*/public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return newApplication(cl.loadClass(className), context);

/*newApplication*/static public Application newApplication(Class<?> clazz, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = (Application)clazz.newInstance();
app.attach(context);

/*attach*/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);
/*callApplicationOnCreate*/public void callApplicationOnCreate(Application app) {
app.onCreate(); //在此处调用了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);
/*详见上面源码,makeApplication通过application的类名实例化类并且设置很多东西~也会调用到attachBaseContext*/
Application orgApp = (Application) JavaRef.invokeMethod("android.app.LoadedApk", "makeApplication",new Class[]{boolean.class,Instrumentation.class}, loadedApkInfo,new Object[]{false,null});
orgApp.onCreate(); //makeApplication内部不会调用onCreate,这里自己调用下
JavaRef.setFieldObject("android.app.ActivityThread","mInitialApplication", currentActivityThread,orgApp);
return;
}

现在结果已经和原来一样了,而且日志也和原来一样说明MyApplication完成了和原程序中一样的操作~

参考

i春秋:https://www.ichunqiu.com/course/58853
https://www.jianshu.com/p/640e956cf41c