Android插件化开发-hook 系统服务(通过binder修改粘贴板服务行为)

如果您还没有阅读第一部分的内容,这篇文章不需往下读,在阅读第一部分后才能继续下面的内容:Hook动态代理

基于上面的一篇博客,我们学习了代理的概念,以及如何寻找Hook点。本篇博客将继续拓展前文,不过这次内容要深入很多,这些都是继续学习插件化的基础,为了避免长篇的介绍代理这些枯燥的概念,我特意把它分开来讲,难度一次提升,希望读者能够耐心阅读。

之前我们解释代理设计模式的时候,用的是小明打官司的例子,通过这个例子我们发现,如果一个对象需要代理,我们就需要定义很多个代理类,这对于大型项目的话,显然有点不太现实(程序员也是人啊),所以,这里又萌生动态代理的概念。

这种机制可以理解为jvm运行时动态为我们生成一个代理类,我们只需简单的实现InvocationHandler接口即可
我们看下demo:

  //定义我们自己的接口
    interface MyInterface {
        void foo();
    }

    interface MyInterface2 {

    }

    //实现类
    static class MyObject implements MyInterface , MyInterface2{

        @Override
        public void foo() {
            System.out.print("proxy foo");
        }
    }

    //动态代理
    static class MyInvocation implements InvocationHandler {

        public MyInvocation() {
        }

        Object m_object;

        public MyInvocation(Object object) {
            m_object = object;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            //当调用foo函数之前我们打印一段haha字符串
            if("foo".equals(method.getName())) {
                System.out.println("haha");
            }

            return method.invoke(m_object, args);
        }
    }

    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        System.out.println(myObject);
        //第一个参数是指定类加载器,第二个参数指定返回的代理对象要实现的接口
        //第三个参数用于处理分发的函数调用,我们可以如本例一样,在执行foo函数之前打印haha字符串
        MyInterface myInterface = (MyInterface) Proxy.newProxyInstance(myObject.getClass().getClassLoader(),
                new Class[]{MyInterface.class, MyInterface2.class}, new MyInvocation(myObject));
        myInterface.foo();
    }

还是很好理解的,所以我们可以开始本文主题了。

我们使用插件的目的,就是希望在无需重新编译app的情况下,升级添加某些功能,但是,如果我们使用插件来达到我们的目的,我们并不能正常使用一些系统服务,比如:粘贴板,网络管理器。这是不能忍的,既然我们使用插件,就应该和平时coding一样,没有任何差异——我们能够调用任何我们能够获取的系统服务。所以,这一节我们就要解决hook系统服务的问题,保证我们插件能够正常使用。

回想一下,如果我们平时需要调用系统服务,我们如何实现呢?
like this:

ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);

我们本节就是打算hook粘贴板服务,试着让粘贴板里面一直有我们自定义的内容

Hook粘贴板服务

在开始之前,我们还是和第一节一样,切进去看下,一步步的寻找hook点,还记得我们第一节寻找hook点的原则吗——静态或单例对象

我们切换到activity的getSystemService中:

 @Override
    public Object getSystemService(@ServiceName @NonNull String name) {
        if (getBaseContext() == null) {
            throw new IllegalStateException(
                    "System services not available to Activities before onCreate()");
        }

        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ensureSearchManager();
            return mSearchManager;
        }
        return super.getSystemService(name);
    }

这里调用了Activity的函数,显然,我们如果使用粘贴板服务的话,还需调用父类的getSystemService方法。切换到Activity的父类——ContextThemeWrapper去看
Android插件化开发-hook 系统服务(通过binder修改粘贴板服务行为)_第1张图片
显然我们又要到getBaseContext这个函数返回的对象去看。
不过可惜的是,它返回的只是一个base对象,从ContextThemeWrapper这个类名就不难看出,它只是一个包装器,真正实现功能的肯定在另一个类中:

它的CTOR:
Android插件化开发-hook 系统服务(通过binder修改粘贴板服务行为)_第2张图片

从我截的图可以看出,初始化或者赋值mBase的话,只有通过构造函数或者attachBaseContext方法,不过,从第一节我们介绍ActivityThread的时候就提出了,activity只会在activity thread这个类中被实例化,所以,我们如果要找mBase被修改的位置,肯定要找到那个类(ActivityThread)并从中找线索

切换到ActivityThread.java
在这个类中有这样一个函数:private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent)

他是用来转载activity的,显然,这里是最有可能找到Activity产生实例的地方。
Android插件化开发-hook 系统服务(通过binder修改粘贴板服务行为)_第3张图片
哦?这里有个newActivity哦,切进去看下
Android插件化开发-hook 系统服务(通过binder修改粘贴板服务行为)_第4张图片
确实,这里是实例化Activity的,不过它并没有调用有参数的构造函数,所以它的父类ContextThemeWrapper也就不会凭空赋一个值

