Android逆向:加壳原理

文章发布时间:

最后更新时间:

文章总字数:
5.2k

预计阅读时间:
23 分钟

Android逆向:加壳原理

为了更好地入门安卓逆向,学习加壳和脱壳,非常有必要进行 Android 基础理论的学习,而不是只是用用工具。这篇文章主要研究一下加壳的具体原理。

前置知识

Android 类加载机制

参考文章:https://juejin.cn/post/7143900207380430855

Android 类加载器

Android 的类加载器分为系统加载器和自定义加载器两种类型,系统类加载器主要包括3种,分别是BootClassloaderPathClassloaderDexClassLoader

image.png

  • BaseDexClassLoader:实现应用层类文件的加载,真正的加载逻辑委托给PathList来完成。

  • PathClassLoader:继承自BaseDexClassLoader,加载系统类和应用程序的类,通常用来加载已安装的apkdex文件,实际上外部存储的dex文件也能加载。

  • DexClassLoader:继承自BaseDexClassLoader,可以加载dex文件以及包含dex的压缩文件(apk,dex,jar,zip),不管加载哪种文件,最终都要加载dex文件。Android8.0之后和PathClassloader无异。

  • BootClassLoaderAndroid系统启动时会使用BootClassLoader来预加载常用类,它继承自ClassLoader,是顶层的父加载器parent

双亲委派模式

再再探 ClassLoader ?!,不过之前探的是 JVM 的类加载,现在是 Android 中的类加载。二者都遵循双亲委派模式。双亲委派模式之前说过了这里就不详细说了,其实很简单一个道理:

当加载一个类时,以递归的方式不断向上父加载器询问是否加载过,如果加载过了就不用再加载,直到顶级加载器。如果顶级加载器也没加载过,则尝试加载,加载失败以递归的方式不断向下委派子加载器加载,直到加载成功。

有什么好处呢?防止类重复加载,对于任意一个类确保其在虚拟机中的唯一性(由加载器和完整类名确定),保证类文件不被篡改,特别是系统类的加载逻辑。

Android 中的双亲委派类加载流程如下图所示:

image.png

需要注意的是这个双亲委派中的亲是父加载器,并不是父类,PathClassLoader 是 DexClassLoder 的父加载器,但他们都继承自 BaseClassLoader 。

各类加载器具体代码的实现参考文章中写的很清楚。

Android 应用的启动流程

参考文章:https://bbs.kanxue.com/thread-273293.htm

Android 系统启动流程

为了了解 app 加壳的原理,需要了解 app 的启动流程,而 Android 系统先于 app 启动了,先了解一下 Android 系统的启动过程。下图是一张哪里都有的系统启动流程图:

image.png

简单来说,加载 BootLoader,初始化内核 ,启动 init 进程,init 进程 fork 出 Zygote 进程,Zygote 进程 fork 出 SystemServer 进程。

而这个 SystemServer 进程完成的众多工作中,PackageManagerService 主要完成了 Android 应用程序的安装,ActivityManagerService 很重要的一个作用就是启动了 Launcher 进程,具体启动方式详见这篇文章:https://blog.csdn.net/itachi85/article/details/56669808 ,进入 Launcher 进程就进入到了 App 启动的流程。

App 启动流程

Launcher 在启动过程中会请求 PMS 返回系统中已经安装了的应用程序的信息,并把这些信息封装成一个快捷图标列表显示在系统屏幕上,这样用户就可以通过点击快捷图标来启动应用。

image.png

