DroidPlugin代码分析(二) Hook机制

来源:互联网 发布:unity5.3.4f1破解mac 编辑:程序博客网 时间:2024/03/29 15:36

接上篇,这篇来看一下Droid Pluginhook机制。Droid Plugin的官方文档提到了下面三点:

  • 动态代理实现函数hook
  • Binder代理绕过部分系统服务限制
  • IO重定向

我们一项一项地来看。

一、动态代理实现函数hook

这部分实现主要在hook/proxy/hook/handle里。先上一张类图:


首先定义了一个基类Hook,这是一个抽象类,外部可以通过setEnable()方法来使能或者关闭该hook。同时它还声明了和install相关的方法,子类可以覆盖这些方法完成相应的初始化。

ProxyHook继承自Hook,同时还实现了InvocationHandler接口。它有一个setOldObj()方法,用来保存将被代理的原始对象。另外,既然实现了InvocationHandler接口,必然要实现其invoke()方法:

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        try {            if (!isEnable()) {                return method.invoke(mOldObj, args);            }            HookedMethodHandler hookedMethodHandler = mHookHandles.getHookedMethodHandler(method);            if (hookedMethodHandler != null) {                return hookedMethodHandler.doHookInner(mOldObj, method, args);            }            return method.invoke(mOldObj, args);        }         ... ...    }

可以看到,这里没有直接在invoke()方法里进行任何处理,而是先通过mHookHandles获取了一个HookedMethodHandler对象。

mHookHandles是一个BaseHookHandle对象,内部包含了一个Map,可以根据API名映射到对应对应的HookedMethodHandler对象。这个Map由其子类IXXXHookHandle在初始化的时候进行填充。

紧接着调用HookedMethodHandlerdoHookInner()方法:

    public synchronized Object doHookInner(Object receiver, Method method, Object[] args) throws Throwable {        try {            ... ...            boolean suc = beforeInvoke(receiver, method, args);            Object invokeResult = null;            if (!suc) {                invokeResult = method.invoke(receiver, args);            }            afterInvoke(receiver, method, args, invokeResult);        .... ...    }

这里其实就跟上一篇的例子一样,在调用实际方法之前,先调用一个beforeInvoke()方法进行一些处理,然后在实际方法调用结束后,再调用afterInvoke()方法进行另外一些处理。需要注意的是,如果beforeInvoke()返回true,那么实际要调用的方法根本不会被执行,直接变被skip掉了。

HookedMethodHandler的子类主要就是覆盖beforeInvoke()afterInvoke()这两个方法,通过修改传入参数达到“瞒上”的目的,通过修改返回值达到“欺下”的目的。但是翻看代码会发现,这些子类一般不直接继承HookedMethodHandler,而是继承自一个叫做ReplaceCallingPackageHookedMethodHandler的类。这个类覆盖了beforeInvoke()方法:

    protected boolean beforeInvoke(Object receiver, Method method, Object[] args) throws Throwable {        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {            if (args != null && args.length > 0) {                for (int index = 0; index < args.length; index++) {                    if (args[index] != null && (args[index] instanceof String)) {                        String str = ((String) args[index]);                        if (isPackagePlugin(str)) {                            args[index] = mHostContext.getPackageName();                        }                    }                }            }        }        return super.beforeInvoke(receiver, method, args);    }

可以看到,如果发现是插件程序的包名的话,会统一替换成宿主(host)的包名。为什么需要这样一层转换呢?原来当我们的app调用一些系统API的时候,都会到AppOpsService那边进行鉴权,AppOpsService会判断当前的uid和包名是不是匹配,如果不匹配就会抛一个“Bad call”的SecurityException(具体参见getOpsRawLocked()方法)。我们启动插件的时候,uid是宿主apk,包名是插件apk,显然是不匹配的。因此经过这层转换以后,我们就可以“欺骗”系统,让其以为是宿主apk调过来的。当然,这样做也是有副作用的,宿主apk必须把所有插件apk需要的权限全都申请上,因为系统只会去检查宿主apk。所以你查看AndroidManifest.xml的时候会发现几乎app能申请的所有权限都被申请了。。。

到此,hook的原理就搞清楚了。以IActivityManager为例:

  • IActivityManagerHook:“劫持”所有IActivityManagerAPI
  • IActivityManagerHookHandle:安装所有被“劫持”的API的处理对象,加入到Map
  • IActivityManagerHookHandle.startActivity:这是一个内部类,专门处理startActivity()方法。以此类推,还有startActivityAsUser类、startActivityAsCall类等等,每个API方法都对应一个处理类。

最后一个问题:这些hook是如何被安装到系统上的?其实就是用了上一篇提到的方法,利用反射替换掉静态单例对象。举个例子,我们看一下IActivityManagerHookonInstall()方法:

    public void onInstall(ClassLoader classLoader) throws Throwable {        Class cls = ActivityManagerNativeCompat.Class();        Object obj = FieldUtils.readStaticField(cls, "gDefault");        if (obj == null) {            ActivityManagerNativeCompat.getDefault();            obj = FieldUtils.readStaticField(cls, "gDefault");        }        if (IActivityManagerCompat.isIActivityManager(obj)) {            setOldObj(obj);            Class<?> objClass = mOldObj.getClass();            List<Class<?>> interfaces = Utils.getAllInterfaces(objClass);            Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];            Object proxiedActivityManager = MyProxy.newProxyInstance(objClass.getClassLoader(), ifs, this);            FieldUtils.writeStaticField(cls, "gDefault", proxiedActivityManager);            Log.i(TAG, "Install ActivityManager Hook 1 old=%s,new=%s", mOldObj, proxiedActivityManager);        }        ... ...    }

