如何拦截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的函数是接口函数,则建议用动态代理,直接拦截掉所有接口,可以在函数调用前改变参数,在函数调用后改变返回值。
- 如何拦截Activity的启动(二)
- 如何拦截Activity的启动(一)
- Android应用开发(二):Activity生命周期剖析以及如何启动新的Activity或网页
- Android 中拦截 Activity 的启动(拦截系统的 Intent)
- Activity的启动流程(二)
- Activity的启动流程(二)
- Activity的启动模式(二)
- Activity学习(二):Activity的启动模式(转载)
- Activity开发之Activity的启动模式(二)
- 谈谈Activity如何启动的
- 使用动态代理拦截Android Activity的启动
- Activity的启动模式——LanuchMode(二)
- Android进阶(二)Activity的启动模式
- Activity的启动模式分析-之二
- Activity的生命周期和启动模式(二)
- Activity详解(二)——Activity的四种启动模式
- Android动态部署四:如何从插件apk中启动Activity(二)
- Activity组件启动过程(二)
- hdoj4907Task schedule【二分】
- Linux常用命令之三
- Smarty模版引擎
- Gallery和GridView浅析
- 深入理解堆和栈的区别
- 如何拦截Activity的启动(二)
- linux内核模块和驱动程序的编写
- hdu 1712 裸分组背包
- 并行计算之路<3>——CUDA与CPP文件联姻
- C# 提取Word文档中的图片
- MFC模态对话框和非模态对话框
- Maven学习 (二) Eclipse 上安装 Maven3插件
- OSV的初体验
- Oracle报错,ORA-28001: 口令已经失效