具体流程用简单的话说大概是这样的:

  1. 点击桌面 App 图标时,Launcher 的 startActivity 方法,通过 Binder 通信,调用 systemServer 进程中 AMS 服务的 startActivity 方法,发起启动请求。
  2. systemServer 进程接收到请求后向 Zygote 进程发起创建进程的请求。
  3. Zygote 进程 fork 出 App 进程,并执行 ActivityThread 的 main 方法(这是加壳的关键),创建 ActivityThread 线程,初始化 MainLooper 和主线程的 Handler,同时还初始化了 ActivityManagerProxy 用于与 AMS 通信交互。
  4. App 进程通过 Binder 向 systemServer 进程发起 attachApplication 请求这里实际上就是App 进程通过 Binder 调用 sytem_server进程中 AMS的attachApplication 方法, AMS 的attachApplication 方法的作用是将 ApplicationThread 对象与 AMS 绑定。
  5. systemServer 进程在收到 attachApplication 的请求,进行一些准备工作后,再通过 binder IPC 向 App 进程发送 handleBindApplication 请求(初始化 Application 并调用 onCreate 方法)和 scheduleLaunchActivity 请求(创建启动 Activity )。
  6. App 进程的 binder 进程 ApplicationThread 通过 handler 向 ActivityThread 中的主线程发送 BIND_APPLICATION 和 LAUNCH_ACTIVITY 消息,这里注意的是 AMS 和主线程并不直接通信,而是 AMS 和主线程的内部类 ApplicationThread 通过 Binder 通信, ApplicationThread 再和主线程通过 Handler 消息交互。
  7. 主线程在收到 Message 后,创建 Application 并调用 onCreate 方法,再通过反射机制创建目标 Activity,并回调 Activity.onCreate() 等方法,App 正式启动,开始进入 Activity 生命周期,执行 onCreate,显示 App 画面。

就是三大进程的七大组件互相进行通信。

ActivityThread 启动流程

参考寒冰师傅的文章:https://bbs.kanxue.com/thread-252630.htm

正如寒冰师傅所说:ActivityThread.main()是进入App世界的大门,看看源码了解 ActivityThread 的具体操作(看源码的网站:http://androidxref.com ):

image.png

如图创建 ActivityThread 实例,之后调用 thread.attach(false) 完成一系列初始化准备工作,并完成全局静态变量 sCurrentActivityThread 的初始化。之后主线程进入消息循环,等待接收来自系统的消息。当收到系统发送来的 bindapplication 的进程间调用时(上述启动中的第六步),调用函数 handlebindapplication 来处理该请求。这里借用寒冰师傅的图,分析得很好:

image.png

在此函数中,启动了一个 application 对象,将 apk 组件的信息绑定进来,接着一连串调用,调用到了 application 的 attachBaseContext 方法,之后在 callApplicationOnCreate 中调用了 application 的 onCreate 方法,这两个方法是最先获得执行权进行代码执行的,故加固工具的主要逻辑都是通过替换 app 入口 Application ,并自实现这两个函数,在这两个函数中进行代码的脱壳以及执行权交付。

Apk 加壳原理解析

参考文章:https://www.cnblogs.com/chenxd/p/7820087.html

image.png

加壳的原理其实很简单,就是准备一个脱壳程序(负责解密源 Apk ,在函数 attachBaseContext 和 onCreate 中执行完加密的 dex 文件的解密后,通过自定义的 Classloader 在内存中加载解密后的 dex 文件),然后通过加壳程序(一个 java 工程)将源 Apk 进行加密,并将其和脱壳 Dex 合并成新的 Dex,最后替换壳程序中的 dex 文件即可,得到新的 Apk ,那么这个新的 Apk 我们也叫作脱壳程序 Apk 。

源 Apk 自不用多说,下面主要分析一下加壳程序和脱壳程序。

加壳程序分析

加壳程序其实就是一个 Java 工程,工作是加密源 Apk ,然后将其写入到脱壳 Dex 文件中,修改文件头,得到一个新的 Dex 文件即可。至于为什么要修改文件头,可以看一下 Dex 文件的头部信息(Dex 和 Class 一样有固定的格式):

image.png

checksum,文件校验码,检查文件错误,signature,用于唯一识别本文件,file_size,表示 Dex 文件的大小,这三个字段是我们需要进行修改的。除了这三个之外,还需要在文件的末尾标注加密的源 Apk 的大小,因为在脱壳的时候需要知道 Apk 的大小,才能正确得到 Apk。修改合并后得到新的 Dex 文件格式如下:

image.png

看一下加壳程序的代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package com.example.reforceapk;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.Adler32;


public class mymain {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
File payloadSrcFile = new File("force/ForceApkObj.apk"); //需要加壳的程序
System.out.println("apk size:"+payloadSrcFile.length());
File unShellDexFile = new File("force/ForceApkObj.dex"); //解壳dex
byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
byte[] unShellDexArray = readFileBytes(unShellDexFile);//以二进制形式读出dex
int payloadLen = payloadArray.length;
int unShellDexLen = unShellDexArray.length;
int totalLen = payloadLen + unShellDexLen +4;//多出4字节是存放长度的。
byte[] newdex = new byte[totalLen]; // 申请了新的长度
//添加解壳代码
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容
//添加加密后的解壳数据
System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容
//添加解壳数据长度
System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4为长度
//修改DEX file size文件头
fixFileSizeHeader(newdex);
//修改DEX SHA1 文件头
fixSHA1Header(newdex);
//修改DEX CheckSum文件头
fixCheckSumHeader(newdex);

String str = "force/classes.dex";
File file = new File(str);
if (!file.exists()) {
file.createNewFile();
}

FileOutputStream localFileOutputStream = new FileOutputStream(str);
localFileOutputStream.write(newdex);
localFileOutputStream.flush();
localFileOutputStream.close();


} catch (Exception e) {
e.printStackTrace();
}
}

