Android外掛化原理解析——Hook機制之動態代理
使用代理機制進行API Hook進而達到方法增強是框架的常用手段,比如J2EE框架Spring通過動態代理優雅地實現了AOP程式設計,極大地提升了Web開發效率;同樣,外掛框架也廣泛使用了代理機制來增強系統API從而達到外掛化的目的。本文將帶你瞭解基於動態代理的Hook機制。
閱讀本文之前,可以先clone一份 understand-plugin-framework,參考此專案的dynamic-proxy-hook
模組。另外,外掛框架原理解析系列文章見索引。
代理是什麼
為什麼需要代理呢?其實這個代理與日常生活中的“代理”,“中介”差不多;比如你想海淘買東西,總不可能親自飛到國外去購物吧,這時候我們使用第三方海淘服務比如惠惠購物助手等;同樣拿購物為例,有時候第三方購物會有折扣比如當初的米折網,這時候我們可以少花點錢;當然有時候這個“代理”比較坑,坑我們的錢,坑我們的貨。
從這個例子可以看出來,代理可以實現方法增強,比如常用的日誌,快取等;也可以實現方法攔截,通過代理方法修改原方法的引數和返回值,從而實現某種不可告人的目的~接下來我們用程式碼解釋一下。
靜態代理
靜態代理,是最原始的代理方式;假設我們有一個購物的介面,如下:
1 2 3 |
public interface Shopping { Object[] doShopping(long money); } |
它有一個原始的實現,我們可以理解為親自,直接去商店購物:
1 2 3 4 5 6 7 8 |
public class ShoppingImpl implements Shopping |
好了,現在我們自己沒時間但是需要買東西,於是我們就找了個代理幫我們買:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class ProxyShopping implements Shopping { Shopping base; ProxyShopping(Shopping base) { this.base = base; } @Override public Object[] doShopping(long money) { // 先黑點錢(修改輸入引數) long readCost = (long) (money * 0.5); System.out.println(String.format("花了%s塊錢", readCost)); // 幫忙買東西 Object[] things = base.doShopping(readCost); // 偷樑換柱(修改返回值) if (things != null && things.length > 1) { things[0] = "被掉包的東西!!"; } return things; } |
很不幸,我們找的這個代理有點坑,坑了我們的錢還坑了我們的貨;先忍忍。
動態代理
傳統的靜態代理模式需要為每一個需要代理的類寫一個代理類,如果需要代理的類有幾百個那不是要累死?為了更優雅地實現代理模式,JDK提供了動態代理方式,可以簡單理解為JVM可以在執行時幫我們動態生成一系列的代理類,這樣我們就不需要手寫每一個靜態的代理類了。依然以購物為例,用動態代理實現如下:
1 2 3 4 5 6 7 8 9 10 |
public static void main(String[] args) { Shopping women = new ShoppingImpl(); // 正常購物 System.out.println(Arrays.toString(women.doShopping(100))); // 招代理 women = (Shopping) Proxy.newProxyInstance(Shopping.class.getClassLoader(), women.getClass().getInterfaces(), new ShoppingHandler(women)); System.out.println(Arrays.toString(women.doShopping(100))); } |
動態代理主要處理InvocationHandler
和Proxy
類;完整程式碼可以見github
代理Hook
我們知道代理有比原始物件更強大的能力,比如飛到國外買東西,比如坑錢坑貨;那麼很自然,如果我們自己建立代理物件,然後把原始物件替換為我們的代理物件,那麼就可以在這個代理物件為所欲為了;修改引數,替換返回值,我們稱之為Hook。
下面我們Hook掉startActivity
這個方法,使得每次呼叫這個方法之前輸出一條日誌;(當然,這個輸入日誌有點點弱,只是為了展示原理;只要你想,你想可以替換引數,攔截這個startActivity
過程,使得呼叫它導致啟動某個別的Activity,指鹿為馬!)
首先我們得找到被Hook的物件,我稱之為Hook點;什麼樣的物件比較好Hook呢?自然是容易找到的物件。什麼樣的物件容易找到?靜態變數和單例;在一個程序之內,靜態變數和單例變數是相對不容易發生變化的,因此非常容易定位,而普通的物件則要麼無法標誌,要麼容易改變。我們根據這個原則找到所謂的Hook點。
然後我們分析一下startActivity
的呼叫鏈,找出合適的Hook點。我們知道對於Context.startActivity
(Activity.startActivity的呼叫鏈與之不同),由於Context
的實現實際上是ContextImpl
;我們看ConetxtImpl
類的startActivity
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Override public void startActivity(Intent intent, Bundle options) { warnIfCallingFromSystemProcess(); if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) { throw new AndroidRuntimeException( "Calling startActivity() from outside of an Activity " + " context requires the FLAG_ACTIVITY_NEW_TASK flag." + " Is this really what you want?"); } mMainThread.getInstrumentation().execStartActivity( getOuterContext(), mMainThread.getApplicationThread(), null, (Activity)null, intent, -1, options); } |
這裡,實際上使用了ActivityThread
類的mInstrumentation
成員的execStartActivity
方法;注意到,ActivityThread
實際上是主執行緒,而主執行緒一個程序只有一個,因此這裡是一個良好的Hook點。
接下來就是想要Hook掉我們的主執行緒物件,也就是把這個主執行緒物件裡面的mInstrumentation
給替換成我們修改過的代理物件;要替換主執行緒物件裡面的欄位,首先我們得拿到主執行緒物件的引用,如何獲取呢?ActivityThread
類裡面有一個靜態方法currentActivityThread
可以幫助我們拿到這個物件類;但是ActivityThread
是一個隱藏類,我們需要用反射去獲取,程式碼如下:
1 2 3 4 5 |
// 先獲取到當前的ActivityThread物件 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); currentActivityThreadMethod.setAccessible(true); Object currentActivityThread = currentActivityThreadMethod.invoke(null); |
拿到這個currentActivityThread
之後,我們需要修改它的mInstrumentation
這個欄位為我們的代理物件,我們先實現這個代理物件,由於JDK動態代理只支援介面,而這個Instrumentation
是一個類,沒辦法,我們只有手動寫靜態代理類,覆蓋掉原始的方法即可。(cglib
可以做到基於類的動態代理,這裡先不介紹)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class EvilInstrumentation extends Instrumentation { private static final String TAG = "EvilInstrumentation"; // ActivityThread中原始的物件, 儲存起來 Instrumentation mBase; public EvilInstrumentation(Instrumentation base) { mBase = base; } public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { // Hook之前, XXX到此一遊! Log.d(TAG, "\n執行了startActivity, 引數如下: \n" + "who = [" + who + "], " + "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " + "\ntarget = [" + target + "], \nintent = [" + intent + "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]"); // 開始呼叫原始的方法, 調不呼叫隨你,但是不呼叫的話, 所有的startActivity都失效了. // 由於這個方法是隱藏的,因此需要使用反射呼叫;首先找到這個方法 try { Method execStartActivity = Instrumentation.class.getDeclaredMethod( "execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class); execStartActivity.setAccessible(true); return (ActivityResult) execStartActivity.invoke(mBase, who, contextThread, token, target, intent, requestCode, options); } catch (Exception e) { // 某該死的rom修改了 需要手動適配 throw new RuntimeException("do not support!!! pls adapt it"); } } } |
Ok,有了代理物件,我們要做的就是偷樑換柱!程式碼比較簡單,採用反射直接修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public static void attachContext() throws Exception{ // 先獲取到當前的ActivityThread物件 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); currentActivityThreadMethod.setAccessible(true); Object currentActivityThread = currentActivityThreadMethod.invoke(null); // 拿到原始的 mInstrumentation欄位 Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation"); mInstrumentationField.setAccessible(true); Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread); // 建立代理物件 Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation); // 偷樑換柱 mInstrumentationField.set(currentActivityThread, evilInstrumentation); } |
好了,我們啟動一個Activity測試一下,結果如下:
可見,Hook確實成功了!這就是使用代理進行Hook的原理——偷樑換柱。整個Hook過程簡要總結如下:
- 尋找Hook點,原則是靜態變數或者單例物件,儘量Hook pulic的物件和方法,非public不保證每個版本都一樣,需要適配。
- 選擇合適的代理方式,如果是介面可以用動態代理;如果是類可以手動寫代理也可以使用cglib。
- 偷樑換柱——用代理物件替換原始物件
完整程式碼參照:understand-plugin-framework;裡面留有一個作業:我們目前僅Hook了Context
類的startActivity
方法,但是Activity
類卻使用了自己的mInstrumentation
;你可以嘗試Hook掉Activity類的startActivity
方法。