上一篇文章裏面已經把動態代理的作用以及實現方法分析了一下,很明顯我們可以用HooK做很多事情,比如例子裏面的代理做了拿了回扣和偷換行貨這種骯髒齷齪的事情。
在真正應用的時候我們可以做更多的事情,比如用戶登錄的時候動態代理他的驗證方法,是不是就可以獲取用戶的賬號密碼呢?還有比如Activity的啓動,我們使用動態代理的手段將contextImp
對象進行動態代理,對startActivity()
函數進行修改,比如修改Intent的flag或者Intent內部數據等等,導致跳轉的頁面發生變化,或者改變傳遞的信息等等。
而實現HOOK的步驟一般分爲三步:
1. 尋找Hook點,原則是靜態變量或者單例對象,儘量Hook pulic的對象和方法,非public不保證每個版本都一樣,需要適配。
2. 選擇合適的代理方式,如果是接口可以用動態代理;如果是類可以手動寫代理也可以使用cglib。
3. 偷樑換柱——用代理對象替換原始對象
下面以改變startActivity()
邏輯爲例來展示HOOK的威力。
首先我們得找到被Hook的對象,我稱之爲Hook點;什麼樣的對象比較好Hook呢?自然是容易找到的對象。什麼樣的對象容易找到?靜態變量和單例;在一個進程之內,靜態變量和單例變量是相對不容易發生變化的,因此非常容易定位,而普通的對象則要麼無法標誌,要麼容易改變。我們根據這個原則找到所謂的Hook點。
然後我們分析一下startActivity的調用鏈,找出合適的Hook點。
我們知道對於Context.startActivity
和 Activity.startActivity
的調用鏈與之不同,由於Context的實現實際上是ContextImpl;我們看ConetxtImpl類的startActivity
方法:
@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);
}
可以看到contextImp的startActivity的邏輯是:
(1) 先判斷intent的flag是不是FLAG_ACTIVITY_NEW_TASK
類型的,如果是走步驟2;如果不是,說明startActivity
不是在activity中調用的,在activity中調用的話,走的流程如下:
Step 1. Activity.startActivity通過指定名稱“activity.subactivity”來告訴應用程序框架層,它要隱式地啓動SubActivity。所不同的是傳入的參數intent沒有Intent.FLAG_ACTIVITY_NEW_TASK標誌,表示這個SubActivity和啓動它的MainActivity運行在同一個Task中。
Step 2. Activity.startActivityForResult
Step 3. Instrumentation.execStartActivity
Step 4. ActivityManagerProxy.startActivity
詳見羅老師源碼分析:[Android應用程序內部啓動Activity過程(startActivity)的源代碼分析](http://blog.csdn.net/Luoshengyang/article/details/6703247)
(2) 調用了ActivityThread類的mInstrumentation成員的execStartActivity方法。
注意到,ActivityThread 實際上是主線程,而主線程一個進程只有一個mInstrumentation,只在應用剛剛打開第一個activity的時候創建(單例模式),之後不會發生變化,而且看到不管是Activity中startActivity還是在其他地方,調用的都是mInstrumentation.execStartActivity()
,因此mInstrumentation對象是一個良好的Hook點。
分析完我們的HOOK的點後,接下來就要替換了我們的mInstrumentation對象了,代碼如下:
第一步首先通過反射把當前進程的ActivityThread對象拿到手:
// 先獲取到當前的ActivityThread對象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
第二步,雖然動態代理可以非常方便的進行代理對象,但是我們的mInstrumentation對象不是接口,因此沒有辦法採用動態代理方式創建代理類,那就沒辦法只能通過繼承來靜態代理我們的mInstrumentation,然後覆寫我們的mInstrumentation的execStartActivity方法,代碼如下:
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");
}
}
}
上面代碼就是靜態代理的代碼,execStartActivity中先是打印一些信息,然後通過反射拿到Instrumentation的execStartActivity方法,進行調用,之所以要反射是因爲這個方法不可見,必須要反射才能調用,這是靜態代理所不能實現的功能。
創建了Instrumentation的代理對象,又找到了HOOK點,最後就只需要把需要替換的對象換掉就可以了。
第三步,使用反射進行Instrumentation對象的替換:代碼如下:
public static void attachContext() throws Exception{
// 1先獲取到當前的ActivityThread對象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 2拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
//3 創建代理對象,使用反射偷樑換柱
Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}
最後都完成了,那麼就要測試一下了,看看能不能打印出我們代理函數裏面的數據了,打印結果如下:
07-11 22:19:20 9207-9207/com.dynamic_proxy_hook.app D/EvilInstrumentation:執行了startActivity,參數如下:
who = [android.app.Application@76726c01],
contextThread = [android.app.ActivityThread$ApplicationThread@4353489dd1],
token = [null],
target = [null],
intent = [Intent { act=android.intent.action.test dat=sadjksadk flg-0x10000000}],
requestCode = [-1],
options = [null]
可以看到打印出來了,結果就是HOOK成功了。