如何拦截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的函数是接口函数,则建议用动态代理,直接拦截掉所有接口,可以在函数调用前改变参数,在函数调用后改变返回值。

版权声明:本文为dingjikerbo原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。