Android逆向:加壳原理
最后更新时间:
文章总字数:
预计阅读时间:
Android逆向:加壳原理
为了更好地入门安卓逆向,学习加壳和脱壳,非常有必要进行 Android 基础理论的学习,而不是只是用用工具。这篇文章主要研究一下加壳的具体原理。
前置知识
Android 类加载机制
参考文章:https://juejin.cn/post/7143900207380430855
Android 类加载器
Android 的类加载器分为系统加载器和自定义加载器两种类型,系统类加载器主要包括3种,分别是BootClassloader
、PathClassloader
和DexClassLoader
BaseDexClassLoader:实现应用层类文件的加载,真正的加载逻辑委托给
PathList
来完成。PathClassLoader:继承自
BaseDexClassLoader
,加载系统类和应用程序的类,通常用来加载已安装的apk
的dex
文件,实际上外部存储的dex
文件也能加载。DexClassLoader:继承自
BaseDexClassLoader
,可以加载dex
文件以及包含dex
的压缩文件(apk,dex,jar,zip)
,不管加载哪种文件,最终都要加载dex
文件。Android8.0
之后和PathClassloader
无异。BootClassLoader:
Android
系统启动时会使用BootClassLoader
来预加载常用类,它继承自ClassLoader
,是顶层的父加载器parent
。
双亲委派模式
再再探 ClassLoader ?!,不过之前探的是 JVM 的类加载,现在是 Android 中的类加载。二者都遵循双亲委派模式。双亲委派模式之前说过了这里就不详细说了,其实很简单一个道理:
当加载一个类时,以递归的方式不断向上父加载器询问是否加载过,如果加载过了就不用再加载,直到顶级加载器。如果顶级加载器也没加载过,则尝试加载,加载失败以递归的方式不断向下委派子加载器加载,直到加载成功。
有什么好处呢?防止类重复加载,对于任意一个类确保其在虚拟机中的唯一性(由加载器和完整类名确定),保证类文件不被篡改,特别是系统类的加载逻辑。
Android 中的双亲委派类加载流程如下图所示:
需要注意的是这个双亲委派中的亲是父加载器,并不是父类,PathClassLoader 是 DexClassLoder 的父加载器,但他们都继承自 BaseClassLoader 。
各类加载器具体代码的实现参考文章中写的很清楚。
Android 应用的启动流程
参考文章:https://bbs.kanxue.com/thread-273293.htm
Android 系统启动流程
为了了解 app 加壳的原理,需要了解 app 的启动流程,而 Android 系统先于 app 启动了,先了解一下 Android 系统的启动过程。下图是一张哪里都有的系统启动流程图:
简单来说,加载 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 返回系统中已经安装了的应用程序的信息,并把这些信息封装成一个快捷图标列表显示在系统屏幕上,这样用户就可以通过点击快捷图标来启动应用。
具体流程用简单的话说大概是这样的:
- 点击桌面 App 图标时,Launcher 的 startActivity 方法,通过 Binder 通信,调用 systemServer 进程中 AMS 服务的 startActivity 方法,发起启动请求。
- systemServer 进程接收到请求后向 Zygote 进程发起创建进程的请求。
- Zygote 进程 fork 出 App 进程,并执行 ActivityThread 的 main 方法(这是加壳的关键),创建 ActivityThread 线程,初始化 MainLooper 和主线程的 Handler,同时还初始化了 ActivityManagerProxy 用于与 AMS 通信交互。
- App 进程通过 Binder 向 systemServer 进程发起 attachApplication 请求这里实际上就是App 进程通过 Binder 调用 sytem_server进程中 AMS的attachApplication 方法, AMS 的attachApplication 方法的作用是将 ApplicationThread 对象与 AMS 绑定。
- systemServer 进程在收到 attachApplication 的请求,进行一些准备工作后,再通过 binder IPC 向 App 进程发送 handleBindApplication 请求(初始化 Application 并调用 onCreate 方法)和 scheduleLaunchActivity 请求(创建启动 Activity )。
- App 进程的 binder 进程 ApplicationThread 通过 handler 向 ActivityThread 中的主线程发送 BIND_APPLICATION 和 LAUNCH_ACTIVITY 消息,这里注意的是 AMS 和主线程并不直接通信,而是 AMS 和主线程的内部类 ApplicationThread 通过 Binder 通信, ApplicationThread 再和主线程通过 Handler 消息交互。
- 主线程在收到 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 ):
如图创建 ActivityThread 实例,之后调用 thread.attach(false) 完成一系列初始化准备工作,并完成全局静态变量 sCurrentActivityThread 的初始化。之后主线程进入消息循环,等待接收来自系统的消息。当收到系统发送来的 bindapplication 的进程间调用时(上述启动中的第六步),调用函数 handlebindapplication 来处理该请求。这里借用寒冰师傅的图,分析得很好:
在此函数中,启动了一个 application 对象,将 apk 组件的信息绑定进来,接着一连串调用,调用到了 application 的 attachBaseContext 方法,之后在 callApplicationOnCreate 中调用了 application 的 onCreate 方法,这两个方法是最先获得执行权进行代码执行的,故加固工具的主要逻辑都是通过替换 app 入口 Application ,并自实现这两个函数,在这两个函数中进行代码的脱壳以及执行权交付。
Apk 加壳原理解析
参考文章:https://www.cnblogs.com/chenxd/p/7820087.html
加壳的原理其实很简单,就是准备一个脱壳程序(负责解密源 Apk ,在函数 attachBaseContext 和 onCreate 中执行完加密的 dex 文件的解密后,通过自定义的 Classloader 在内存中加载解密后的 dex 文件),然后通过加壳程序(一个 java 工程)将源 Apk 进行加密,并将其和脱壳 Dex 合并成新的 Dex,最后替换壳程序中的 dex 文件即可,得到新的 Apk ,那么这个新的 Apk 我们也叫作脱壳程序 Apk 。
源 Apk 自不用多说,下面主要分析一下加壳程序和脱壳程序。
加壳程序分析
加壳程序其实就是一个 Java 工程,工作是加密源 Apk ,然后将其写入到脱壳 Dex 文件中,修改文件头,得到一个新的 Dex 文件即可。至于为什么要修改文件头,可以看一下 Dex 文件的头部信息(Dex 和 Class 一样有固定的格式):
checksum,文件校验码,检查文件错误,signature,用于唯一识别本文件,file_size,表示 Dex 文件的大小,这三个字段是我们需要进行修改的。除了这三个之外,还需要在文件的末尾标注加密的源 Apk 的大小,因为在脱壳的时候需要知道 Apk 的大小,才能正确得到 Apk。修改合并后得到新的 Dex 文件格式如下:
看一下加壳程序的代码:
1 | package com.example.reforceapk; |
具体操作也很明了,二进制读出源 Apk ,利用 encrypt 加密(为了增加破解难度,使用更复杂的加密算法,或者将加密操作放到 native 层去做),合并文件,在文件末尾追加源 Apk 长度,修改文件头即可完成。而脱壳 Dex 要通过脱壳项目(一个 Android 项目)编译后得到,接下来看看这个项目的原理和具体操作。
脱壳项目分析
正如上文所述,attachBaseContext 方法会在 Application 的 onCreate 方法执行前执行,是最早获得执行权进行代码执行的,故我们脱壳的工作就需要在这里进行,完成使用权的交付。
类加载器修正
要想动态加载dex文件必须使用自定义的 DexClassLoader ,也就是说在脱壳项目中,我们需要实现类加载器的修正。当前实现类加载器的修正,主要有两种方案:
第一种是替换系统组件加载器为自定义的 DexClassLoader,同时设置自定义 DexClassLoader 的 parent 为系统组件加载器,这就保证了即加载了源程序又没有放弃原先加载的资源与系统代码。如何替换系统类加载器,就和我们上面分析的 ActivityThread 中的 LoadedApk 有关,LoaderApk 主要负责加载一个 Apk 程序,可以看到使用了 mClassLoader :
通过反射获取 mclassLoader ,然后使用我们的 DexClassLoader 进行替换即可,代码实现:
1 | public static void replaceClassLoader(Context context,ClassLoader dexClassLoader){ |
第二种是打破原有的双亲委派关系,在系统组件类加载器 PathClassLoader 和 BootClassLoader 的中间插入我们自己的 DexClassLoader
1 | public static void replaceClassLoader(Context context, ClassLoader dexClassLoader){ |
反射运行源 Apk
要加载一个完整的 Apk ,并让他运行起来,我们需要找到其的 Application 对象,运行 Application 的 onCreate 方法,源 Apk 才开始他的运行生命周期。使用 meta 标签进行设置来得到源 Apk 的 Application 类。先看看源码(自实现 attachBaseContext ):
1 | package com.example.reforceapk; |
陈程师傅写的注释已经非常清晰了,仔细看看就能明白整个流程。从脱壳程序 Apk 中找到源 Apk,并进行解密操作,这里的 decrypt 需要和之前的 encrypt 对应,然后加载解密之后的源程序 Apk,找到他的 Application 程序并运行。
在 AndroidManifest.xml 里的 meta 标签定义了源程序 Apk 的类名。
加壳总结
先通过编译源工程获得了源 Apk 和编译脱壳工程获得了脱壳 Dex ,再通过加壳程序获得了合并后的 Dex,使用解压缩软件将脱壳工程生成的 Apk 的 Dex 替换成合并 Dex ,重签名即可得到最终的加壳 Apk。