本文我们将以一个工程为例,验证拦截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的函数是接口函数,则建议用动态代理,直接拦截掉所有接口,可以在函数调用前改变参数,在函数调用后改变返回值。