//直接返回数据,读者可以添加自己加密方法
private static byte[] encrpt(byte[] srcdata){
for(int i = 0;i<srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}

/**
* 修改dex头,CheckSum 校验码
* @param dexBytes
*/
private static void fixCheckSumHeader(byte[] dexBytes) {
Adler32 adler = new Adler32();
adler.update(dexBytes, 12, dexBytes.length - 12);//从12到文件末尾计算校验码
long value = adler.getValue();
int va = (int) value;
byte[] newcs = intToByte(va);
//高位在前,低位在前掉个个
byte[] recs = new byte[4];
for (int i = 0; i < 4; i++) {
recs[i] = newcs[newcs.length - 1 - i];
System.out.println(Integer.toHexString(newcs[i]));
}
System.arraycopy(recs, 0, dexBytes, 8, 4);//效验码赋值(8-11)
System.out.println(Long.toHexString(value));
System.out.println();
}


/**
* int 转byte[]
* @param number
* @return
*/
public static byte[] intToByte(int number) {
byte[] b = new byte[4];
for (int i = 3; i >= 0; i--) {
b[i] = (byte) (number % 256);
number >>= 8;
}
return b;
}

/**
* 修改dex头 sha1值
* @param dexBytes
* @throws NoSuchAlgorithmException
*/
private static void fixSHA1Header(byte[] dexBytes)
throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(dexBytes, 32, dexBytes.length - 32);//从32为到结束计算sha--1
byte[] newdt = md.digest();
System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)
//输出sha-1值,可有可无
String hexstr = "";
for (int i = 0; i < newdt.length; i++) {
hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16)
.substring(1);
}
System.out.println(hexstr);
}

/**
* 修改dex头 file_size值
* @param dexBytes
*/
private static void fixFileSizeHeader(byte[] dexBytes) {
//新文件长度
byte[] newfs = intToByte(dexBytes.length);
System.out.println(Integer.toHexString(dexBytes.length));
byte[] refs = new byte[4];
//高位在前,低位在前掉个个
for (int i = 0; i < 4; i++) {
refs[i] = newfs[newfs.length - 1 - i];
System.out.println(Integer.toHexString(newfs[i]));
}
System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)
}


