如何拦截Activity的启动(二)

来源:互联网 发布:淘宝网围巾专卖 编辑:程序博客网 时间:2024/04/30 22:19

本文我们将以一个工程为例,验证拦截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:

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