那么线索就只剩下attachBaseContext了,检索下,我们又找到了这里:
Android插件化开发-hook 系统服务(通过binder修改粘贴板服务行为)_第5张图片
在Activity的attach这个函数中,调用了attachBaseContext,而这个显然在ActivityThread这个类中出现过,我们再次切换到ActivityThread:

  private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

      ...

        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }

        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);

            if (localLOGV) Slog.v(TAG, "Performing launch of " + r);
            if (localLOGV) Slog.v(
                    TAG, r + ": app=" + app
                    + ", appName=" + app.getPackageName()
                    + ", pkg=" + r.packageInfo.getPackageName()
                    + ", comp=" + r.intent.getComponent().toShortString()
                    + ", dir=" + r.packageInfo.getAppDir());

            if (activity != null) {
                Context appContext = createBaseContextForActivity(r, activity);
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                        + r.activityInfo.name + " with config " + config);
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor);

               ...

        return activity;
    }


createBaseContextForActivity的返回值,被作为参数传入到了Activity的attach。
我们在切入到createBaseContextForActivity中去查看:

    private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
        int displayId = Display.DEFAULT_DISPLAY;
        try {
            displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token);
        } catch (RemoteException e) {
        }

        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, displayId, r.overrideConfig);
        appContext.setOuterContext(activity);
        Context baseContext = appContext;

        final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
        // For debugging purposes, if the activity's package name contains the value of
        // the "debug.use-second-display" system property as a substring, then show
        // its content on a secondary display if there is one.
        String pkgName = SystemProperties.get("debug.second-display.pkg");
        if (pkgName != null && !pkgName.isEmpty()
                && r.packageInfo.mPackageName.contains(pkgName)) {
            for (int id : dm.getDisplayIds()) {
                if (id != Display.DEFAULT_DISPLAY) {
                    Display display =
                            dm.getCompatibleDisplay(id, appContext.getDisplayAdjustments(id));
                    baseContext = appContext.createDisplayContext(display);
                    break;
                }
            }
        }
        return baseContext;
    }

函数生成了一个ContextImpl对象,并且赋给baseContext,然后返回,哈哈,终于找到了Context这个接口真实的实现类!ContextImpl。

我们先总结一下刚刚到底发生了什么:
我们从Activity的getSystemService一直跟踪到了它的父类ContextThemeWrapper,想再往下找的时候,发现ContextThemeWrapper只是一个包装器,真实的实现不在这里,所以我们又要找到真实的实现位置。一路跟踪,最后找到了ContextImpl

我们现在切换到ContextImpl中:

  @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

看来又要看看SystemServiceRegistry了,不过还好,在SystemServiceRegistry里面我们找到了想要的内容:

Android插件化开发-hook 系统服务(通过binder修改粘贴板服务行为)_第6张图片
哇哦,这里都是调用了ServiceManager的Binder,然后 I*Manager.*.asInterface作为接口返回,最后再生成具体的java平台类!

我们看下ServiceManager.java:

 public static IBinder getService(String name) {
50         try {
51             IBinder service = sCache.get(name);//静态变量哦!
52             if (service != null) {
53                 return service;
54             } else {
55                 return getIServiceManager().getService(name);
56             }
57         } catch (RemoteException e) {
58             Log.e(TAG, "error in getService", e);
59         }
60         return null;
61     }

哇塞,看到没sCache,从命名就可以看出它是一个静态变量,回忆之前我们寻找hook点时候指出,最好的hook点就是静态或者单例对象,毕竟他们只有一个,且相对不会变化。那么思路来了,如果我们能够生成一自定义的ibinder对象,然后存放到sCache中,不久能够Hook系统服务了吗?简直完美啊!

但是值得注意的是,由于粘贴板服务和我们的app不在同一个进程里面,这就意味着,及时我们hook了远程的粘贴板服务,我们并不能修改当前app中binder代理的行为。这里需要读者熟悉android的aidl多进程通信。如果不太懂,就去阅读下aidl相关的文章

所以我们还得看下本地服务代理的代码:
Android插件化开发-hook 系统服务(通过binder修改粘贴板服务行为)_第7张图片
101行获得了远程服务的binder,之后,通过IClipboard.Stub.asInterface函数,我们获得了远程服务的接口。之后就能像调用本地代码一样调用远程服务了。

我们再到102行函数里查看:

26  public static android.content.IClipboard asInterface(android.os.IBinder obj)
27  {
28      if ((obj==null)) {
29          return null;
30      }

        //调用IBinder的queryLocalInterface函数,获得服务操作的接口
31      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);

        //如果不为空 那么就返回
32      if (((iin!=null)&&(iin instanceof android.content.IClipboard))) {
33          return ((android.content.IClipboard)iin);
34      }

        //否则创建一个服务代理对象
35      return new android.content.IClipboard.Stub.Proxy(obj);
36  }

显然我们一定不能让31行的iin为空,因为为空它会返回系统自己定义的服务代理对象。那个对象的行为我们无法修改。我们只能是的31永远不为空,而返回的对象正好可以是我们定制过的对象!

思路来了,首先我们hook ServiceManger中的sCache所关联ClipboardService的IBinder对象,使之在调用queryLocalInterface的时候返回一个永远不为空的android.os.IInterface对象,我们修改那个返回对象的行为就达到hook ClipboardService的目的!

代码:

public class HookApplication extends Application {


    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        try {
            //获得ServiceManger的class
            Class<?> serviceManager = Class.forName("android.os.ServiceManager", false, getClassLoader());
            //找到getService静态方法
            Method getService = serviceManager.getDeclaredMethod("getService", String.class);
            //获得原始的binder对象
            IBinder rawBinder = (IBinder) getService.invoke(null, CLIPBOARD_SERVICE);

            //定制我们自己的IBinder对象
            ProxyBinder proxyBinder = new ProxyBinder(rawBinder, this);
            IBinder proxy = (IBinder) Proxy.newProxyInstance(getClassLoader(), new Class[]{IBinder.class},
                    proxyBinder);

            //注入到ServiceManager的sCache中
            Field field = serviceManager.getDeclaredField("sCache");
            field.setAccessible(true);
            Map<String, IBinder> map = (Map<String, IBinder>) field.get(null);
            map.put(CLIPBOARD_SERVICE, proxy);
        } catch (ClassNotFoundException | IllegalAccessException | NoSuchMethodException
                | InvocationTargetException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

ProxyBinder的源码:

/** * Created by chan on 16/4/10. */
public class ProxyBinder implements InvocationHandler {

    private Class<?> m_iClipboardInterfaceClz;
    private Object m_iClipboardInterface;
    private Object m_base;
    private Context m_context;

    public ProxyBinder(Object base, Context context)
            throws ClassNotFoundException, NoSuchMethodException,
            InvocationTargetException,
            IllegalAccessException {

        m_base = base;
        m_context = context;

        //获得IClipboard的class
        m_iClipboardInterfaceClz = Class.forName("android.content.IClipboard",
                false, context.getClassLoader());

        //获得服务的Stub 这是aidl内容 需读者熟悉aidl
        Class<?> clipboardServiceStub = Class.forName("android.content.IClipboard$Stub",
                false, context.getClassLoader());

        //获取stub的asInterface静态方法
        Method asInterface = clipboardServiceStub.getDeclaredMethod("asInterface", IBinder.class);

        //获得android.content.IClipboard实例 这是系统默认的实例
        m_iClipboardInterface = asInterface.invoke(null, base);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        //修改queryLocalInterface的行为
        //我们动态的修改为我们自定义的IBinder对象
        if ("queryLocalInterface".equals(method.getName())) {
            return Proxy.newProxyInstance(m_context.getClassLoader(),
                    new Class[]{IBinder.class, m_iClipboardInterfaceClz, IInterface.class},
                    new BinderHook(m_iClipboardInterface));
        }

        //其他的操作都是调用跟原来一样的操作
        return method.invoke(m_base, args);
    }
}

BinderHook.java:


/** * Created by chan on 16/4/10. */
public class BinderHook implements InvocationHandler {
    private Object m_base;

    public BinderHook(Object iClipboardInterface) {
        m_base = iClipboardInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        //当调用getPrimaryClip的时候 我们始终返回我们自己定义的一段字符串
        if ("getPrimaryClip".equals(method.getName())) {
            return ClipData.newPlainText(null, "I am crazy");
        }

        //使得客户端一直默认粘贴板中有东西
        if ("getPrimaryClip".equals(method.getName())) {
            return true;
        }

        //其它的操作都还是一样调用远程的服务接口
        return method.invoke(m_base, args);
    }
}

demo代码:

  findViewById(R.id.id_start).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ClipData.Item item = m_clipboardManager.getPrimaryClip().getItemAt(0);
                Toast.makeText(MainActivity.this, item.getText(), Toast.LENGTH_SHORT).show();
            }
        });

效果:
Android插件化开发-hook 系统服务(通过binder修改粘贴板服务行为)_第8张图片

你可能感兴趣的:(android,插件,Android插件,hook)