/**
* 以二进制读出文件内容
* @param file
* @return
* @throws IOException
*/
private static byte[] readFileBytes(File file) throws IOException {
byte[] arrayOfByte = new byte[1024];
ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(file);
while (true) {
int i = fis.read(arrayOfByte);
if (i != -1) {
localByteArrayOutputStream.write(arrayOfByte, 0, i);
} else {
return localByteArrayOutputStream.toByteArray();
}
}
}
}

具体操作也很明了,二进制读出源 Apk ,利用 encrypt 加密(为了增加破解难度,使用更复杂的加密算法,或者将加密操作放到 native 层去做),合并文件,在文件末尾追加源 Apk 长度,修改文件头即可完成。而脱壳 Dex 要通过脱壳项目(一个 Android 项目)编译后得到,接下来看看这个项目的原理和具体操作。

脱壳项目分析

正如上文所述,attachBaseContext 方法会在 Application 的 onCreate 方法执行前执行,是最早获得执行权进行代码执行的,故我们脱壳的工作就需要在这里进行,完成使用权的交付。

类加载器修正

要想动态加载dex文件必须使用自定义的 DexClassLoader ,也就是说在脱壳项目中,我们需要实现类加载器的修正。当前实现类加载器的修正,主要有两种方案:

第一种是替换系统组件加载器为自定义的 DexClassLoader,同时设置自定义 DexClassLoader 的 parent 为系统组件加载器,这就保证了即加载了源程序又没有放弃原先加载的资源与系统代码。如何替换系统类加载器,就和我们上面分析的 ActivityThread 中的 LoadedApk 有关,LoaderApk 主要负责加载一个 Apk 程序,可以看到使用了 mClassLoader :
image.png
通过反射获取 mclassLoader ,然后使用我们的 DexClassLoader 进行替换即可,代码实现:

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
37
public static void replaceClassLoader(Context context,ClassLoader dexClassLoader){
ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
try {
//1.获取ActivityThread实例
Class ActivityThread = pathClassLoader.loadClass("android.app.ActivityThread");
Method currentActivityThread = ActivityThread.getDeclaredMethod("currentActivityThread");
Object activityThreadObj = currentActivityThread.invoke(null);
//2.通过反射获得类加载器
//final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
Field mPackagesField = ActivityThread.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
//3.拿到LoadedApk
ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj);
String packagename = context.getPackageName();
WeakReference wr = (WeakReference) mPackagesObj.get(packagename);
Object LoadApkObj = wr.get();
//4.拿到mclassLoader
Class LoadedApkClass = pathClassLoader.loadClass("android.app.LoadedApk");
Field mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
Object mClassLoader =mClassLoaderField.get(LoadApkObj);
Log.e("mClassLoader",mClassLoader.toString());
//5.将系统组件ClassLoader给替换
mClassLoaderField.set(LoadApkObj,dexClassLoader);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}

