Android Hook 技術深入解析以及簡單實戰
1. 什麼是 Hook
Hook 英文翻譯過來就是「鉤子」的意思,那我們在什麼時候使用這個「鉤子」呢?在 Android 作業系統中系統維護著自己的一套事件分發機制。應用程式,包括應用觸發事件和後臺邏輯處理,也是根據事件流程一步步地向下執行。而「鉤子」的意思,就是在事件傳送到終點前截獲並監控事件的傳輸,像個鉤子鉤上事件一樣,並且能夠在鉤上事件時,處理一些自己特定的事件。

Hook 原理圖
Hook 的這個本領,使它能夠將自身的程式碼「融入」被勾住(Hook)的程式的程序中,成為目標程序的一個部分。API Hook 技術是一種用於改變 API 執行結果的技術,能夠將系統的 API 函式執行重定向。在 Android 系統中使用了沙箱機制,普通使用者程式的程序空間都是獨立的,程式的執行互不干擾。這就使我們希望通過一個程式改變其他程式的某些行為的想法不能直接實現,但是 Hook 的出現給我們開拓瞭解決此類問題的道路。當然,根據 Hook 物件與 Hook 後處理的事件方式不同,Hook 還分為不同的種類,比如訊息 Hook、API Hook 等。
2. 常用的 Hook 框架
- 關於 Android 中的 Hook 機制,大致有兩個方式:
- 要 root 許可權,直接 Hook 系統,可以幹掉所有的 App。
- 免 root 許可權,但是隻能 Hook 自身,對系統其它 App 無能為力。
- 幾種 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 層,這樣的相容性就非常好。
原理是這樣的,直接構造出新舊方法對應的虛擬機器資料結構,然後替換資訊寫到記憶體中即可。
3. 使用 Java 反射實現 API Hook
通過對 Android 平臺的虛擬機器注入與 Java 反射的方式,來改變 Android 虛擬機器呼叫函式的方式(ClassLoader),從而達到 Java 函式重定向的目的,這裡我們將此類操作稱為 Java API Hook。
下面通過 Hook View 的 OnClickListener 來說明 Hook 的使用方法。
首先進入 View 的 setOnClickListener 方法,我們看到 OnClickListener 物件被儲存在了一個叫做 ListenerInfo 的內部類裡,其中 mListenerInfo 是 View 的成員變數。ListeneInfo 裡面儲存了 View 的各種監聽事件,比如 OnClickListener、OnLongClickListener、OnKeyListener 等等。
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; }
我們的目標是 Hook OnClickListener,所以就要在給 View 設定監聽事件後,替換 OnClickListener 物件,注入自定義的操作。
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 設定 OnClickListener 後,執行 Hook 操作。點選按鈕後,日誌的列印結果是:Before click → onClick → After click。
Button btnSend = (Button) findViewById(R.id.btn_send); btnSend.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { log.info("onClick"); } }); hookOnClickListener(btnSend);
4. 使用 Hook 攔截應用內的通知
當應用內接入了眾多的 SDK,SDK 內部會使用系統服務 NotificationManager 傳送通知,這就導致通知難以管理和控制。現在我們就用 Hook 技術攔截部分通知,限制應用內的通知傳送操作。
傳送通知使用的是 NotificationManager 的 notify 方法,我們跟隨 API 進去看看。它會使用 INotificationManager 型別的物件,並呼叫其 enqueueNotificationWithTag 方法完成通知的傳送。
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) { } }
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 是個靜態成員變數,而且只會初始化一次。只要把 sService 替換成自定義的不就行了麼,確實如此。下面用到大量的 Java 反射和動態代理,特別要注意程式碼的書寫。
private 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 裡面操作。
@Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(newBase); hookNotificationManager(newBase); }
這樣我們就完成了對通知的攔截,可見 Hook 技術真的是非常強大,好多外掛化的原理都是建立在 Hook 之上的。
總結一下:
- Hook 的選擇點:靜態變數和單例,因為一旦建立物件,它們不容易變化,非常容易定位。
- Hook 過程:
- 尋找 Hook 點,原則是靜態變數或者單例物件,儘量 Hook public 的物件和方法。
- 選擇合適的代理方式,如果是介面可以用動態代理。
- 偷樑換柱——用代理物件替換原始物件。
-
Android 的 API 版本比較多,方法和類可能不一樣,所以要做好 API 的相容工作。
【附錄】

資料圖