-
-
从 App 启动流程看 Android 整体加固
-
发表于: 3小时前 83
-
从 App 启动流程看 Android 整体加固
一、前言
最近在学习安卓加固的时候遇到了一些问题不太明白,比如说**为什么一定要替换掉mClassLoader而不是别的东西,**于是就是想着通过源码分析一下看看有没有什么突破点,这里做一下记录。由于 Android Framework 体系庞大且逻辑复杂,本人也比水,个人理解难免存在局限或偏差。如果在阅读过程中发现任何错误或疏漏,恳请各位大佬不吝赐教。
这篇笔记基于Android-13.0.0_r83源码进行分析。可以从该网站找到:XRefAndroid - Support Android 16.0 & OpenHarmony 6.0 (AndroidXRef/AospXRef)
二、Application的启动
Application源码分析
这里从Application的启动过程开始分析。这里直接从ActivityThread.java的main函数开始分析。这个是整个Android应用进程的真正入口点。

接着就需要跟进到attach当中进一步分析了:

该函数当中会调用attachApplicationLocked的函数,该函数非常的长,内部会调用thread.bindApplication

一个是测试模式的启动,一个是正常模式的启动。通过bindApplication,将一系列的参数,消息封装好发送给主线程进行处理。进入该函数内部之后就可以发现将数据打包,最后通过 sendMessage将消息类型和消息数据发送出去了:

这里sendMessage将消息发送到了主线程的消息队列,那么由谁来对这个消息进行处理呢?可以看到H.BIND_APPLICATION,跟进去查看一下这个H:

这个H继承的是Handler(一个ActivityThread的内部类),内部有一个handleMessage来处理这些对应的消息,这里传入的是BIND_APPLICATION类型的消息,那只需要找到case即可:
这里就是BIND_APPLICATION消息的处理了,内部接收到data之后会调用handleBindApplication来处理这些消息,这是一个最重要的位置了,继续跟进这个handleBindApplication进行分析:
在这个函数当中,有一个非常重要的地方,就是下面这里:

通过这个getPackageInfoNoCheck来获取LoadedApk,这个LoadedApk是一个加固的重点,这里简单说一下:LoadedApk就是一个容器对象,它将硬盘上的APK文件解构成了内存当中可以使用的类加载器、资源管理器、配置信息等,供系统随时调用。
这个data.info类型就是LoadedApk:

在加固的时候我们同样需要获取这个LoadedApk,所以接下来需要跟进这个getPackageInfoNoCheck来看看到底是怎么获取这个LoadedApk,以及这个函数里面做了什么操作:

调用的是getPackageInfo,继续跟进:

可以看到这里的:
1 | LoadedApk packageInfo = ref != null ? ref.get() : null; |
如果ref不为空,则表示LoadedApk不为空,则复用LoadedApk。通过这里也可以大致了解到,一个app有且只有一个LoadedApk。接着就继续往下分析:

这里可以看到,如果没有LoadedApk则直接new一个出来,接着要来看看这个LoadedApk是怎么工作的:

如图所示,会初始化一些必要的东西,例如mPackageName。只不过这里没有说明要怎么从别的地方(比如我们自己的app)中获取这个LoadedApk,这个后续再看。
现在让我们回到handleBindApplication继续往下分析,下一个里程碑的位置就是这个位置:

这里通过LoadedApk获取了一个Application的类。进入分析一下这个makeApplicationInner:

接着会查看缓存sApplications,如果之前创建过则直接复用。接着就是获取Application的类名

接着就是获取类加载器,创建Application的上下文,接着创建Application并实例attach(这里是用户代码第一次执行的地方)

跟进这个newApplication分析:


可以看到这里attach当中调用了attachBaseContext,这个attachBaseContext非常重要,这就是获取到**Context**后,用户代码最早执行的地方。由于这个特性可以对这个地方做很多文章。
最终makeApplicationInner会返回app这个Application。现在回到handleBindApplication继续分析。接下来就是通过callApplicationOnCreate来调用Application.OnCreate()这个函数了

到这里就差不多完成了handleBindApplication这个函数了,下一步就可以处理EXECUTE_TRANSACTION这个消息了,这个消息处理的是activity的启动
Application小结
通过对Application的启动流程可以看出用户代码的执行流程如下: attachBaseContext -> Application.onCreate。实际上这两个之间还有一个 ContentProvider.onCreate这里还没有提及。不过attachBaseContext基本上就是用户代码最早执行的一个地方,整体加固的壳代码会在这个位置进行解壳,然后恢复环境。
三、Activity的启动
Activity源码分析
1. 怎么从Application过渡到Activity
首先先来分析一下,应用在执行完Application.OnCreate到Activity究竟发生了什么。想了解这个问题得接着继续往下看
接下来回到attachApplicationLocked继续往下看,会看到这段代码:

