在之前的文章中,我们有讲过Android插件化加载资源。其核心思想是,通过仿照安装的流程,自行创建Resources,然后通过ResId去加载相应的资源。
同样,在启动插件Activity时,我们的思路也类似。通过仿照Activity的启动过程,我们自行创建Activity,“偷梁换柱”,交给系统去启动。
思路参考:VirtualAPK
为了成功地实施“偷梁换柱”我们首先要熟悉Activity的启动流程:
Activity.startActivity
Activity.startActivityForResult
Instrumentation.execStartActivity
ActivityManagerProxy.startActivity
---
ActivityManagerService.startActivity
ActivityStack.startActivityMayWait
ActivityStack.startActivityLocked
ActivityStack.startActivityUncheckedLocked
ActivityStack.resumeTopActivityLocked
ActivityStack.startPausingLocked
ApplicationThreadProxy.schedulePauseActivity
---
ApplicationThread.schedulePauseActivity
ActivityThread.queueOrSendMessage
H.handleMessage
ActivityThread.handlePauseActivity
ActivityManagerProxy.activityPaused
---
ActivityManagerService.activityPaused
ActivityStack.activityPaused
ActivityStack.completePauseLocked
ActivityStack.resumeTopActivityLokced
ActivityStack.startSpecificActivityLocked
ActivityStack.realStartActivityLocked
---
ApplicationThreadProxy.scheduleLaunchActivity
ApplicationThread.scheduleLaunchActivity
ActivityThread.queueOrSendMessage
H.handleMessage
ActivityThread.handleLaunchActivity
ActivityThread.performLaunchActivity
AcitiviyB.onCreate
从上表中我们可以看到,Activity在启动时,第一次进入AMS之前只有四步。前两步是我们的外部接口,最后一步是Binder方法。
首先我们要明确一定,AMS是系统的服务,我们是不能改变的。如果AMS的行为被我们改变,手机中所有App的行为都会被改变,这就是病毒了。。。
我们可以看到在Activity的启动过程中,我们真正能改变的并不多。在交给AMS前,我们只能通过Intent带上我们自己的信息。然后在ActivityThread#performLaunchActivity中去修改行为。
我们知道Activity一如其他类一样,是由ClassLoader加载类的方式启动的。
在ActivityThread#performLaunchActivity中,我们可以看到:
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
Android在Instrumentation#newActivity中,用ClassLoader创建了Activity。
如果我们要插件式启动Activity,我们首先要改变这个行为。所以,VirtualAPK首先hook了Instrumentation的newActivity方法。我们要用自己的ClassLoader,自己的newActivity方法,去启动我们自己的Activity。
我们可以看一下VirtualAPK的代码以获得思路:
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try {
cl.loadClass(className);
} catch (ClassNotFoundException e) {
// 获取已加载的插件,包含了插件的所有信息
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
// 获取要启动的Activity的类名
String targetClassName = PluginUtil.getTargetActivity(intent);
if (targetClassName != null) {
// 用插件的ClassLoader启动插件Activity
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
// 设置intent
activity.setIntent(intent);
try {
// for 4.1+
// 设置插件ContextThemeWrapper的Resources
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
} catch (Exception ignored) {
// ignored.
}
return activity;
}
}
return mBase.newActivity(cl, className, intent);
}
这是VirtualAPK hook后的newActivity的代码,我自己添加了些简单的注释。我们可以看到,VirtualAPK将插件的ClassLoader和Resources封装在了LoadedPlugin中。所以,我们还是用Instrumentation即mBase来调用newActivity方法。只是我们传入了自己的ClassLoader,类名和Intent。
这样创建的Activty因为没有安装,因此,是没有Resources的。打个不恰当的比方,它只是一个Activity的空壳。不过这个空壳是一个开始。我们接下来要做的工作,就是将它填满,让它和一个真的Activity一样。
获取插件的Resources的方法参考Android插件化——资源加载
在performLaunchActivity中,完成了newActivity后,在onCreate之前,我们会调用callActivityOnCreate
方法。它原本是这样的:
/**
* Perform calling of an activity's {@link Activity#onCreate}
* method. The default implementation simply calls through to that method.
* @param activity The activity being created.
* @param icicle The previously frozen state (or null) to pass through to
* @param persistentState The previously persisted state (or null)
*/
public void callActivityOnCreate(Activity activity, Bundle icicle,
PersistableBundle persistentState) {
prePerformCreate(activity);
activity.performCreate(icicle, persistentState);
postPerformCreate(activity);
}
这个方法,VirtualApk也进行了hook。在调用它之前,VirtualApk作了如下操作:
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) {
Context base = activity.getBaseContext();
try {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources());
ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext());
ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication());
ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext());
// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
} catch (Exception e) {
e.printStackTrace();
}
}
mBase.callActivityOnCreate(activity, icicle);
}
VirtualApk在这个方法中通过反射做了较多的工作:
- 给 Activity的Context 赋予 插件的Resources
- 给 ContextWrapper 赋予 插件的Context
- 给 Activity 赋予 插件的Application
- 给 ContextThemeWrapper 赋予 插件的Context
最后输入了activity的屏幕旋转信息。
通过上面两个方法,我们得知VirtualApk是如何加载一个包外的Activity。
看到这里也许会有所疑问,类似PluginUtil.getComponent(intent)
这样的信息又是从哪里来的呢?
其实在AMS启动前,我们还HOOK了一个方法:
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
// null component is an implicitly intent
if (intent.getComponent() != null) {
Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
intent.getComponent().getClassName()));
// resolve intent with Stub Activity if needed
this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
}
ActivityResult result = realExecStartActivity(who, contextThread, token, target,
intent, requestCode, options);
return result;
}
在这个方法中,我们在调用execStartActivity之前会对intent进行处理。
我们可以来看一看execStartActivity的细节
首先,在VirtualApk启动Activity时,方式是这样的:
Intent intent = new Intent();
intent.setClassName(this, "com.didi.virtualapk.demo.aidl.BookManagerActivity");
startActivity(intent);
我们可以看到,我们是通过ClassName隐式调用的。但其实,BookManagerActivity也根本不在我们的宿主apk中,甚至他的apk尚未安装。那么,为了和前面讲的通过loadClass的方式加载Activity顺利打通,我们是如何操作的呢?
这里就要说到execStartActivity了。
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
// null component is an implicitly intent
if (intent.getComponent() != null) {
Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
intent.getComponent().getClassName()));
// resolve intent with Stub Activity if needed
this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
}
ActivityResult result = realExecStartActivity(who, contextThread, token, target,
intent, requestCode, options);
return result;
}
在execStartActivity中首先我们调用了:transformIntentToExplicitAsNeeded根据方法名猜测,它对我们的隐式intent做了一些处理。让我们来看看它的实现:
/**
* transform intent from implicit to explicit
*/
public Intent transformIntentToExplicitAsNeeded(Intent intent) {
ComponentName component = intent.getComponent();
if (component == null
|| component.getPackageName().equals(mContext.getPackageName())) {
ResolveInfo info = mPluginManager.resolveActivity(intent);
if (info != null && info.activityInfo != null) {
component = new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
intent.setComponent(component);
}
}
return intent;
}
我们可以看到,这个方法主要用于在目标包名与宿主包名一致时调用,将隐式intent转为显式intent。
我们继续看execStartActivity:
public void markIntentIfNeeded(Intent intent) {
if (intent.getComponent() == null) {
return;
}
String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
// search map and return specific launchmode stub activity
if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
intent.putExtra(Constants.KEY_IS_PLUGIN, true);
intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
dispatchStubActivity(intent);
}
}
private void dispatchStubActivity(Intent intent) {
ComponentName component = intent.getComponent();
String targetClassName = intent.getComponent().getClassName();
LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
ActivityInfo info = loadedPlugin.getActivityInfo(component);
if (info == null) {
throw new RuntimeException("can not find " + component);
}
int launchMode = info.launchMode;
Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
themeObj.applyStyle(info.theme, true);
String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
intent.setClassName(mContext, stubActivity);
}
两个方法可以一起看一下。在这两个方法中,首先对于包名进行了判断。如果包名与宿主不相等,且与已加载的插件包名相等。即将插件的包名和类名以及ActivityInfo放入intent中。
而我们在前面讲的newActivity方法中,调用的:
String targetClassName = PluginUtil.getTargetActivity(intent);
的实现正是:
public static String getTargetActivity(Intent intent) {
return intent.getStringExtra(Constants.KEY_TARGET_ACTIVITY);
}
至此,整个Activity启动hook的流程就完全清晰了。
PS:如何绕过AndroidManifest?
我们知道,通常来说,我们新建一个Activity都需要在AndroidManifest中进行注册,否则会报错。但是,我们的插件App没有安装,我们是如何绕过检查的呢?
首先,我们要了解Android是在哪里对Activity是否有注册进行检查的。先看Instrumentation#execStartActivity的代码:
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
if (mActivityMonitors != null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i= 0 ? am.getResult() : null;
}
break;
}
}
}
}
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess();
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
}
return null;
}
最后一句,** checkStartActivityResult(result, intent);**中抛出了
have you declared this activity in your AndroidManifest.xml?
的异常。
因此,我们可以想到,就是上一句,startActivity中,做了检查。最终的异常抛出形式:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
这一流程,我们的方法execStartActivity也会走到:
ActivityResult result = realExecStartActivity(who, contextThread, token, target,
intent, requestCode, options);
private ActivityResult realExecStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
ActivityResult result = null;
try {
Class[] parameterTypes = {Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class,
int.class, Bundle.class};
result = (ActivityResult)ReflectUtil.invoke(Instrumentation.class, mBase,
"execStartActivity", parameterTypes,
who, contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
if (e.getCause() instanceof ActivityNotFoundException) {
throw (ActivityNotFoundException) e.getCause();
}
e.printStackTrace();
}
return result;
}
上面一段代码中,我们可以看到,我们还是会调用系统的execStartActivity方法。那么,被检查,在所难免。
于是VirtualApk在library中,加入了一个AndroidManifest.xml。
这就是常说的,占坑。
在上面有提到的dispatchStubActivity方法中,
mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
即是负责把目标Activity修改为坑中的名称,骗过系统。至此,Activity启动的坑就被VirtualApk一一绕过去了。