如何攔截Activity的啟動(二)
本文我們將以一個工程為例,驗證攔截Activity啟動的可行性,我們的目標是將普通的APK當做外掛載入起來,不做任何修改,外掛內Activity跳轉也沒有任何問題。這個APK自然是沒有安裝的,但是可以安裝後正常獨立執行。
首先新建外掛工程,和正常APP一般無二,沒有任何特別的地方。所有的Activity都是從android.app.Activity繼承,可以安裝並獨立執行。
接下來新建宿主工程,並將外掛Apk用adb push到宿主的外掛目錄下,稍後宿主會掃描並解析這個目錄下的所有外掛。先給出宿主的入口Activity,如下:
public class MainActivity extends Activity {
private File mRoot;
private Button mBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRoot = getExternalFilesDir("plugin");
if (!mRoot.exists() && !mRoot.mkdirs()) {
throw new IllegalStateException("plugin dir invalid");
}
try {
scanAllPlugins();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
mBtn = (Button) findViewById(R.id.btn);
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
launchApk("com.example.plugin");
}
});
}
private void scanAllPlugins() throws Exception {
File[] files = mRoot.listFiles();
if (files != null) {
for (File file : files) {
PluginManager.installPlugin(this, file);
}
}
}
private void launchApk(String packageName) {
ComponentName component = PluginManager.getLauncherComponent(packageName);
Intent intent = new Intent();
intent.setClassName(component.getPackageName(), component.getClassName());
startActivity(intent);
}
}
這裡Activity啟動時會掃描外掛目錄下所有外掛,並依次安裝。這裡的安裝和系統安裝Apk是兩碼事,只是解析Apk包並快取一些必要的資訊而已。當點選按鈕後會啟動包名為com.example.plugin的外掛。我們來看看PluginManager是如何安裝外掛包的:
public static void installPlugin(Context context, File apkFile) {
try {
PluginPackageParser parser = new PluginPackageParser(context, apkFile);
mParsers.put(parser.getPackageName(), parser);
File dexOutputPath = context.getDir("plugin", 0);
FileUtils.cleanDir(dexOutputPath);
DexClassLoader dexClassLoader = new DexClassLoader(
apkFile.getAbsolutePath(), dexOutputPath.getAbsolutePath(), null,
PluginManager.class.getClassLoader());
mLoaders.put(parser.getPackageName(), dexClassLoader);
Object object = ActivityThreadCompat.currentActivityThread();
Object loadedApk = MethodUtils.invokeMethod(object, "getPackageInfoNoCheck", parser.getApplicationInfo(0), CompatibilityInfoCompat.DEFAULT_COMPATIBILITY_INFO());
FieldUtils.writeDeclaredField(loadedApk, "mClassLoader", dexClassLoader);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
這裡主要做了四件事,為外掛Apk新建一個PluginPackageParser,並準備好DexClassLoader,然後反射呼叫ActivityThread的getPackageInfoNoCheck拿到外掛的LoadedApk,這個LoadedApk系統會快取起來,稍後呼叫getPackageInfo時會直接從快取中取。最後通過反射將DexClassLoader賦給這個LoadedApk的mClassLoader,這一步非常重要,因為稍後載入外掛Apk中的Activity類時就要用到這個mClassLoader。
startActivity的流程很複雜,大部分都是和AMS通訊,進行各種解析和校驗,真正載入Activity類是在ActivityThread的performLaunchActivity中,所以Hook的關鍵就在於首先要讓整個流程順利地走到這裡,然後我們在performLaunchActivity之前改變其引數。不過問題是因為外掛尚未安裝,所以整個流程會因為解析失敗而中斷。為了解決這個問題,我們需要在startActivity時改變啟動的物件,指向宿主的ProxyActivity,這樣就可以騙過系統的各種解析和校驗,從而走到最後。
總結一下,我們要做兩件事,startActivity時改變要啟動的物件,從而騙過系統,然後在performLaunchActivity之前再改回來,從而順利載入外掛的Activity並賦予上下文。
首先看如何改變啟動物件,我們知道startActivity會調到Instrumentation的execStartActivity,裡面會繼續呼叫ActivityManagerNative.getDefault().startActivity,這個getDefault返回的是IActivityManager介面,這是個單例,我們可以Hook這個介面。如下:
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
Object gDefault = FieldUtils.readStaticField(cls, "gDefault");
Object mInstance = FieldUtils.readField(gDefault, "mInstance");
List<Class<?>> interfaces = Utils.getAllInterfaces(mInstance.getClass());
final Object object = MyProxy.newProxyInstance(mInstance.getClass().getClassLoader(), interfaces, this);
FieldUtils.writeField(gDefault, "mInstance", object);
這樣就攔截掉了IActivityManager中所有的介面函式,當函式為startActivity時我們改變一下引數,將啟動物件指向宿主的ProxyActivity:
Intent intent = (Intent) args[intentOfArgIndex];
ActivityInfo activityInfo = PluginManager
.resolveActivityInfo(intent);
ComponentName component = new ComponentName(
mContext.getPackageName(),
"com.example.plugin.activity.ProxyActivity");
Intent newIntent = new Intent();
ClassLoader pluginClassLoader = PluginManager
.getLoader(component.getPackageName());
setIntentClassLoader(newIntent, pluginClassLoader);
newIntent.setComponent(component);
newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
newIntent.setFlags(intent.getFlags());
args[intentOfArgIndex] = newIntent;
args[1] = mContext.getPackageName();
這裡偽造了一個Intent,不過原始的Intent也得帶上,便於之後還原。這樣處理之後,系統就會誤認為我們要啟動的是ProxyActivity,因為這是我們自己人,所以一路會暢行無阻,直到最後執行ActivityThread的performLaunchActivity。我們要在最接近呼叫這個函式的地方把Intent還原過來。performLaunchActivity不是介面函式,所以如果要Hook的話只能採用靜態代理,將ActivityThread整個替換掉,這個就很麻煩了。我們再往前看,發現performLaunchActivity是由handleLaunchActivity呼叫的,這也不是個介面函式,或者說ActivityThread類沒有實現任何介面,那我們只能繼續往前看了,這就到了Handler的handleMessage中,這裡可是Hook的上佳之所啊,關於Handler的Hook可以參考關於Handler的Hook。
我們將ActivityThread中的Handler的callback替換成我們自己的代理callback,如下:
Object target = ActivityThreadCompat.currentActivityThread();
Class<?> ActivityThreadClass = ActivityThreadCompat.activityThreadClass();
Field mHField = FieldUtils.getField(ActivityThreadClass, "mH");
Handler handler = (Handler) FieldUtils.readField(mHField, target);
Field mCallbackField = FieldUtils.getField(Handler.class, "mCallback");
Object mCallback = FieldUtils.readField(mCallbackField, handler);
PluginCallback value = new PluginCallback(mContext, mCallback);
FieldUtils.writeField(mCallbackField, handler, value);
這樣,在Handler呼叫handlerMessage前都會被我們攔截,調到我們代理callback的handleMessage:
@Override
public boolean handleMessage(Message msg) {
// TODO Auto-generated method stub
if (msg.what == LAUNCH_ACTIVITY) {
try {
return handleLaunchActivity(msg);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (mCallback != null) {
return mCallback.handleMessage(msg);
} else {
return false;
}
}
我們判斷訊息如果為LAUNCH_ACTIVITY就開始動手腳,否則還是按系統的流程走。來看看這個手腳是怎麼動的:
private boolean handleLaunchActivity(Message msg) throws Exception {
Intent stubIntent = (Intent) FieldUtils.readField(msg.obj, "intent");
Intent targetIntent = stubIntent
.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
if (targetIntent != null) {
ComponentName targetComponentName = targetIntent
.resolveActivity(mHostContext.getPackageManager());
ActivityInfo targetActivityInfo = PluginManager.getActivityInfo(
targetComponentName, 0);
if (targetActivityInfo != null) {
ClassLoader pluginClassLoader = PluginManager
.getLoader(targetComponentName.getPackageName());
setIntentClassLoader(targetIntent, pluginClassLoader);
setIntentClassLoader(stubIntent, pluginClassLoader);
FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent);
FieldUtils.writeDeclaredField(msg.obj, "activityInfo",
targetActivityInfo);
}
}
if (mCallback != null) {
return mCallback.handleMessage(msg);
} else {
return false;
}
}
這個Message的obj裡是個ActivityClientRecord,裡面有Intent,activityInfo之類和要啟動的物件有關的資料。我們先通過反射拿到Intent,不過這個Intent是我們偽造的,我們得從裡面取出真正的Intent,然後覆蓋ActivityClientRecord中的Intent和activityInfo。這個過程都是祕密進行的,系統毫不知情。
這之後,外掛的Activity就能被順利載入了,外掛內部Activity之間跳轉也沒有任何問題。
最後總結一下Hook的要點,大概分兩點,如何選擇Hook點和如何Hook。
- Hook點選擇的原則在於穩定,通常是單例或者類的靜態成員變數
- Hook的方式通常根據要Hook的物件來決定,如果要Hook的函式是非介面函式,則只能用靜態代理,不過這樣就需要替換這個函式所在的物件為代理物件。如果這個代理物件不是單例的或者靜態成員變數那就會很麻煩。如果要Hook的函式是介面函式,則建議用動態代理,直接攔截掉所有介面,可以在函式呼叫前改變引數,在函式呼叫後改變返回值。