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去看
這裏寫圖片描述
顯然我們又要到getBaseContext這個函數返回的對象去看。
不過可惜的是,它返回的只是一個base對象,從ContextThemeWrapper這個類名就不難看出,它只是一個包裝器,真正實現功能的肯定在另一個類中:
這裏寫圖片描述
它的CTOR:
這裏寫圖片描述

從我截的圖可以看出,初始化或者賦值mBase的話,只有通過構造函數或者attachBaseContext方法,不過,從第一節我們介紹ActivityThread的時候就提出了,activity只會在activity thread這個類中被實例化,所以,我們如果要找mBase被修改的位置,肯定要找到那個類(ActivityThread)並從中找線索

切換到ActivityThread.java
在這個類中有這樣一個函數:private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent)

他是用來轉載activity的,顯然,這裏是最有可能找到Activity產生實例的地方。
這裏寫圖片描述
哦?這裏有個newActivity哦,切進去看下
這裏寫圖片描述
確實,這裏是實例化Activity的,不過它並沒有調用有參數的構造函數,所以它的父類ContextThemeWrapper也就不會憑空賦一個值

那麼線索就只剩下attachBaseContext了,檢索下,我們又找到了這裏:
這裏寫圖片描述
在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裏面我們找到了想要的內容:

這裏寫圖片描述
哇哦,這裏都是調用了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相關的文章

所以我們還得看下本地服務代理的代碼:
這裏寫圖片描述
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();
            }
        });

效果:
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章