1. 程式人生 > >Android 外掛化之Hook機制

Android 外掛化之Hook機制

Android Hook簡介

什麼是Hook

Hook 英文翻譯過來就是「鉤子」的意思,就是在程式執行的過程中去擷取其中的資訊。Android 作業系統中系統維護著自己的一套事件分發機制,那麼Hook就是在事件傳送到終點前截獲並監控事件的傳輸。其原理示意圖如下:
這裡寫圖片描述

眾所周知,Android 系統中使用了沙箱機制,普通使用者程式的程序空間都是獨立的,程式的執行互不干擾,而程序之間要實現通訊需要藉助Android的Binder機制。
在Hook技術中,根據 Hook 物件與 Hook 後處理的事件方式不同,Hook 可以分為訊息 Hook、API Hook 等。

Hook 框架

在Android開發中,有以下常見的一些Hook框架:

通過替換 /system/bin/app_process 程式控制 Zygote 程序,使得 app_process 在啟動過程中會載入 XposedBridge.jar 這個 Jar 包,從而完成對 Zygote 程序及其建立的 Dalvik 虛擬機器的劫持。
Xposed 在開機的時候完成對所有的 Hook Function 的劫持,在原 Function 執行的前後加上自定義程式碼。

Cydia Substrate 框架為蘋果使用者提供了越獄相關的服務框架,當然也推出了 Android 版 。Cydia Substrate 是一個程式碼修改平臺,它可以修改任何程序的程式碼。不管是用 Java 還是 C/C++(native程式碼)編寫的,而 Xposed 只支援 Hook app_process 中的 Java 函式。

Legend 是 Android 免 Root 環境下的一個 Apk Hook 框架,該框架程式碼設計簡潔,通用性高,適合逆向工程時一些 Hook 場景。大部分的功能都放到了 Java 層,這樣的相容性就非常好。
原理是這樣的,直接構造出新舊方法對應的虛擬機器資料結構,然後替換資訊寫到記憶體中即可。

Hook基礎之反射

反射是運行於JVM中的程式檢測和修改執行時的一種行為,通過反射可以在執行時獲取物件的屬性和方法,這種動態獲取資訊以及動態呼叫物件方法的功能特性被稱為反射機制。
關於反射更詳細的內容可以檢視下面的連結:Java基礎之反射
為了方便後面理解,這裡先說下對於Hook的一些基本的常識:

Hook的選擇點
Hook時應儘量選擇靜態變數和單例物件,因為一旦建立物件,它們不容易變化,非常容易定位。

Hook流程

  1. 尋找 Hook 點,原則是靜態變數或者單例物件,儘量 Hook public 的物件和方法;
  2. 選擇合適的代理方式,如果是介面可以用動態代理;
  3. 使用代理物件替換原始物件。

1,使用Hook攔截點選事件

下面以如何HooK Android的OnClickListener來講解如何Hook API 。首先,我們看一下setOnClickListener的原始碼。

public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

可以發現,OnClickListener 物件被儲存在了一個叫做 ListenerInfo 的內部類裡,其中 mListenerInfo 是 View 的成員變數。而ListeneInfo 裡面儲存了 View 的各種監聽事件,比如 OnClickListener、OnLongClickListener、OnKeyListener 等等。

如果我們要Hook OnClickListener事件,可以給 View 設定監聽事件後,然後注入自定義的操作。

private void hookOnClickListener(View view) {
        try {
            // 得到 View 的 ListenerInfo 物件
            Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
            getListenerInfo.setAccessible(true);
            Object listenerInfo = getListenerInfo.invoke(view);
            // 得到 原始的 OnClickListener 物件
            Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
            Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
            mOnClickListener.setAccessible(true);
            View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
            // 用自定義的 OnClickListener 替換原始的 OnClickListener
            View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener);
            mOnClickListener.set(listenerInfo, hookedOnClickListener);
        } catch (Exception e) {
            log.warn("hook clickListener failed!", e);
        }
    }

    class HookedOnClickListener implements View.OnClickListener {
        private View.OnClickListener origin;

        HookedOnClickListener(View.OnClickListener origin) {
            this.origin = origin;
        }

        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show();
            log.info("Before click, do what you want to to.");
            if (origin != null) {
                origin.onClick(v);
            }
            log.info("After click, do what you want to to.");
        }
    }

