最近插件化比较流行,同时也涉及到很多底层源码,内容较多,本文将会将最精华的部分挑出来讲,尽可能把插件化吃下去,将按照下面三个模块来分析,:
- 插件化诞生背景
- Hook是什么
- 底层实现原理
插件化诞生背景
什么是插件化?
我们先来看看下面的场景:
1、如果要做功能丰富的apk,在里面,我们能玩到各种各样的游戏(如一个游戏平台),如果要将所有的游戏代码都预先写到一个apk里面,用户的下载及安装的时间成本则非常高,用户体验无疑十分糟糕。(按需加载,功能解耦)
2、现在我们开发的apk功能需要持续迭代更新,更新频率比较高,是否意味着要让用户高频地下载安装来更新apk?有没有什么好的办法,能解决这个问题?(安装成本,热更新)
为了能同时解决上面两个痛点,插件化就出来了:
插件化就是能将一个apk的复杂业务按功能分成多个模块,每个模块作为一个单独的apk,通过hook机制让用户按需下载安装,在需要更新时,能够实现免安装更新的技术
Hook是什么?
从上面的对插件化的解释,可知实现插件化的关键技术就是Hook,所以问题又来了,什么是Hook?
我们先来想一下,解决上面两个问题的方法:
上面两个问题翻译一下就是怎么免去用户主动下载安装这个过程,来到达在安卓系统中运行或更新我们的“业务功能”,我们知道由于安卓系统的设计和权限的限制,任何app都必须先安装之后才能运行业务工能,那有什么好办法?
Hook
你android系统即然一定要安装,那我就“欺骗”你一下,让你误以为安装了,这就是Hook:
一种函数拦截技术,在进程间正常通信的时候进行拦截,将通信过程中传递的结果替换为我们想要的结果,实现欺上瞒下,从而达到免安装运行的目的
那现在知道了hook是一门“欺骗”的技术,那它要去骗谁,才能达到免安装apk的目的?
这其中最关键的是对AMS(Activity Manage Service)和PMS(Package Manage Service)以及Handler的hook。AMS负责管理Android中Activity、 Service、Content Provider、Broadcast四大组件的生命周期以及各种调度工作,我们hook它可以实现对插件四大组件的管理及调度;
PMS负责管理系统中安装的所有App,我们hook它是为了让插件以为自己已经被安装;
Handler是系统向插件传递消息的一个桥梁,我们hook它则是为了把系统发向宿主的消息转发给插件。
底层实现原理
由于篇幅巨大,所以主要讲解图二中Hook是如何“欺骗AMS”,来达到启动一个我们想要的activity(界面)的目的
由于涉及AMS,而AMS的底层实现是基于Binder的,所以会根据以下思路讲解:
1. Binder基本原理
Binder分为Client和Server两个进程
2. AMS基本原理
如果站在四大组件的角度来看,AMS就是Binder中的Server
AMS(ActivityManagerService)从字面意思上看是管理Activity的,但其实四大组件都归它管
由此而说到了插件化,有两个让人困惑问题:
-
App的安装过程,为什么不把apk解压缩到本地?这样读取图片就不用每次从apk包中读取了
-
为什么Hook永远是在Binder Client端,也就是四大组件这边,而不是在AMS那一侧进行Hook
这里要说清楚第二个问题。就拿Android剪切板举例吧,它也是个Binder服务
AMS要负责和所有App的四大组件进行通信。 如果在一个App中,在AMS层面把剪切板功能进行 了Hook,那会导
致Android系统所有的剪切板功能被Hook——这就是病毒了,如果是这样的话,Android系统早就死翘翘了。所以
Android系统不允许我们这么做。
我们只能在AMS的另一侧,即Client端,也就是四大组件这边做Hook,这样即使我们把剪切板功 能进行了
Hook,也只影响Hook代码所在的App,在 别的App中,剪切板功能还是正常的。
所以说对于AMS,需要有两个明确的概念:
- 用Binder思维,它属于Server角色;
- 在app与AMS通讯过程中,进行Hook的话,我们需要从app端下手
3. Hook底层原理
接下来,将以一个Demo讲解Hook原理
上面我们也说了,Hook可以欺骗安卓系统,来启动我们想要的东西,那行,现在我们来启动一个activity,而且是没在AndroidManifest中声明过的
那么问题又来了:为什么要起一个未声明过的activity呢?与Hook有什么关系呢?
安卓系统分配给每个进程的资源是有限的,而这种欺骗本质上就是为了获取系统更多的资源,所以启动一个未声明过的activity,实际上并不会作为宿主的资源去加载,那么我们插件化的目的就达到了,即加载任何我们希望加载的内容,同时不被系统限制
所以下面的内容,就是要分析如何绕开这种限制。
既然要启动一个未声明的activity,说白了,还是要走启动的流程,只不过这个流程是我们想要的流程,所以我们先要知道activity原来的启动流程:
总体上分为两大步骤:(以A界面启动B界面为例,B未声明)
一:A通知AMS去启动B
二:AMS通知app进程去启动B
流程这么多,该Hook哪里,才能达到启动未声明activity的目的呢?
上面说过,要从client端下手,即app进程,同时结合逆向分析
逆向分析:
如果启动一个未声明的activity,那它在原来的启动流程中的哪一个环节会出现问题呢?这个问题点能不能通过
hook解决呢?能,不就是我们Hook的解决方案了吗?
事实上,这就是找准Hook点的关键思路:
在熟悉原有的完整流程前提下,去设想流程中发生了一件我们要想发生的事,比如这里的启动未声明的activity,然后去看这个流程会发生什么问题,那么这个问题点的前后,往往就是我们可以Hook的位置
回到例子中,因为AMS对Activity是否在AndroidManifest中声明的检查,是在第2步完成的,所以要想办法在它前后做文章
以下是解决思路:
-
在第1步,发送要启动的Activity信息给AMS之前,把这个Activity替换为一个在AndroidManifest中声明的StubActivity,这样就能绕过AMS的检查了。在替换的过程中,要把原来的Activity信息存放在Bundle中
-
在第5步,AMS通知App启动StubActivity时,我们自然不会启动StubActivity,而是在即将启动的时候,把StubActivity替换为原先的Activity。原先的Activity信息存放在Bundle中,取出来就好了
有了解决思路之后,我们来思考具体实现方案:
一:第一步
第一步在代码中的具体表达其实就是:
ActivityManagerNative.getDefault().startActivity();
拦截startActivity方法,从参数中取出原有的Intent,替换为启动StubActivity的newIntent,同时把原有的Intent保存在newIntent中,这就是第一步的具体实现方案,说白了就是把假的信息保存进去,AMS取的时候,就让他取真的
具体代码:
辅助类:
public class AMSHookHelper {
public static final String EXTRA_TARGET_INTENT = "extra_target_intent";
/**
* Hook AMS
* 主要完成的操作是 "把真正要启动的Activity临时替换为在AndroidManifest.xml
* 的替身Activity",进而骗过AMS
*/
public static void hookAMN() throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException,
IllegalAccessException, NoSuchFieldException {
//获取AMN的gDefault单例gDefault,gDefault是final静态的\
Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault");
// gDefault是一个 android.util.Singleton<T>对象; 我们取出这个单例里面 mInstance字段
Object mInstance = RefInvoke.getFieldObject("android.util.Singl
// 创建一个这个对象的代理对象MockClass1, 然后替换这个字段
Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class<?>[] { classB2Interface },
new MockClass1(mInstance));
//把gDefault的mInstance字段,修改为proxy
RefInvoke.setFieldObject("android.util.Singleton", gDefault, "mInstance");
}
}
实现类代码:
class MockClass1 implements InvocationHandler {
private static final String TAG = "MockClass1";
Object mBase;
public MockClass1(Object base) {
mBase = base;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) th
Log.e("bao", method.getName());
if ("startActivity".equals(method.getName())) { // 只拦截这个方法
// 替换参数, 任你所为;甚至替换原始Activity启动别的Activity偷梁换柱 // 找到参数里面的第一个Intent 对象\
Intent raw;
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break; }
}
raw = (Intent) args[index];
Intent newIntent = new Intent();
// 替身Activity的包名, 也就是我们自己的包名
String stubPackage = raw.getComponent().getPackageName();
// 这里我们把启动的Activity临时替换为 StubActivity ComponentName componentName = new ComponentName(stubPackage newIntent.setComponent(componentName);
// 把我们原始要启动的TargetActivity先存起来 newIntent.putExtra(AMSHookHelper.EXTRA_TARGET_INTENT, raw);
// 替换掉Intent, 达到欺骗AMS的目的 args[index] = newIntent;
Log.d(TAG, "hook success");
return method.invoke(mBase, args);
}
return method.invoke(mBase, args);
}
二:第五步:
如果成功“欺骗了AMS”,AMS会通知App进程启动StubActivity,也就是第4步。我们没有权限修改AMS进程,只能修改第5步,把StubActivity再替换回TargetActivity
网友评论