这里调用的是mAtmInternal.attachApplication这个函数,要注意的是,这里并不是调用ActivityManagerService这个类当中的attachApplication函数,先来看看这个mAtmInternal是哪个类的:

可以看到是这个ActivityTaskManagerInternal这个类的,而这个类的实现在ActivityTaskManagerService这个类当中的LocalService私有内部类,接下来得看看这个类当中attachApplication的实现了:

过来之后又发现将wpc委托给了mRootWindowContainer的attachApplication函数,这个mRootWindowContainer是RootWindowContainer类的实例,其中attachApplication函数定义如下:

接着可以看到的是调用了mAttachApplicationHelper当中的process函数,这个mAttachApplicationHelper是该RootWindowContainer类当汇总的一个私有内部类,最后可以找到这个process:

接着调用getChildAt(displayNdx).forAllRootTask,遍历该屏幕下所有的“根任务”。然后通过回调this的函数来处理,这里的调用路径是RootWindowContainer --> WindowContainer --> Task下的forAllRootTask:

也就是说调用的是mAttachApplicationHelper当中的accept,而accept当中还调用了rootTask.forAllActivities函数

如果进入这个位置,最后就会调用test,这个test当中有一个realStartActivityLocked函数:

进入该函数分析,该函数当中又调用了scheduleTransaction:

这里会通过ClientTransactionHandler来发送EXECUTE_TRANSACTION,接下来就是loop循环对这个消息进行处理,启动activity了。

2. Activity的启动
消息循环获取到EXECUTE_TRANSACTION之后就开始通过 mTransactionExecutor来进行事务逻辑的分发

此时需要跟进execute进行代码跟踪:

对于这个函数来说,内部执行过程中需要关注这两个函数:executeCallbacks和executeLifecycleState。首先来看看这个executeCallbacks函数
executeCallbacks

进来函数第一步首先将transaction(也就是sendMessage传入的data),的callBack取出来,然后在下方循环中,通过callbacks将item取出来,再调用item.execute继续向下调用,这里要注意的是item并不是 ClientTransactionItem类型,这里使用多态,item真正的类型是:LaunchActivityItem。
接下里进入到LaunchActivityItem.execute进行分析:

首先对ActivityClientRecord r这个变量进行数据封装,然后进入到client.handleLaunchActivity这个函数,这个是真正的执行入口,标志着逻辑从“事务管理层”(Transaction 框架)正式进入了“Activity 线程管理层”(ActivityThread)。
这里的client在运行的时候其实就是ActivityThread,所以这里使用的是ActivityThread当中的handleLaunchActivity。接下来就需要对handleLaunchActivity进行分析:

该函数中performLaunchActivity这里是最最重要的一部分,通过该函数会输出一个初始化完毕、正在运行的 Activity 对象。接下来该好好分析这个performLaunchActivity函数了
performLaunchActivity

首先就是通过createBaseContextForActivity获取ContextImpl,然后通过appContext的getClassLoader来获取类加载器,这里需要跟进查看一下这个getClassLoader加载器是怎么获取的,对后续加壳原理的理解非常有帮助。

可以看到getClassLoader是通过mPackageInfo.getClassLoader()来获取的,而这个mPackageInfo类型其实是LoadedApk,该类的getClassLoader()是返回mClassLoader:

接下来就是mInstrumentation.newActivity了,这行代码过程,内存中就存在了一个Activity的对象了

来看看这个newActivity的调用链newActivity --> getFactory(pkg).instantiateActivity --> cl.loadClass(className).newInstance:

这里直接使用了反射调用activity的构造函数。这个new Activity结束之后,下一步就是用这个创建的activity对象进行操作了:

接下来activity使用attach来做一些初始化工作,这一步会完成window的创建以及Context的绑定,接下来就是最后一步了:

这里就会调用Activity.OnCreate,接下来就是一些状态设置以及后续activity的启动了。这里就不继续往下分析了。
3. Activity小结
本阶段梳理了 Activity 的实例化过程,揭示了 LoadedApk.mClassLoader 在其中的决定性作用。源码显示,系统在 performLaunchActivity 中强制使用 appContext.getClassLoader() 获取加载器,并通过反射 (loadClass().newInstance()) 创建 Activity 实例。 这一机制解释了为何加固壳必须替换 mClassLoader:只有“偷梁换柱”,将解密后的 DEX 注入到这个特定的 LoadedApk 实例中,才能骗过系统的检查,让原本不存在于 base.apk 中的 Activity 被成功加载和启动。
四、总结
小小的总结了一下:加固的本质就是在 Application.attachBaseContext 阶段,利用 LoadedApk 的单例特性,通过“偷梁换柱”的方式替换 mClassLoader,从而欺骗系统在后续 Activity 启动时去加载我们解密后的代码。
还有就是app的启动流程:attachBaseContext --> ContentProvider.onCreate --> Application.OnCreate --> Activity.OnCreate
后续再根据这个逻辑来实现一个简单的整体加固