第二种是打破原有的双亲委派关系,在系统组件类加载器 PathClassLoader 和 BootClassLoader 的中间插入我们自己的 DexClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void replaceClassLoader(Context context, ClassLoader dexClassLoader){
//将pathClassLoader父节点设置为DexClassLoader
ClassLoader pathClassLoaderobj = context.getClassLoader();
Class<ClassLoader> ClassLoaderClass = ClassLoader.class;
try {
Field parent = ClassLoaderClass.getDeclaredField("parent");
parent.setAccessible(true);
parent.set(pathClassLoaderobj,dexClassLoader);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

}

反射运行源 Apk

要加载一个完整的 Apk ,并让他运行起来,我们需要找到其的 Application 对象,运行 Application 的 onCreate 方法,源 Apk 才开始他的运行生命周期。使用 meta 标签进行设置来得到源 Apk 的 Application 类。先看看源码(自实现 attachBaseContext ):

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
package com.example.reforceapk;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import dalvik.system.DexClassLoader;

public class ProxyApplication extends Application{
private static final String appkey = "APPLICATION_CLASS_NAME";
private String apkFileName;
private String odexPath;
private String libPath;

//这是context 赋值
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
//创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录
File odex = this.getDir("payload_odex", MODE_PRIVATE);
File libs = this.getDir("payload_lib", MODE_PRIVATE);
odexPath = odex.getAbsolutePath();
libPath = libs.getAbsolutePath();
apkFileName = odex.getAbsolutePath() + "/payload.apk";
File dexFile = new File(apkFileName);
Log.i("demo", "apk size:"+dexFile.length());
if (!dexFile.exists())
{
dexFile.createNewFile(); //在payload_odex文件夹内,创建payload.apk
// 读取程序classes.dex文件
byte[] dexdata = this.readDexFileFromApk();

// 分离出解壳后的apk文件已用于动态加载
this.splitPayLoadFromDex(dexdata);
}
// 配置动态加载环境
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
String packageName = this.getPackageName();//当前apk的包名
//下面两句不是太理解
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mPackages");
WeakReference wr = (WeakReference) mPackages.get(packageName);
//创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)
DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
libPath, (ClassLoader) RefInvoke.getFieldOjbect(
"android.app.LoadedApk", wr.get(), "mClassLoader"));
//base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
//把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader ----有点c++中进程环境的意思~~
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
wr.get(), dLoader);

Log.i("demo","classloader:"+dLoader);

try{
Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
Log.i("demo", "actObj:"+actObj);
}catch(Exception e){
Log.i("demo", "activity:"+Log.getStackTraceString(e));
}


} catch (Exception e) {
Log.i("demo", "error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
}

@Override
public void onCreate() {
{
//loadResources(apkFileName);

Log.i("demo", "onCreate");
// 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
String appClassName = null;
try {
ApplicationInfo ai = this.getPackageManager()
.getApplicationInfo(this.getPackageName(),
PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml文件中的。
} else {
Log.i("demo", "have no application class name");
return;
}
} catch (NameNotFoundException e) {
Log.i("demo", "error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
//有值的话调用该Applicaiton
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});
Object mBoundApplication = RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mBoundApplication");
Object loadedApkInfo = RefInvoke.getFieldOjbect(
"android.app.ActivityThread$AppBindData",
mBoundApplication, "info");
//把当前进程的mApplication 设置成了null
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
loadedApkInfo, null);
Object oldApplication = RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mInitialApplication");
//http://www.codeceo.com/article/android-context.html
ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
.getFieldOjbect("android.app.ActivityThread",
currentActivityThread, "mAllApplications");
mAllApplications.remove(oldApplication);//删除oldApplication

ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
.getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
"mApplicationInfo");
ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
.getFieldOjbect("android.app.ActivityThread$AppBindData",
mBoundApplication, "appInfo");
appinfo_In_LoadedApk.className = appClassName;
appinfo_In_AppBindData.className = appClassName;
Application app = (Application) RefInvoke.invokeMethod(
"android.app.LoadedApk", "makeApplication", loadedApkInfo,
new Class[] { boolean.class, Instrumentation.class },
new Object[] { false, null });//执行 makeApplication(false,null)
RefInvoke.setFieldOjbect("android.app.ActivityThread",
"mInitialApplication", currentActivityThread, app);


ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mProviderMap");
Iterator it = mProviderMap.values().iterator();
while (it.hasNext()) {
Object providerClientRecord = it.next();
Object localProvider = RefInvoke.getFieldOjbect(
"android.app.ActivityThread$ProviderClientRecord",
providerClientRecord, "mLocalProvider");
RefInvoke.setFieldOjbect("android.content.ContentProvider",
"mContext", localProvider, app);
}

Log.i("demo", "app:"+app);

app.onCreate();
}
}