很明显,首先获取全局的IActivityManager对象gDefault,然后通过Proxy.newProxyInstance()创建一个代理对象,最后用这个代理对象替换掉之前的gDefaultgDefault变成了mOldObj)。 IActivityManagerHook是这个代理对象的InvocationHandler,因此所有的API调用都会走到它的invoke()方法中来。这样所有路径就都打通了。


二、Binder代理绕过部分系统服务限制

这部分实现主要在hook/binder/hook/handle里。

Binder代理其实基本和之前差不多,稍微有些差别。还是先上一张类图:



这里出现了3个新面孔,我们先看MyServiceManager,这个类包含了3Map

  • mOriginServiceCache:这里存储的是原始的service cache。每个ActivityThreadbindApplication()的时候,会从ServiceManager那边获得一个service cache,每次要和某个service通信时,会先检查这个cache里有没有代理对象,如果有的话就直接用,不需要再和ServiceManager进行一次binder交互了。
  • mProxiedServiceCache:这里存储的就是service cache的代理对象了,因为我们要“劫持”这些binder调用,所以必须把service cache也替换成我们的代理对象,每次调用都会走进ServiceManagerCacheBinderHook对象的invoke()方法。
  • mProxiedObjCache:这里存储的是所有的service代理对象,那原始的service对象放在哪里呢?在BinderHookmOldObj里。

2个类ServiceManagerCacheBinderHook主要就是来替换掉service cache对象的。看一下ServiceManagerCacheBinderHookonInstall()方法:

    protected void onInstall(ClassLoader classLoader) throws Throwable {        Object sCacheObj = FieldUtils.readStaticField(ServiceManagerCompat.Class(), "sCache");        if (sCacheObj instanceof Map) {            Map sCache = (Map) sCacheObj;            Object Obj = sCache.get(mServiceName);            if (Obj != null && false) {                throw new RuntimeException("Can not install binder hook for " + mServiceName);            } else {                sCache.remove(mServiceName);                IBinder mServiceIBinder = ServiceManagerCompat.getService(mServiceName);                if (mServiceIBinder != null) {                    MyServiceManager.addOriginService(mServiceName, mServiceIBinder);                    Class clazz = mServiceIBinder.getClass();                    List<Class<?>> interfaces = Utils.getAllInterfaces(clazz);                    Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];                    IBinder mProxyServiceIBinder = (IBinder) MyProxy.newProxyInstance(clazz.getClassLoader(), ifs, this);                    sCache.put(mServiceName, mProxyServiceIBinder);                    MyServiceManager.addProxiedServiceCache(mServiceName, mProxyServiceIBinder);                }            }        }    }

首先把原始的service cache存起来,然后生成一个代理对象并通过反射替换掉cache中的对象,最后把这个代理对象也存起来。下次如果要真正和service进行通信,通过getOriginService()把原始的service cache拿出来用就行了。

3个类ServiceManagerBinderHook继承自ProxyHook,主要是用来hookgetService()checkService()这两个API。如果这两个API被调用,并且在mProxiedObjCache发现有对应的代理对象,则直接返回这个代理对象,参见它里面的ServiceManagerHookafterInvoke()方法(setFakedResult()会导致API的返回值被替换成proxiedObj):

            protected void afterInvoke(Object receiver, Method method, Object[] args, Object invokeResult) throws Throwable {                int index = 0;                if (args != null && args.length > index && args[index] instanceof String) {                    String servicename = ((String) args[index]);                    Object proxiedObj = MyServiceManager.getProxiedObj(servicename);                    if (proxiedObj != null) {                        setFakedResult(proxiedObj);                    }                }                Log.e("ServiceManagerBinderHook", "%s(%s)=%s", method.getName(), Arrays.toString(args), invokeResult);                super.afterInvoke(receiver, method, args, invokeResult);            }

看完这3个新面孔,我们来看一下BinderHook。首先BinderHook增加了一个新方法:

public abstract String getServiceName();

所有子类必须实现该方法,这个我们就可以知道需要hook哪个service

