Hook的概念
*所謂對API的Hook, 其實(shí)就是對方法的動(dòng)態(tài)替換. *
采用代理的方式柏腻, 創(chuàng)建一個(gè)新的對象微饥, 其內(nèi)部封裝原始對象,通過這種方式咆贬,可以修改這個(gè)方法的參數(shù)以及返回值偷崩, 或是在方法中新打印一行l(wèi)og辟拷, 達(dá)到 方法增強(qiáng) 的目的.
實(shí)現(xiàn)方式
在運(yùn)行時(shí)撞羽, 采用反射的方式阐斜, 用自己新建的代理對象把原始對象給替換掉.
代理對象本質(zhì)上還是通過原始對象去干事.
對Context.startActivity的hook.
啟動(dòng)Activity是最常見的操作, Context.startActivity的真正實(shí)現(xiàn)是在ContextImpl.java中.
// ContextImpl.java
@Override
public void startActivities(Intent[] intents, Bundle options) {
warnIfCallingFromSystemProcess();
if ((intents[0].getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
throw new AndroidRuntimeException(
"Calling startActivities() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag on first Intent."
+ " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivities(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intents, options);
}
可以看到這個(gè)API真正的實(shí)現(xiàn)是在ActivityThread的成員變量
Instrumentation mInstrumentation;的 execStartActivities()方法.
所以hook的思路就是實(shí)現(xiàn)一個(gè)Instrumentation的代理類, 在代理類中提供一個(gè)新的execStartActivities()方法的實(shí)現(xiàn),
用這個(gè)代理類的對象诀紊,把ActivityThread的成員變量
Instrumentation mInstrumentation給替換掉.
@hide
public final class ActivityThread {
Instrumentation mInstrumentation;
public Instrumentation getInstrumentation() {
return mInstrumentation;
}
}
ActivityThread是一個(gè)隱藏類谒出,我們需要用反射去獲取,代碼如下:
// 先獲取到當(dāng)前的ActivityThread對象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
拿到這個(gè)currentActivityThread對象之后邻奠,我們需要修改它的mInstrumentation這個(gè)字段為我們的代理對象.
新建Instrumentation的代理類.
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執(zhí)行了startActivity, 參數(shù)如下: \n" + "who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
// 開始調(diào)用原始的方法, 調(diào)不調(diào)用隨你,但是不調(diào)用的話, 所有的startActivity都失效了.
// 由于這個(gè)方法是隱藏的,因此需要使用反射調(diào)用;首先找到這個(gè)方法
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修改了 需要手動(dòng)適配
throw new RuntimeException("do not support!!! pls adapt it");
}
}
}
完整的代碼如下:
package com.ahking.hookdemo;
import android.app.Instrumentation;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
initHook();
} catch (Exception e) {
e.printStackTrace();
}
}
public void launchSecondActivity(View view) {
Intent intent = new Intent(this, SecondActivity.class);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
this.getApplicationContext().startActivity(intent);
}
private void initHook() throws Exception{
// 先獲取到當(dāng)前的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);
// 創(chuàng)建代理對象笤喳, 構(gòu)造時(shí)把原始對象作為參數(shù)傳進(jìn)去.
Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
// 偷梁換柱——用代理對象替換原始對象
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}
}
log輸出如下:
com.ahking.hookdemo D/ahking: 執(zhí)行了startActivity, 參數(shù)如下:
who = [android.app.Application@41e6eb20],
contextThread = [android.app.ActivityThread$ApplicationThread@41e68f50],
token = [null],
target = [null],
intent = [Intent { flg=0x10000000 cmp=com.ahking.hookdemo/.SecondActivity }],
requestCode = [-1],
options = [null]
基于這樣的思路, 插件的原型就出來了.
- 在host app的AndroidManifest.xml中碌宴, 預(yù)先注冊一個(gè)Activity杀狡, 比如叫PluginActivity.
- 通過在host app中, hook startActivity(intent)方法, 當(dāng)host app要啟動(dòng)plugin app中的某個(gè)Activity時(shí)(需要明確指出要啟動(dòng)頁面的完整包名和類名)贰镣, 在hook了的startActivity中呜象, 把要啟動(dòng)的頁面修改為PluginActivity, 這樣AMS就不會(huì)報(bào)錯(cuò)了.
- AMS回調(diào)host app進(jìn)程中的ActivityThread的 handleLaunchActivity(),
這個(gè)方法負(fù)責(zé)創(chuàng)建Activity的對象, 然后依次調(diào)用它的onCreate(), onStart()和onResume().
public final class ActivityThread {
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
Activity a = performLaunchActivity(r, customIntent);
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ComponentName component = r.intent.getComponent();
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
mInstrumentation.callActivityOnCreate(activity, r.state);
}
這行代碼很關(guān)鍵碑隆, 通過Instrumentation創(chuàng)建具體Activity的對象, 這里component.getClassName()的值必然是AMS傳進(jìn)來的PluginActivity.
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
我們可以hook Instrumentation.newActivity()這個(gè)方法恭陡, 當(dāng)發(fā)現(xiàn)傳進(jìn)來的參數(shù)是PluginActivity時(shí), 并不去創(chuàng)建PluginActivity的對象, 而修改成去創(chuàng)建 plugin app中的Activity的對象, 進(jìn)而調(diào)用這個(gè)對象的onCreate(), onStart()和onResume().
如何去創(chuàng)建出 plugin app中的Activity的對象呢? 這就要通過DexClassLoader類.
Instrumentation的原始方法:
public class Instrumentation {
public Activity newActivity(Class<?> clazz, Context context,
IBinder token, Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
Object lastNonConfigurationInstance) throws InstantiationException,
IllegalAccessException {
Activity activity = (Activity)clazz.newInstance();
ActivityThread aThread = null;
activity.attach(context, aThread, this, token, 0, application, intent,
info, title, parent, id,
(Activity.NonConfigurationInstances)lastNonConfigurationInstance,
new Configuration(), null, null);
return activity;
}
}
可以看到在原始方法中上煤, 是通過ClassLoader的newInstance()方法休玩, 去創(chuàng)建Activity的對象.
用DexClassLoader類, 可以加載一個(gè)apk文件中的classes.dex.
例如這段代碼:
DexClassLoader classloader = new DexClassLoader("apkPath",
optimizedDexOutputPath.getAbsolutePath(),
null, context.getClassLoader());
Class<?> clazz = classloader.loadClass("com.plugindemo.test");
Object obj = clazz.newInstance();
Class[] param = new Class[2];
param[0] = Integer.TYPE;
param[1] = Integer.TYPE;
Method method = clazz.getMethod("add", param);
method.invoke(obj, 1, 2);
我們可以用DexClassLoader這個(gè)類把插件apk中的classes.dex加載進(jìn)來劫狠, 然后調(diào)用它的loadClass(“完整的類名”)方法把要啟動(dòng)的Activity類加載進(jìn)來拴疤, 再調(diào)用Class類的newInstance()創(chuàng)建出插件中Activity的對象, 進(jìn)而再通過調(diào)用mInstrumentation.callActivityOnCreate(activity, r.state);啟動(dòng)這個(gè)Activity.
這樣就完成了對插件中頁面的啟動(dòng)工作独泞, 在host app中要做的遥赚, 就是要明確指定好要啟動(dòng)頁面的完整包名和類名.
用hook機(jī)制解決的一個(gè)實(shí)際問題.
來launcher這邊的公司后, 同事碰到這樣一個(gè)棘手問題阐肤, 一直沒法解決.
在mediaV廣告模擬點(diǎn)擊后凫佛, 出現(xiàn)sdk中使用deeplink打開別的app頁面讲坎, 比如京東. 導(dǎo)致這個(gè)功能一直無法上線.
我使用上面的代碼, 對startActivity() API進(jìn)行hook愧薛, 把京東這樣的intent給過濾掉晨炕, 這樣就完美解決了這個(gè)棘手問題.
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
String intentInfo = intent.toString().toLowerCase();
if (sMockClick > 0 && (intentInfo.contains("akactivity") || intentInfo.contains("jdmobile"))) {
sMockClick--;
Log.i(TAG, "ignore it triggered by mediav");
return null;
}
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) {
throw new RuntimeException("3rd party rom modify this maybe, by ahking");
}
}
所以說, 有些知識(shí)平時(shí)多積累一些毫炉, 在一些關(guān)鍵時(shí)刻就能派上用場瓮栗, 像activity的啟動(dòng)流程, hook的實(shí)現(xiàn)瞄勾, 當(dāng)初學(xué)的時(shí)候看似無用费奸, 學(xué)不學(xué)看似對實(shí)際的開發(fā)并沒有任何的意思, 但如果當(dāng)初不學(xué)进陡, 今天這樣的問題愿阐, 打死也想不到可以用這樣的方式去解決.
-------DONE.-------------
refer to:
Android插件化原理解析——Hook機(jī)制之動(dòng)態(tài)代理
http://weishu.me/2016/01/28/understand-plugin-framework-proxy-hook/?nsukey=r%2BreMOlnWhDVfOrGukrJH1b%2FDJ9hDbJ0u4hfr6EQY2YIT4RCeJwqR20Lv0rQPVcPyLN4eX%2BgjW3k9fluG6CRgaUj1GyMa1GlVxN1F7%2FU%2FhiikosDgBCklABQCWbrFuXXHL0Q9QnQGDLOcL3demC82ZPcSTFjQrhrm8fEYqxTTxyn9JRzzsfCpZ3CG%2Bn6Z46s
http://zjmdp.github.io/2014/07/22/a-plugin-framework-for-android/
/home/wangxin/src/github/hookDemo (demo代碼的位置)