到此,我們成功Hook 了 OnClickListener事件,然後我們可以使用下面的程式碼來進行呼叫。

Button btnSend = (Button) findViewById(R.id.btn_send);
        btnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                log.info("onClick");
            }
        });
        hookOnClickListener(btnSend);

Hook技術除了上面的作用外,還可以用於無痕埋點等功能。

2,使用 Hook 攔截通知

當我們在專案中使用眾多的SDK,而美國SDK內部可能會使用NotificationManager 傳送通知,這就導致通知難以管理和控制。傳送通知使用的是 NotificationManager 的notify方法,而NotificationManager 會返回一個INotificationManager 型別的物件,並呼叫其 enqueueNotificationWithTag 方法完成通知的傳送。以下是notify方法的原始碼:

public void notify(String tag, int id, Notification notification)
    {
        INotificationManager service = getService();
        …… // 省略部分程式碼
        try {
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    stripped, idOut, UserHandle.myUserId());
            if (id != idOut[0]) {
                Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]);
            }
        } catch (RemoteException e) {
        }
    }

而INotificationManager的原始碼如下:

private static INotificationManager sService;

    /** @hide */
    static public INotificationManager getService()
    {
        if (sService != null) {
            return sService;
        }
        IBinder b = ServiceManager.getService("notification");
        sService = INotificationManager.Stub.asInterface(b);
        return sService;
    }

INotificationManager 是跨程序通訊的 Binder 類,sService 是 NMS(NotificationManagerService) 在客戶端的代理,也就是說傳送通知後首先委託給 sService,由它傳遞給 NMS。由上面的原始碼可以發現 sService 是一個靜態成員變數,所以該物件只會初始化一次。所以,要想Hook NotificationManager,可以通過反射拿到sService。核心程式碼如下:

rivate void hookNotificationManager(Context context) {
        try {
            NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            // 得到系統的 sService
            Method getService = NotificationManager.class.getDeclaredMethod("getService");
            getService.setAccessible(true);
            final Object sService = getService.invoke(notificationManager);

            Class iNotiMngClz = Class.forName("android.app.INotificationManager");
            // 動態代理 INotificationManager
            Object proxyNotiMng = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{iNotiMngClz}, new InvocationHandler() {

                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    log.debug("invoke(). method:{}", method);
                    if (args != null && args.length > 0) {
                        for (Object arg : args) {
                            log.debug("type:{}, arg:{}", arg != null ? arg.getClass() : null, arg);
                        }
                    }
                    // 操作交由 sService 處理,不攔截通知
                    // return method.invoke(sService, args);
                    // 攔截通知,什麼也不做
                    return null;
                    // 或者是根據通知的 Tag 和 ID 進行篩選
                }
            });
            // 替換 sService
            Field sServiceField = NotificationManager.class.getDeclaredField("sService");
            sServiceField.setAccessible(true);
            sServiceField.set(notificationManager, proxyNotiMng);
        } catch (Exception e) {
            log.warn("Hook NotificationManager failed!", e);
        }
    }

對於Hook,應當儘可能的早,例如可以在attachBaseContext進行Hook。

3,Hook Activity

熟悉Android的Activity啟動流程的同學都知道,啟動Activity是由Instrumentation類的execStartActivity來執行的,而execStartActivity函式有一個核心的物件ActivityManagerService(AMS)。

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        ....
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);

        //通過ActivityManagerNative.getDefault()獲取一個物件,開始啟動新的Activity
            int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);


            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }

