1. 程式人生 > >如何攔截Activity的啟動(二)

如何攔截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的函式是介面函式,則建議用動態代理,直接攔截掉所有介面,可以在函式呼叫前改變引數,在函式呼叫後改變返回值。