安卓整体加壳(一代壳)原理及实践
目录
写在前面:写这篇文章真是呕心沥血,网上对一代壳的技术分析很多,但是有实践操作的文章少。一代壳虽然原理简单,但是实现细节很多,并且学习一代壳能够学习到很多二三代壳也用得到的原理和技术,写这篇文章反反复复看了很多其他大佬的文章,但难免还是会漏掉一些要点,比如双亲委派模型就一句话带过了,还需要读者们自己去看文章了解。由于整体加壳的方式是两个项目嵌套,想要写一步调试一步是有点麻烦的,写的过程中只能摸着石头过河,写完了再一起去debug,还是挺磨性子的。希望这篇文章能给刚入门脱壳的读者们带来一些启发,文中写的不严谨的地方欢迎指正。
1 一代壳简介
1.1 DEX加密(也称落地加载)
第一代壳将整个 apk 文件压缩加密到壳 dex 文件的后面,在壳 dex 文件上写上解压代码,动态加载执行,由于是加密整个 apk,在大型应用中很耗资源,因此这代壳很早就被放弃了但思路还是不变。其中这种加密还可以具体划分为几个方向,如下:
- Dex 字符串加密
- 静态 DEX 文件整体加密解密
- 资源加密( xml 与 arsc 文件加密及十六进制加密)
- 对抗反编译(针对反编译工具,如 apktool。利用反编译工具本身存在的缺陷,使得反编译失败,以此实现对反编译工具的抵抗)
- Ptrace 反调试、TracePid 值校验反调试
- 自定义 DexClassLoader(主要是针对 dex 文件加固、加壳等情况)
- 落地加载( dex 可以在 apk 目录下看到)
1.2 相关脱壳方法
- 内存 Dump 法
- 缓存脱壳法
- 文件监视法
- Hook 法
- 定制系统法
- 动态调试法
2 app启动流程
ActivityThread.main()
是进入App
世界的大门,所以从ActivityThread.main()
开始,了解一下app
启动过程中的一些细节。如果十分了解app
的启动流程,这部分就可以看的快一点。
了解启动流程主要关注调用了Application
什么方法,因为一代壳的实现是依赖于app启动时的一些初始化调用来加载或解密dex
文件的。
2.1 ActivityThread.java
源码地址:/frameworks/base/core/java/android/app/ActivityThread.java
attach
方法如下
这里的调用栈就不深入了,接下来会调用到bindApplication
方法↓
这里的H
是ActivityThread
的一个内置类
在这个H
类中有处理消息的逻辑
最终调用handleBindApplication
,进行app
实例化。
从实例化开始,就要进行深入了。data.info
是一个LoadedApk
类。
2.2 LoadedApk.java
源码地址:/frameworks/base/core/java/android/app/LoadedApk.java
app
的创建进入到了Instrumentation
的newApplication
中
2.3 Instrumentation.java
源码地址:/frameworks/base/core/java/android/app/Instrumentation.java
newApplication
有两种实现模式,这里看参数采用的应当是第一种。
两种方式都是先实例化app
,然后调用app.attach
。于是接下来看attach
做了什么。
2.4 Application.java
源码地址:/frameworks/base/core/java/android/app/Application.java
在attach
中调用了attachBaseContext
2.5 ActivityThread.java
源码地址:/frameworks/base/core/java/android/app/ActivityThread.java
再回到ActivityThread.java
的handleBindApplication
中,还会有调用Application
的OnCreate
函数。
至此可知App
的启动流程是
3 基本原理
在Application
启动流程结束之后才会进入MainActivity
中的attachBaseContext
函数、onCreate
函数。
所以壳要在程序正式执行前,也就是上面的流程中进行动态加载和类加载器的修正,这样才能对加密的dex
进行释放,而一般的壳往往选择在Application
中的attachBaseContext
或onCreate
函数进行。
简单点说,就是把源程序给藏起来,然后在外面包一层用于脱壳的程序,这个脱壳的程序会把源程序给释放出来,并通过反射机制,加载源程序。
4 加壳实践
加壳之前,需要明确分为哪几步。
- 生成一个源程序(安卓项目),一般来说是将源程序打包为
apk
之后藏起来,这样的好处在于源程序的各类资源也都被藏了起来。举一反三既然可以藏整个apk
,那么也可以分开藏一些东西。 - 写一个加壳工具,这个程序不是一个安卓项目,可以用任意语言(本文使用
python
)实现功能,就是一个工具。 - 脱壳程序,确定了我们如何藏我们的
apk
文件之后,使用脱壳程序来释放源程序,并加载。
构建完成之后我们app
的入口应当在脱壳程序里。
4.1 源程序
简单新建项目,创建一个空Activity
。
在Activity
的OnCreate
方法中打印一下。
1 | Log.i("demo", "app:"+getApplicationContext()); |
然后添加一个MyAppliaction
类,并重写一下OnCreate
,输出一下Log
。
4.2 加壳工具
将按照下图的结构构建新dex
这里为了理解画了一个流程图
这里没有对apk
数据进行处理,如有需要修改process_apk_data
即可。
1 | import hashlib |
这里涉及到重打包的过程,具体可以看安卓打包流程。这里需要用安卓SDK
中的zipalign
和apksigner
进行对齐和重打包。
4.3 脱壳程序
接下来编写套在源程序外面的脱壳程序。由于我们最终需要运行的是我们的源程序,所以我们必须在启动流程调用Application
的OnCreate
之前释放出源程序,并替换Application
为我们的源程序Application
实例(原来是脱壳程序的Application
实例)。
我们在基本原理这一节中研究了启动流程,所以在Application
的OnCreate
之前,有一个attachBaseContext
方法,我们可以通过重写该方法来实现上面的效果。
4.3.1 代理Application
这里我们要写一个代理Application
,作为app
的Application
实例。并重写一下attachBaseContext
。
1 | public class ProxyApplication extends Application { |
然后要修改AndroidManifest.xml
,将Application
的实例改为我们自定义的ProxyApplication
之后运行。在Logcat
中看到输出了log
则说明成功。
4.3.2 读取自身apk
在Application
中,需要获取到自身的apk
文件。
1 | private ZipFile getApkZip() throws IOException { |
我们先测试一下,打印看看this.getApplicationInfo().sourceDir
是什么
发现是一个缓存存储apk
的地址,并且就是apk
的路径(而非文件夹路径)。
4.3.3 读取dex
1 | private byte[] readDexFileFromApk() throws IOException { |
4.3.4 提取源apk
1 | private byte[] splitSrcApkFromDex(byte[] dexFileData) { |
4.3.5 修正加载器(重点)
这里开始需要了解双亲委派模型,简单而言就是java
中的类加载器有父子关系,当某个加载器需要加载某个类的时候,先会交给其父类,如果加载过了就直接返回,如此往上,如果父加载器都加载不了,再抛回来自己加载。
关于加载源apk,这里有两个细节且重要的问题需要思考清楚。从这里开始希望大家放慢阅读速度。
- 如何加载**
dex
**文件? - 如何让加载之后的**
Application
**进入后续的加载流程?
这里拿一张非常重要的图
首先解决第一个问题,如何加载**dex
**文件?
引用佬的文章介绍一下BaseDexClassLoader
类加载器
Android里边的
BaseDexClassLoader
可以实现在运行的时候加载在编译时未知的dex文件,经过此加载器的加载,ART虚拟机内存中会形成相应的数据结构,对应的dex文件也会由mmap映射到虚拟内存当中,通过此加载器的loadClass(String className, boolean resolve)
方法就可以得到类的Class对象,从而可以使用该类。查看源码可以看到
PathClassLoader
是继承自BaseDexClassLoader
的,而PathClassLoader
还有另外两个兄弟:InMemoryDexClassLoader
以及DexClassLoader
,而壳程序很多都使用了这两个类加载器来加载解密后的dex文件。其中InMemoryDexClassLoader
是Android8.0以后新增的类,可以实现所谓的”不落地加载”。作者:Jerry_Deng
链接:https://juejin.cn/post/6962096676576165918
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
总结一下,InMemoryDexClassLoader
、DexClassLoader
、PathClassLoader
都继承自BaseDexClassLoader
,我们可以用他们来加载dex
。
第二个问题,如何让加载之后的**Application
**进入后续的加载流程?
后续的加载流程指的就是app
组件(比如Activity
)的加载,而加载组件时,使用的是加载应用程序的ClassLoader
。
如若不做任何处理,仅仅在**attachBaseContext
方法中使用上面讲的某个类加载器对dex
加载,后续加载源程序的组件时会出现ClassNotFoundException
**的错误,为什么会这样?
这是因为如果仅仅在attachBaseContext
方法中使用类加载器加载dex
,之后加载组件时使用的ClassLoader
和我们使用的加载器不同,并且,加载组件的ClassLoader
通过双亲委派模型发现没有人能加载组件类(因为组件类在我们的dex
中),导致ClassNotFoundException
。
还记得BaseDexClassLoader
吗,其有一个DexPathList
,记录了已加载的dex
文件路径。
加载组件时对应的BaseDexClassLoader
的DexPathList
是没有源程序的dex
路径的,如果尝试让BaseDexClassLoader
加载不在这个列表中的类,就会报ClassNotFoundException
。
因此有两种方法可以解决这个问题。
既然使用的加载器不同,那么改成相同的不就行了。
通过反射获取到
LoadedApk
,修改其mClassLoader
为我们加载dex
文件的ClassLoader
实例,这样后续试图加载组件类的时候,就能找到相应的类。通过打破原有双亲委派关系,添加我们的
ClassLoader
进入关系网。原先的
mClassLoader
是PathClassLoader
,其在双亲委派关系中的父亲是BootClassLoader
,所以只要将我们的ClassLoader
添加进他们两个之间即可。也就是将PathClassLoader
的父亲设置为我们自己的ClassLoader
,再将我们自己的ClassLoader
的父亲设置为BootClassLoader
。如下图
理解完以上这些,可以开始实践了。
第一种方法,需要思考如何拿到LoadedApk
。在启动流程的handleBindApplication
中,data.info
就是我们要拿到的LoadedApk
。
向上找到data.info
初始化的地方。
跟进方法
关键代码
所以我们要从mPackages
里面找LoadedApk
。
1 | public static void replaceClassLoader1(Context context,DexClassLoader dexClassLoader){ |
4.3.6 加载源apk
我们使用DexClassLoader
加载dex
,还需要解决几个参数。
dexPath
:dex
文件路径optimizedDirectory
:dex
优化后存放的位置,在ART
上,会执行oat
对dex
进行优化,生成机器码,这里就是存放优化后的odex
文件的位置。librarySearchPath
:native
依赖的位置parent
:双亲委派中的父亲,这里是PathClassLoader
。
Context.getDir
方法是在app
的目录下新建app_
的文件夹。比如打印base.getDir("opt_dex",0)
,结果是 /data/user/0/com.xxx.unshell/app_opt_dex
代码如下
1 |
|
当我们替换掉加载器之后,app
加载流程走完,会加载Activity
,此时我们为了让系统加载我们源程序的Activity
,我们需要修改xml
文件,将脱壳程序的Activity
入口替换为源程序的入口。
之后我们build apk
,然后用加壳程序处理,并安装。
启动程序,查看log
,发现了我们在源程序中写的Log
,说明启动源程序的Activity
成功。
加载成功!
4.3.7 加载源Application
到了这里加壳的核心部分已经结束了,接下来都是补充的部分。
如果源程序也有自定义的Application
,我们就需要重新makeApplication
,进入到源程序的Application
,保证程序的完整生命周期。
注册
application
(用LoadedApk
中的makeApplication
方法注册)。为了使用
makeApplication
重新注册application
,需要先把mApplication
置空并且还需要在在
ActivityThread
下的链表mAllApplications
中移除mInitialApplication
。mAllApplications
存放的是所有的应用,mInitialApplication
存放的是初始化的应用(即当前壳应用)。把当前的壳应用,从现有的应用中移除掉,然后在makeApplication
方法中会把新构建的加入到里面去。之后,替换
ActivityThread
中mInitialApplication
为刚刚makeApplication
创建的app
。总结操作流程就是
- 将
LoadedApk
的mApplication
置空 - 从
ActivityThread
的mAllApplications
中移除mInitialApplication
makeApplication()
- 替换
ActivityThread
中mInitialApplication
解释一下这里修改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
49super.onCreate();
Log.i(TAG,"进入onCreate方法");
// 提取提前配置的ApplicationName,来引导到源程序的Application入口
String applicationName="";
ApplicationInfo ai=null;
try {
ai=getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
if (ai.metaData!=null){
applicationName=ai.metaData.getString("ApplicationName");
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
// 将当前进程的mApplication设置为null
Object activityThreadObj=RefinvokeMethod.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",new Class[]{},new Object[]{});
Object mBoundApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mBoundApplication");
Object info=RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"info");
RefinvokeMethod.setField("android.app.LoadedApk","mApplication",info,null);
// 从ActivityThread的mAllApplications中移除mInitialApplication
Object mInitApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mInitialApplication");
ArrayList<Application> mAllApplications= (ArrayList<Application>) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mAllApplications");
mAllApplications.remove(mInitApplication);
// 更新两处className
ApplicationInfo mApplicationInfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.LoadedApk",info,"mApplicationInfo");
ApplicationInfo appinfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"appInfo");
mApplicationInfo.className=applicationName;
appinfo.className=applicationName;
// 执行makeApplication(false,null)
Application app= (Application) RefinvokeMethod.invokeMethod("android.app.LoadedApk","makeApplication",info,new Class[]{boolean.class, Instrumentation.class},new Object[]{false,null});
// 替换ActivityThread中mInitialApplication
RefinvokeMethod.setField("android.app.ActivityThread","mInitialApplication",activityThreadObj,app);
// 更新ContentProvider
ArrayMap mProviderMap= (ArrayMap) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mProviderMap");
Iterator iterator=mProviderMap.values().iterator();
while (iterator.hasNext()){
Object mProviderClientRecord=iterator.next();
Object mLocalProvider=RefinvokeMethod.getField("android.app.ActivityThread$ProviderClientRecord",mProviderClientRecord,"mLocalProvider");
RefinvokeMethod.setField("android.content.ContentProvider","mContext",mLocalProvider,app);
}
// 执行新app的onCreate方法
app.onCreate();className
的操作。源程序可能也有自定义的一个Application
类,如果有的话我们需要提前配置在xml
的meta-data
中提前设置,之后提取出来。
当然也可以通过解析源程序的
xml
来实现,感兴趣可以研究一下。- 将
更新
ContentProvider
。ContentProvider
是Android
系统中的一个组件,用于在不同的应用程序之间共享数据。需要修改mProviderMap
中所有ContentProvider
的mContext
为新app
1
2
3
4
5
6
7ArrayMap mProviderMap= (ArrayMap) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mProviderMap");
Iterator iterator=mProviderMap.values().iterator();
while (iterator.hasNext()){
Object mProviderClientRecord=iterator.next();
Object mLocalProvider=RefinvokeMethod.getField("android.app.ActivityThread$ProviderClientRecord",mProviderClientRecord,"mLocalProvider");
RefinvokeMethod.setField("android.content.ContentProvider","mContext",mLocalProvider,app);
}执行新app的
onCreate
方法1
app.onCreate();
总体代码如下,这里用到的RefinvokeMethod
贴到了文章最下面问题一节中。
1 |
|
这里通过Log
我们可能会看到报错如下
这是不影响的,因为我们等于是又重新启动了一次App
,只不过这次的Application
设置的是源程序的Application
。
这个报错的源码位于LoadedApk.java
可以看到我们的源程序自定义的Application
还是成功加载了
4.3.8 加载资源
我们通过这样的方式加载源程序,源程序的资源似乎并没有被加载进来,所以这里继续讲如何把源程序的资源加载进来。
当然我们也可以直接把源程序的资源复制到壳程序下面,但加壳的目就是为了保护代码和资源,所以最好还是动态加载。
这里资源加载可能还涉及到Resources
类的更换,实现起来还有点麻烦,这里就暂且阁下不表。
5 问题
- 在过程中出现
Writable dex file '/data/user/0/com.jok.unshell/app_opt_dex/src.apk' is not allowed
这样的报错,原因是Android14
有一个改动: 更安全的动态代码加载,简单来说就是打开DEX
、JAR
、APK
等文件时必须将DEX
文件设置为只读。 RefinvokeMethod.java
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
72import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class RefinvokeMethod {
public static Object invokeStaticMethod(String class_name,String method_name,Class[] classes,Object[] objects){
try {
Class aClass = Class.forName(class_name);
Method method = aClass.getMethod(method_name, classes);
return method.invoke(null,objects);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static Object invokeMethod(String class_name,String method_name,Object obj,Class[] classes,Object[] objects){
try {
Class aClass = Class.forName(class_name);
Method method = aClass.getMethod(method_name, classes);
return method.invoke(obj,objects);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static Object getField(String class_name,Object obj,String field_name){
try {
Class aClass = Class.forName(class_name);
Field field = aClass.getDeclaredField(field_name);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static Object getStaticField(String class_name,String field_name){
try {
Class aClass = Class.forName(class_name);
Field field = aClass.getDeclaredField(field_name);
field.setAccessible(true);
return field.get(null);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void setField(String class_name,String field_name,Object obj,Object value){
try {
Class aClass = Class.forName(class_name);
Field field = aClass.getDeclaredField(field_name);
field.setAccessible(true);
field.set(obj,value);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void setStaticField(String class_name,String field_name,Object value){
try {
Class aClass = Class.forName(class_name);
Field field = aClass.getDeclaredField(field_name);
field.setAccessible(true);
field.set(null,value);
} catch (Exception e) {
e.printStackTrace();
}
}
}- 如果没在
xml
里面指定源程序的Activity
,那就需要在壳程序的attachBaseContext
中添加如下代码运行MainActivity
。1
2
3
4
5
6try {
Object objectMain = dexClassLoader.loadClass("com.example.sourceapk.MainActivity");
Log.i(TAG,"MainActivity类加载完毕");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}