而ActivityManagerNative是一個抽象類,它實現了IActivityManager介面。

public abstract class ActivityManagerNative extends Binder implements IActivityManager

也就是說,ActivityManagerNative是為了遠端服務通訊做準備的”Stub”類,一個完整的AID L有兩部分,一個是個跟服務端通訊的Stub,一個是跟客戶端通訊的Proxy。

閱讀原始碼以發現,ActivityManagerNative就是Stub,除此之外,ActivityManagerNative 檔案中還有個ActivityManagerProxy,而更加詳細的內容本文就不多講解了。不過要實現Hook效果,需要注意下IActivityManager的getDefault函式。

gDefault()是一個單例,ServiceManager通過獲取到AMS遠端服務的Binder物件,然後使用asInterface方法轉化成本地化物件。涉及的核心程式碼如下:

 private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
        protected IActivityManager create() {
            IBinder b = ServiceManager.getService("activity");
            if (false) {
                Log.v("ActivityManager", "default service binder = " + b);
            }
            IActivityManager am = asInterface(b);
            if (false) {
                Log.v("ActivityManager", "default service = " + am);
            }
            return am;
        }

不過Activity的啟動過程可以使用下面的核心類來幫助理解:

ActivityManagerService、ActivityManagerNative、ActivityManagerProxy、ActivityThread、ViewRootImpl、PhoneWindow

下面繼續看,那麼我們究竟如何實現Hook Activity呢?很簡單,只要通過反射拿到IActivityManager物件,然後拿到ActivityManager物件,然後通過動態代理,用代理物件替換掉真實的ActivityManager即可。這也是很多元件化框架的使用的原理。

下面是網上提供的一個例項,通過Hook Activity增加Log輸出。

public class HookUtil {
    private Class<?> proxyActivity;
    private Context context;

    public HookUtil(Class<?> proxyActivity, Context context) {
        this.proxyActivity = proxyActivity;
        this.context = context;
    }

    public void hookAms() {
        try {
            //通過完整的類名拿到class
            Class<?> ActivityManagerNativeClss = Class.forName("android.app.ActivityManagerNative");
            //拿到這個類的某個field
            Field gDefaultFiled = ActivityManagerNativeClss.getDeclaredField("gDefault");
            //field為private,設定為可訪問的
            gDefaultFiled.setAccessible(true);
            //拿到ActivityManagerNative的gDefault的Field的例項
            //gDefault為static型別,不需要傳入具體的物件
            Object gDefaultFiledValue = gDefaultFiled.get(null);

            //拿到Singleton類
            Class<?> SingletonClass = Class.forName("android.util.Singleton");
            //拿到類對應的field
            Field mInstanceField = SingletonClass.getDeclaredField("mInstance");
            //field是private
            mInstanceField.setAccessible(true);
            //gDefaultFiledValue是Singleton的例項物件
            //拿到IActivityManager
            Object iActivityManagerObject = mInstanceField.get(gDefaultFiledValue);

            AmsInvocationHandler handler = new AmsInvocationHandler(iActivityManagerObject);
            Class<?> IActivityManagerIntercept = Class.forName("android.app.IActivityManager");
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class<?>[]{IActivityManagerIntercept}, handler);
            mInstanceField.set(gDefaultFiledValue, proxy);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private class AmsInvocationHandler implements InvocationHandler {

        private Object iActivityManagerObject;

        private AmsInvocationHandler(Object iActivityManagerObject) {
            this.iActivityManagerObject = iActivityManagerObject;
        }

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

            Log.i("HookUtil", method.getName());
            //新增日誌
            if ("startActivity".contains(method.getName())) {
                Log.e("HookUtil","Activity Hook已經開始啟動");
            }
            return method.invoke(iActivityManagerObject, args);
        }
    }
}

然後使用的時候在Application註冊下即可:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        HookUtil hookUtil=new HookUtil(SecondActivity.class, this);
        hookUtil.hookAms();
    }
}