Android插件化之Hook底层原理

1,936 阅读9分钟

最近插件化比较流行,同时也涉及到很多底层源码,内容较多,本文将会将最精华的部分挑出来讲,尽可能把插件化吃下去,将按照下面三个模块来分析,:

  1. 插件化诞生背景
  2. Hook是什么
  3. 底层实现原理

插件化诞生背景

什么是插件化?

我们先来看看下面的场景:

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两个进程

注意,Client和Server是相对的。谁发消息,谁就是Client,谁接收消息,谁就是Server
举个例子,进程A和进程B之间使用Binder通信,进程A发消息给进程B,那么这时候A是Binder Client,
B是Binder Server;进程B发消息给进程A,那么这时候B是Binder Client,A是Binder Server
其实,这么说虽然简单,但是不太严谨,我们先这么理解。

(Binder底层原理图)

总结来说:就是类似于代理


2. AMS基本原理

如果站在四大组件的角度来看,AMS就是Binder中的Server

AMS(ActivityManagerService)从字面意思上看是管理Activity的,但其实四大组件都归它管

由此而说到了插件化,有两个让人困惑问题:

  1. App的安装过程,为什么不把apk解压缩到本地?这样读取图片就不用每次从apk包中读取了

  2. 为什么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. 在第1步,发送要启动的Activity信息给AMS之前,把这个Activity替换为一个在AndroidManifest中声明的StubActivity,这样就能绕过AMS的检查了。在替换的过程中,要把原来的Activity信息存放在Bundle中

  2. 在第5步,AMS通知App启动StubActivity时,我们自然不会启动StubActivity,而是在即将启动的时候,把StubActivity替换为原先的Activity。原先的Activity信息存放在Bundle中,取出来就好了

以下为Hook之后的时序图:

(Hook之后的时序图)


有了解决思路之后,我们来思考具体实现方案:

一:第一步

第一步在代码中的具体表达其实就是:

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

代码可尝试自行实现