/**
* 释放被加壳的apk文件,so文件
* @param data
* @throws IOException
*/
private void splitPayLoadFromDex(byte[] apkdata) throws IOException {
int ablen = apkdata.length;
//取被加壳apk的长度 这里的长度取值,对应加壳时长度的赋值都可以做些简化
byte[] dexlen = new byte[4];
System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
DataInputStream in = new DataInputStream(bais);
int readInt = in.readInt();
System.out.println(Integer.toHexString(readInt));
byte[] newdex = new byte[readInt];
//把被加壳apk内容拷贝到newdex中
System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
//这里应该加上对于apk的解密操作,若加壳是加密处理的话
//?

//对源程序Apk进行解密
newdex = decrypt(newdex);

//写入apk文件
File file = new File(apkFileName);
try {
FileOutputStream localFileOutputStream = new FileOutputStream(file);
localFileOutputStream.write(newdex);
localFileOutputStream.close();
} catch (IOException localIOException) {
throw new RuntimeException(localIOException);
}

//分析被加壳的apk文件
ZipInputStream localZipInputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(file)));
while (true) {
ZipEntry localZipEntry = localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
if (localZipEntry == null) {
localZipInputStream.close();
break;
}
//取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)
String name = localZipEntry.getName();
if (name.startsWith("lib/") && name.endsWith(".so")) {
File storeFile = new File(libPath + "/"
+ name.substring(name.lastIndexOf('/')));
storeFile.createNewFile();
FileOutputStream fos = new FileOutputStream(storeFile);
byte[] arrayOfByte = new byte[1024];
while (true) {
int i = localZipInputStream.read(arrayOfByte);
if (i == -1)
break;
fos.write(arrayOfByte, 0, i);
}
fos.flush();
fos.close();
}
localZipInputStream.closeEntry();
}
localZipInputStream.close();


}

/**
* 从apk包里面获取dex文件内容(byte)
* @return
* @throws IOException
*/
private byte[] readDexFileFromApk() throws IOException {
ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
ZipInputStream localZipInputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(
this.getApplicationInfo().sourceDir)));
while (true) {
ZipEntry localZipEntry = localZipInputStream.getNextEntry();
if (localZipEntry == null) {
localZipInputStream.close();
break;
}
if (localZipEntry.getName().equals("classes.dex")) {
byte[] arrayOfByte = new byte[1024];
while (true) {
int i = localZipInputStream.read(arrayOfByte);
if (i == -1)
break;
dexByteArrayOutputStream.write(arrayOfByte, 0, i);
}
}
localZipInputStream.closeEntry();
}
localZipInputStream.close();
return dexByteArrayOutputStream.toByteArray();
}


// //直接返回数据,读者可以添加自己解密方法
private byte[] decrypt(byte[] srcdata) {
for(int i=0;i<srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}


//以下是加载资源
protected AssetManager mAssetManager;//资源管理器
protected Resources mResources;//资源
protected Theme mTheme;//主题

protected void loadResources(String dexPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath);
mAssetManager = assetManager;
} catch (Exception e) {
Log.i("inject", "loadResource error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
Resources superRes = super.getResources();
superRes.getDisplayMetrics();
superRes.getConfiguration();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}

@Override
public AssetManager getAssets() {
return mAssetManager == null ? super.getAssets() : mAssetManager;
}

@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}

@Override
public Theme getTheme() {
return mTheme == null ? super.getTheme() : mTheme;
}

}

陈程师傅写的注释已经非常清晰了,仔细看看就能明白整个流程。从脱壳程序 Apk 中找到源 Apk,并进行解密操作,这里的 decrypt 需要和之前的 encrypt 对应,然后加载解密之后的源程序 Apk,找到他的 Application 程序并运行。

image.png

在 AndroidManifest.xml 里的 meta 标签定义了源程序 Apk 的类名。

加壳总结

先通过编译源工程获得了源 Apk 和编译脱壳工程获得了脱壳 Dex ,再通过加壳程序获得了合并后的 Dex,使用解压缩软件将脱壳工程生成的 Apk 的 Dex 替换成合并 Dex ,重签名即可得到最终的加壳 Apk。