一、Hook技術概述
Hook技術的核心實際上是動態分析技術,動態分析是指在程式執行時對程式進行除錯的技術。眾所周知,Android系統的程式碼和回撥是按照一定的順序執行的,這裡舉一個簡單的例子,如圖所示。
物件A呼叫類物件B,物件B處理後將資料回撥給物件A。接下來看看採用Hook的呼叫流程,如下圖: 上圖中的Hook可以是一個方法或者一個物件,它就想一個鉤子一樣,始終連著AB,在AB之間互傳資訊的時候,hook會在中間做一些處理,比如修改方法的引數和返回值等,就這樣hook起到了欺上瞞下的作用,我們把hook的這種行為稱之為劫持。同理,大家知道,系統程式和應該程式之間是相互獨立的,應用程式要想直接去修改系統程式,這個是很難實現的,有了hook技術,就可以在程式之間進行行為更改了。如圖所示: 可見,hook將自己融入到它所劫持的物件B所在的程式中,成為系統程式的一部分,這樣我們就可以通過hook來更改物件B的行為了,物件B就稱為hook點。二、Hook Instrumentation
上面講了Hook可以劫持物件,被劫持的物件叫hook點,用代理物件來替代這個Hook點,這樣我們就可以在代理上實現自己想做的操作。這裡我們用Hook startActivity來舉例。Activity的外掛化中需要解決的一個問題就是啟動一個沒有在AndroidManifest中註冊的Activity,如果按照正常的啟動流程是會報crash的。這裡先簡要介紹一下Activity的啟動,具體的啟動方式講解還需移步專門的文獻。
2.1 Activity的Hook點
啟動Activity時應用程式會發訊息給AMS,請求AMS建立Activity,AMS在SystemServer系統程式中,其與應用程式是隔離的,AMS管理所有APP的啟動,所以我們無法在系統程式下做hook操作,應該在應用程式中。為了繞過AMS的驗證,我們需要新增一個在Manifest中註冊過的Activity,這個Activity稱為佔坑,這樣可以達到欺上瞞下的效果,當AMS驗證通過後再用外掛Activity替換佔坑去實現相應的功能。 核心功能兩點:
- 替換外掛Activity為佔坑Activity
- 繞過AMS驗證後需要還原外掛Activity
啟動Activity的時候會呼叫Activity的startActivity()如下:
@Override
public void startActivity(Intent intent) {
this.startActivity(intent, null);
}
複製程式碼
接著又呼叫了startActivity()
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
複製程式碼
檢視startActivityForResult方法
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
// If this start is requesting a result, we can avoid making
// the activity visible until the result is received. Setting
// this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
// activity hidden during this time, to avoid flickering.
// This can only be done when a result is requested because
// that guarantees we will get information back when the
// activity is finished, no matter what happens to it.
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
// TODO Consider clearing/flushing other event sources and events for child windows.
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
// Note we want to go through this method for compatibility with
// existing applications that may have overridden it.
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}
複製程式碼
上述方法中呼叫mInstrumentation的execStartActivity方法來啟動Activity,這個mInstrumentation是Activity的成員變數,我們就選擇Instrumentation為Hook點,用代理的Instrumentation去替換原始的Instrumentation來完成Hook,如下是代理類:
public class InstrumentationProxy extends Instrumentation {
private Instrumentation mInstrumentation;
private PackageManager mPackageManager;
public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
this.mInstrumentation = instrumentation;
this.mPackageManager = packageManager;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
List<ResolveInfo> resolveInfo = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
//判斷啟動的外掛Activity是否在AndroidManifest.xml中註冊過
if (null == resolveInfo || resolveInfo.size() == 0) {
//儲存目標外掛
intent.putExtra(HookHelper.REQUEST_TARGET_INTENT_NAME, intent.getComponent().getClassName());
//設定為佔坑Activity
intent.setClassName(who, "replugin.StubActivity");
}
try {
Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
return (ActivityResult) execStartActivity.invoke(mInstrumentation, who, contextThread, token, target, intent, requestCode, options);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
String intentName = intent.getStringExtra(HookHelper.REQUEST_TARGET_INTENT_NAME);
if (!TextUtils.isEmpty(intentName)) {
return super.newActivity(cl, intentName, intent);
}
return super.newActivity(cl, className, intent);
}
}
複製程式碼
InstrumentationProxy類繼承類Instrumentation,實現了類execStartActivity方法,接著通過反射去用原始Instrumentation的execStartActivity方法,這就是替換為佔坑Activity的過程。Activity的建立是在ActivityThread中,裡面有個performLaunchActivity方法;
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
}
...
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
...
}
複製程式碼
這裡的newActivity就是建立Activity的過程,我們同樣的在代理類中去實現這個方法,這就是還原外掛Activity 的過程。
接下來我們看個例子: 佔位坑Activity:
public class StubActivity extends BaseActivity {
@Override
public int bindLayout() {
return R.layout.activity_stub;
}
@Override
public void initViews() {
}
@Override
public void onClick(View v) {
}
}
複製程式碼
這個Activity一定是需要在AndroidManifest中去註冊。 再寫一個外掛Activity
public class TargetActivity extends BaseActivity {
@Override
public int bindLayout() {
return R.layout.activity_target;
}
@Override
public void initViews() {
}
@Override
public void onClick(View v) {
}
}
複製程式碼
都是很簡單的Activity,TargetActivity並沒有註冊,現在我們需要啟動這個Activity。代理類上面程式碼已經貼出來了。接下來就是替換代理類,達到Hook的目的,我們在Application中做這個事情:
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
hookActivityThreadInstrumentation();
}
private void hookActivityThreadInstrumentation() {
try {
Class<?> activityThreadClass=Class.forName("android.app.ActivityThread");
Field activityThreadField=activityThreadClass.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
//獲取ActivityThread物件sCurrentActivityThread
Object activityThread=activityThreadField.get(null);
Field instrumentationField=activityThreadClass.getDeclaredField("mInstrumentation");
instrumentationField.setAccessible(true);
//從sCurrentActivityThread中獲取成員變數mInstrumentation
Instrumentation instrumentation= (Instrumentation) instrumentationField.get(activityThread);
//建立代理物件InstrumentationProxy
InstrumentationProxy proxy=new InstrumentationProxy(instrumentation,getPackageManager());
//將sCurrentActivityThread中成員變數mInstrumentation替換成代理類InstrumentationProxy
instrumentationField.set(activityThread,proxy);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
複製程式碼
這樣就把原始的Instrumentation替換為代理的了,具體的操作我們在InstrumentationProxy中去做實現。接下來我們就是從主介面跳轉外掛Activity了:
public class PluginActivity extends BaseActivity {
@Override
public int bindLayout() {
return R.layout.activity_stub;
}
@Override
public void initViews() {
Log.d("", "initViews: ");
findViewById(R.id.btn_start_replugin).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(PluginActivity.this, TargetActivity.class
));
}
});
}
@Override
public void onClick(View v) {
}
public static void startActivity(Context context) {
Intent i = new Intent(context, PluginActivity.class);
context.startActivity(i);
}
}
複製程式碼