然后对比一下ProxyHookBinderHook,发现BinderHook自己已经实现了onInstall()方法,这样它的子类就不必实现该方法了。看一下它的onInstall()方法:

    protected void onInstall(ClassLoader classLoader) throws Throwable {        new ServiceManagerCacheBinderHook(mHostContext, getServiceName()).onInstall(classLoader);        mOldObj = getOldObj();        Class<?> clazz = mOldObj.getClass();        List<Class<?>> interfaces = Utils.getAllInterfaces(clazz);        Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];        Object proxiedObj = MyProxy.newProxyInstance(clazz.getClassLoader(), ifs, this);        MyServiceManager.addProxiedObj(getServiceName(), proxiedObj);    }

先调用ServiceManagerCacheBinderHookonInstall()方法更新一下service cache,然后生成一个新的代理对象放到mProxiedObjCache里。这样下次不管是从cache里取,还是直接通过binder调用,就都会返回我们的代理对象。

至此,Binder代理就分析完了。


三、IO重定向

IO重定向”,听起来是不是很高大上?其实所谓的“重定向”不过就是替换一下要访问的路径。插件程序的所有数据都是放在宿主程序的目录下的,方便统一管理。因此,当插件访问“/data/data/插件包名/xxx”时,需要把路径替换成“/data/data/插件宿主包名/Plugin/插件包名/data/插件包名/xxx”。

具体是实现在LibCoreHookHandle里,libcore主要是一些系统调用的实现(如open(), remove(), mkdir()等等),因此需要在进行系统调用之前把路径替换掉。看一下里面的BaseLibCore类的beforeInvoke()方法:

private abstract static class BaseLibCore extends HookedMethodHandler {        protected boolean beforeInvoke(Object receiver, Method method, Object[] args) throws Throwable {            int index = 0;            replace(args, index);            return super.beforeInvoke(receiver, method, args);        }}

这里调用了一个replace()方法:

        protected void replace(Object[] args, int index) {            if (args != null && args.length > index && args[index] instanceof String) {                String path = (String) args[index];                String newPath = tryReplacePath(path);                if (newPath != null) {                    args[index] = newPath;                }            }        }

遍历所有的参数,依次调用tryReplacePath()方法:

        private String tryReplacePath(String tarDir) {            if (tarDir != null && tarDir.length() > mDataDir.length() && !TextUtils.equals(tarDir, mDataDir) && tarDir.startsWith(mDataDir)) {                if (!tarDir.startsWith(mHostDataDir) && !TextUtils.equals(tarDir, mHostDataDir)) {                    String pkg = tarDir.substring(mDataDir.length() + 1);                    int index = pkg.indexOf("/");                    if (index > 0) {                        pkg = pkg.substring(0, index);                    }                    if (!TextUtils.equals(pkg, mHostPkg)) {                        tarDir = tarDir.replace(pkg, String.format("%s/Plugin/%s/data/%s", mHostPkg, pkg, pkg));                        return tarDir;                    }                }            }            return null;        }

这里完成了路径的替换工作,也就完成了“IO重定向”。

 

四、InstrumentationHook

最后再提一个特殊类型的hookInstrumentationHook。这个hook的目的主要是监控activity的生命周期,拦截一些关键的回调函数。实现也比较简单,直接替换掉全局静态变量mInstrumentation,没有对应的hook handle对象。参见它的onInstall()方法:

    protected void onInstall(ClassLoader classLoader) throws Throwable {        Object target = ActivityThreadCompat.currentActivityThread();        Class ActivityThreadClass = ActivityThreadCompat.activityThreadClass();         /*替换ActivityThread.mInstrumentation,拦截组件调度消息*/        Field mInstrumentationField = FieldUtils.getField(ActivityThreadClass, "mInstrumentation");        Instrumentation mInstrumentation = (Instrumentation) FieldUtils.readField(mInstrumentationField, target);        if (!PluginInstrumentation.class.isInstance(mInstrumentation)) {            PluginInstrumentation pit = new PluginInstrumentation(mHostContext, mInstrumentation);            pit.setEnable(isEnable());            mPluginInstrumentations.add(pit);            FieldUtils.writeField(mInstrumentationField, target, pit);            Log.i(TAG, "Install Instrumentation Hook old=%s,new=%s", mInstrumentationField, pit);        } else {            Log.i(TAG, "Instrumentation has installed,skip");        }    }

可以看到,这里把mInstrumentation替换成了一个PluginInstrumentation对象,而mInstrumentation本身成为了PluginInstrumentation对象的一个成员变量mTarget

这个PluginInstrumentation是继承自Instrumentation的,覆盖了父类了下面这些方法。这些方法里会增加一些额外的处理,最终通过mTarget完成系统服务的调用。

  • onActivityCreated()
  • onActivityOnNewIntent()
  • onActivityDestory()
  • callActivityOnCreate()
  • callActivityOnDestroy()
  • callActivityOnNewIntent()
  • callApplicationOnCreate()

除此以外,还有一个PluginCallbackHook,这个hook和后面的占坑部分密切相关,我们留到后面再做分析。

到这里Hook机制部分基本就分析完了,下一篇分析占坑部分。

2 0
原创粉丝点击