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 方案:
- Xposed
通過替換 /system/bin/app_process 程序控制 Zygote 進程,使得 app_process 在啟動過程中會加載 XposedBridge.jar 這個 Jar 包,從而完成對 Zygote 進程及其創建的 Dalvik 虛擬機的劫持。
Xposed 在開機的時候完成對所有的 Hook Function 的劫持,在原 Function 執行的前後加上自定義代碼。
-
Cydia Substrate
Cydia Substrate 框架為蘋果用戶提供了越獄相關的服務框架,當然也推出了 Android 版 。Cydia Substrate 是一個代碼修改平臺,它可以修改任何進程的代碼。不管是用 Java 還是 C/C++(native代碼)編寫的,而 Xposed 只支持 Hook app_process 中的 Java 函數。
-
Legend
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 等等。
1 public void setOnClickListener(@Nullable OnClickListener l) { 2 if (!isClickable()) { 3 setClickable(true); 4 } 5 getListenerInfo().mOnClickListener = l; 6 } 7 8 ListenerInfo getListenerInfo() { 9 if (mListenerInfo != null) { 10 return mListenerInfo; 11 } 12 mListenerInfo = new ListenerInfo(); 13 return mListenerInfo; 14 }
我們的目標是 Hook OnClickListener,所以就要在給 View 設置監聽事件後,替換 OnClickListener 對象,註入自定義的操作。
1 private void hookOnClickListener(View view) { 2 try { 3 // 得到 View 的 ListenerInfo 對象 4 Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo"); 5 getListenerInfo.setAccessible(true); 6 Object listenerInfo = getListenerInfo.invoke(view); 7 // 得到 原始的 OnClickListener 對象 8 Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo"); 9 Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener"); 10 mOnClickListener.setAccessible(true); 11 View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo); 12 // 用自定義的 OnClickListener 替換原始的 OnClickListener 13 View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener); 14 mOnClickListener.set(listenerInfo, hookedOnClickListener); 15 } catch (Exception e) { 16 log.warn("hook clickListener failed!", e); 17 } 18 } 19 20 class HookedOnClickListener implements View.OnClickListener { 21 private View.OnClickListener origin; 22 23 HookedOnClickListener(View.OnClickListener origin) { 24 this.origin = origin; 25 } 26 27 @Override 28 public void onClick(View v) { 29 Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show(); 30 log.info("Before click, do what you want to to."); 31 if (origin != null) { 32 origin.onClick(v); 33 } 34 log.info("After click, do what you want to to."); 35 } 36 }
到這裏,我們成功 Hook 了 OnClickListener,在點擊之前和點擊之後可以執行某些操作,達到了我們的目的。下面是調用的部分,在給 Button 設置 OnClickListener 後,執行 Hook 操作。點擊按鈕後,日誌的打印結果是:Before click → onClick → After click。
1 Button btnSend = (Button) findViewById(R.id.btn_send); 2 btnSend.setOnClickListener(new View.OnClickListener() { 3 @Override 4 public void onClick(View v) { 5 log.info("onClick"); 6 } 7 }); 8 hookOnClickListener(btnSend);
4. 使用 Hook 攔截應用內的通知
當應用內接入了眾多的 SDK,SDK 內部會使用系統服務 NotificationManager 發送通知,這就導致通知難以管理和控制。現在我們就用 Hook 技術攔截部分通知,限制應用內的通知發送操作。
發送通知使用的是 NotificationManager 的 notify 方法,我們跟隨 API 進去看看。它會使用 INotificationManager 類型的對象,並調用其 enqueueNotificationWithTag 方法完成通知的發送。
1 public void notify(String tag, int id, Notification notification) 2 { 3 INotificationManager service = getService(); 4 …… // 省略部分代碼 5 try { 6 service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, 7 stripped, idOut, UserHandle.myUserId()); 8 if (id != idOut[0]) { 9 Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]); 10 } 11 } catch (RemoteException e) { 12 } 13 } 14 private static INotificationManager sService; 15 16 /** @hide */ 17 static public INotificationManager getService() 18 { 19 if (sService != null) { 20 return sService; 21 } 22 IBinder b = ServiceManager.getService("notification"); 23 sService = INotificationManager.Stub.asInterface(b); 24 return sService; 25 }
INotificationManager 是跨進程通信的 Binder 類,sService 是 NMS(NotificationManagerService) 在客戶端的代理,發送通知要委托給 sService,由它傳遞給 NMS,具體的原理在這裏不再細究,感興趣的可以了解系統服務和應用的通信過程。
我們發現 sService 是個靜態成員變量,而且只會初始化一次。只要把 sService 替換成自定義的不就行了麽,確實如此。下面用到大量的 Java 反射和動態代理,特別要註意代碼的書寫。
1 private void hookNotificationManager(Context context) { 2 try { 3 NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 4 // 得到系統的 sService 5 Method getService = NotificationManager.class.getDeclaredMethod("getService"); 6 getService.setAccessible(true); 7 final Object sService = getService.invoke(notificationManager); 8 9 Class iNotiMngClz = Class.forName("android.app.INotificationManager"); 10 // 動態代理 INotificationManager 11 Object proxyNotiMng = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{iNotiMngClz}, new InvocationHandler() { 12 13 @Override 14 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 15 log.debug("invoke(). method:{}", method); 16 if (args != null && args.length > 0) { 17 for (Object arg : args) { 18 log.debug("type:{}, arg:{}", arg != null ? arg.getClass() : null, arg); 19 } 20 } 21 // 操作交由 sService 處理,不攔截通知 22 // return method.invoke(sService, args); 23 // 攔截通知,什麽也不做 24 return null; 25 // 或者是根據通知的 Tag 和 ID 進行篩選 26 } 27 }); 28 // 替換 sService 29 Field sServiceField = NotificationManager.class.getDeclaredField("sService"); 30 sServiceField.setAccessible(true); 31 sServiceField.set(notificationManager, proxyNotiMng); 32 } catch (Exception e) { 33 log.warn("Hook NotificationManager failed!", e); 34 } 35 }
Hook 的時機還是盡量要早,我們在 attachBaseContext 裏面操作。
1 @Override 2 protected void attachBaseContext(Context newBase) { 3 super.attachBaseContext(newBase); 4 hookNotificationManager(newBase); 5 }
這樣我們就完成了對通知的攔截,可見 Hook 技術真的是非常強大,好多插件化的原理都是建立在 Hook 之上的。
總結一下:
- Hook 的選擇點:靜態變量和單例,因為一旦創建對象,它們不容易變化,非常容易定位。
- Hook 過程:
- 尋找 Hook 點,原則是靜態變量或者單例對象,盡量 Hook public 的對象和方法。
- 選擇合適的代理方式,如果是接口可以用動態代理。
- 偷梁換柱——用代理對象替換原始對象。
- Android 的 API 版本比較多,方法和類可能不一樣,所以要做好 API 的兼容工作。
參考文章:
- 動態註入技術
- Android插件化原理解析——Hook機制之動態代理
使用代理機制進行API Hook進而達到方法增強是框架的常用手段,比如J2EE框架Spring通過動態代理優雅地實現了AOP編程,極大地提升了Web開發效率;同樣,插件框架也廣泛使用了代理機制來增強系統API從而達到插件化的目的。本文將帶你了解基於動態代理的Hook機制。
閱讀本文之前,可以先clone一份 understand-plugin-framework,參考此項目的dynamic-proxy-hook
模塊。另外,插件框架原理解析系列文章見索引。
二、Hook機制之動態代理
代理是什麽
為什麽需要代理呢?其實這個代理與日常生活中的“代理”,“中介”差不多;比如你想海淘買東西,總不可能親自飛到國外去購物吧,這時候我們使用第三方海淘服務比如惠惠購物助手等;同樣拿購物為例,有時候第三方購物會有折扣比如當初的米折網,這時候我們可以少花點錢;當然有時候這個“代理”比較坑,坑我們的錢,坑我們的貨。
從這個例子可以看出來,代理可以實現方法增強,比如常用的日誌,緩存等;也可以實現方法攔截,通過代理方法修改原方法的參數和返回值,從而實現某種不可告人的目的~接下來我們用代碼解釋一下。
靜態代理
靜態代理,是最原始的代理方式;假設我們有一個購物的接口,如下:
1 public interface Shopping { 2 Object[] doShopping(long money); 3 }
它有一個原始的實現,我們可以理解為親自,直接去商店購物:
1 public class ShoppingImpl implements Shopping { 2 @Override 3 public Object[] doShopping(long money) { 4 System.out.println("逛淘寶 ,逛商場,買買買!!"); 5 System.out.println(String.format("花了%s塊錢", money)); 6 return new Object[] { "鞋子", "衣服", "零食" }; 7 } 8 }
好了,現在我們自己沒時間但是需要買東西,於是我們就找了個代理幫我們買:
1 public class ProxyShopping implements Shopping { 2 3 Shopping base; 4 5 ProxyShopping(Shopping base) { 6 this.base = base; 7 } 8 9 @Override 10 public Object[] doShopping(long money) { 11 12 // 先黑點錢(修改輸入參數) 13 long readCost = (long) (money * 0.5); 14 15 System.out.println(String.format("花了%s塊錢", readCost)); 16 17 // 幫忙買東西 18 Object[] things = base.doShopping(readCost); 19 20 // 偷梁換柱(修改返回值) 21 if (things != null && things.length > 1) { 22 things[0] = "被掉包的東西!!"; 23 } 24 25 return things; 26 }
很不幸,我們找的這個代理有點坑,坑了我們的錢還坑了我們的貨;先忍忍。
動態代理
傳統的靜態代理模式需要為每一個需要代理的類寫一個代理類,如果需要代理的類有幾百個那不是要累死?為了更優雅地實現代理模式,JDK提供了動態代理方式,可以簡單理解為JVM可以在運行時幫我們動態生成一系列的代理類,這樣我們就不需要手寫每一個靜態的代理類了。依然以購物為例,用動態代理實現如下:
1 public static void main(String[] args) { 2 Shopping women = new ShoppingImpl(); 3 // 正常購物 4 System.out.println(Arrays.toString(women.doShopping(100))); 5 // 招代理 6 women = (Shopping) Proxy.newProxyInstance(Shopping.class.getClassLoader(), 7 women.getClass().getInterfaces(), new ShoppingHandler(women)); 8 9 System.out.println(Arrays.toString(women.doShopping(100))); 10 }
動態代理主要處理InvocationHandler
和Proxy
類;完整代碼可以見github
代理Hook
我們知道代理有比原始對象更強大的能力,比如飛到國外買東西,比如坑錢坑貨;那麽很自然,如果我們自己創建代理對象,然後把原始對象替換為我們的代理對象,那麽就可以在這個代理對象為所欲為了;修改參數,替換返回值,我們稱之為Hook。
下面我們Hook掉startActivity
這個方法,使得每次調用這個方法之前輸出一條日誌;(當然,這個輸入日誌有點點弱,只是為了展示原理;只要你想,你想可以替換參數,攔截這個startActivity
過程,使得調用它導致啟動某個別的Activity,指鹿為馬!)
首先我們得找到被Hook的對象,我稱之為Hook點;什麽樣的對象比較好Hook呢?自然是容易找到的對象。什麽樣的對象容易找到?靜態變量和單例;在一個進程之內,靜態變量和單例變量是相對不容易發生變化的,因此非常容易定位,而普通的對象則要麽無法標誌,要麽容易改變。我們根據這個原則找到所謂的Hook點。
然後我們分析一下startActivity
的調用鏈,找出合適的Hook點。我們知道對於Context.startActivity
(Activity.startActivity的調用鏈與之不同),由於Context
的實現實際上是ContextImpl
;我們看ConetxtImpl
類的startActivity
方法:
1 @Override 2 public void startActivity(Intent intent, Bundle options) { 3 warnIfCallingFromSystemProcess(); 4 if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) { 5 throw new AndroidRuntimeException( 6 "Calling startActivity() from outside of an Activity " 7 + " context requires the FLAG_ACTIVITY_NEW_TASK flag." 8 + " Is this really what you want?"); 9 } 10 mMainThread.getInstrumentation().execStartActivity( 11 getOuterContext(), mMainThread.getApplicationThread(), null, 12 (Activity)null, intent, -1, options); 13 }
接下來就是想要Hook掉我們的主線程對象,也就是把這個主線程對象裏面的mInstrumentation
給替換成我們修改過的代理對象;要替換主線程對象裏面的字段,首先我們得拿到主線程對象的引用,如何獲取呢?ActivityThread
類裏面有一個靜態方法currentActivityThread
可以幫助我們拿到這個對象類;但是ActivityThread
是一個隱藏類,我們需要用反射去獲取,代碼如下:這裏,實際上使用了ActivityThread
類的mInstrumentation
成員的execStartActivity
方法;註意到,ActivityThread
實際上是主線程,而主線程一個進程只有一個,因此這裏是一個良好的Hook點。
1 // 先獲取到當前的ActivityThread對象 2 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); 3 Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); 4 currentActivityThreadMethod.setAccessible(true); 5 Object currentActivityThread = currentActivityThreadMethod.invoke(null);
1 public class EvilInstrumentation extends Instrumentation { 2 3 private static final String TAG = "EvilInstrumentation"; 4 5 // ActivityThread中原始的對象, 保存起來 6 Instrumentation mBase; 7 8 public EvilInstrumentation(Instrumentation base) { 9 mBase = base; 10 } 11 12 public ActivityResult execStartActivity( 13 Context who, IBinder contextThread, IBinder token, Activity target, 14 Intent intent, int requestCode, Bundle options) { 15 16 // Hook之前, XXX到此一遊! 17 Log.d(TAG, "\n執行了startActivity, 參數如下: \n" + "who = [" + who + "], " + 18 "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " + 19 "\ntarget = [" + target + "], \nintent = [" + intent + 20 "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]"); 21 22 // 開始調用原始的方法, 調不調用隨你,但是不調用的話, 所有的startActivity都失效了. 23 // 由於這個方法是隱藏的,因此需要使用反射調用;首先找到這個方法 24 try { 25 Method execStartActivity = Instrumentation.class.getDeclaredMethod( 26 "execStartActivity", 27 Context.class, IBinder.class, IBinder.class, Activity.class, 28 Intent.class, int.class, Bundle.class); 29 execStartActivity.setAccessible(true); 30 return (ActivityResult) execStartActivity.invoke(mBase, who, 31 contextThread, token, target, intent, requestCode, options); 32 } catch (Exception e) { 33 // 某該死的rom修改了 需要手動適配 34 throw new RuntimeException("do not support!!! pls adapt it"); 35 } 36 } 37 }
拿到這個currentActivityThread
之後,我們需要修改它的mInstrumentation
這個字段為我們的代理對象,我們先實現這個代理對象,由於JDK動態代理只支持接口,而這個Instrumentation
是一個類,沒辦法,我們只有手動寫靜態代理類,覆蓋掉原始的方法即可。(cglib
可以做到基於類的動態代理,這裏先不介紹)
Ok,有了代理對象,我們要做的就是偷梁換柱!代碼比較簡單,采用反射直接修改:
1 public static void attachContext() throws Exception{ 2 // 先獲取到當前的ActivityThread對象 3 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); 4 Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); 5 currentActivityThreadMethod.setAccessible(true); 6 Object currentActivityThread = currentActivityThreadMethod.invoke(null); 7 8 // 拿到原始的 mInstrumentation字段 9 Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation"); 10 mInstrumentationField.setAccessible(true); 11 Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread); 12 13 // 創建代理對象 14 Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation); 15 16 // 偷梁換柱 17 mInstrumentationField.set(currentActivityThread, evilInstrumentation); 18 }
好了,我們啟動一個Activity測試一下,結果如下:
可見,Hook確實成功了!這就是使用代理進行Hook的原理——偷梁換柱。整個Hook過程簡要總結如下:
- 尋找Hook點,原則是靜態變量或者單例對象,盡量Hook pulic的對象和方法,非public不保證每個版本都一樣,需要適配。
- 選擇合適的代理方式,如果是接口可以用動態代理;如果是類可以手動寫代理也可以使用cglib。
- 偷梁換柱——用代理對象替換原始對象
完整代碼參照:understand-plugin-framework;裏面留有一個作業:我們目前僅Hook了Context
類的startActivity
方法,但是Activity
類卻使用了自己的mInstrumentation
;你可以嘗試Hook掉Activity類的startActivity
方法。
Android Hook技術