如何攔截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之間跳轉也沒有任何問題。

本文工程鏈接:https://github.com/dingjikerbo/Techs-Report/tree/master/files/droidplugin

最後總結一下Hook的要點,大概分兩點,如何選擇Hook點和如何Hook。

  • Hook點選擇的原則在於穩定,通常是單例或者類的靜態成員變量
  • Hook的方式通常根據要Hook的對象來決定,如果要Hook的函數是非接口函數,則只能用靜態代理,不過這樣就需要替換這個函數所在的對象爲代理對象。如果這個代理對象不是單例的或者靜態成員變量那就會很麻煩。如果要Hook的函數是接口函數,則建議用動態代理,直接攔截掉所有接口,可以在函數調用前改變參數,在函數調用後改變返回值。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章