文章目录
DroidPlugin原理解析
从系统设计的角度,组件和窗口的逻辑实体都存在于系统服务,比如Activity创建后,其逻辑控制主体在AMS,对于窗口,其逻辑控制主体在WMS
android将逻辑主体放置于系统服务,系统就可以对组件和窗口的生命周期,显示状态进行强掌控,这样就能做到在各种状态变更时能做到及时回调通知
所以,创建任何组件,都需要通过RPC通讯到AMS创建 — 第一个hook点
那逻辑主体确定后,AMS就需要创建进程去运行真实的Activity对象(可以认为它是一个提线木偶)
Android进程启动后,JAVA的入口是ActivityThread.main
ActivityThread主要干两件事件
- 创建IApplicationThread native binder和AMS进行通讯
- 收到AMS发来的RPC事件后,创建并保存各个组件相关的数据 ---- 第二个hook点
组件相关数据主要包括两个
- 组件所属包信息和对应的loadedApk - 保存于mPackages
- 将AMS中逐渐的逻辑主体对象token和真实组件对象一同保存,便于后续跟踪操作 - 比如Activity相关的保存于mActivities,service相关保存于mServices
还有,ActivityThread的设计本身好像就支持加载多个application,多个application会被保存到mAllApplications中
插件包安装
DroidPlugin实现了一个简易的IPluginManagerImpl用于插件APK包的安装和解析,当然这部分代码是参考系统的PMS来实现的,主要职责:
- 插件APK安装到本地目录
- 对插件APK的组件等数据进行解析
插件包解析和加载
- 插件包的解析,就是对AndroidManifest的解析,主要通过反射系统的PackageParser来完成
- 在Activity启动前(hook见下面介绍),会调用
PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo);
preLoadApk内部会根据targetActivityInfo包含的包名来判断LoadedApk是
否创建,如果未创建,则会通过反射调用ActivityThread的函数来创建插件
LoadedApk并保存到ActivityThread的mPackages中,接着创建
PluginClassLoader并设置到LoadedApk对象中
- 最后通过反射调用LoadedApk的makeApplication创建插件Application对象并调用onCreate
插件Activity启动解析
我们先来看下Android常规Activity的启动流程
- 调用Context.startActivity -> ActivityManagerNative -> AMS, AMS通过Intent从PMS拿到ActivityInfo并创建ActivityRecord和token放入前台ActivityStack,接着按需启动Activity所属进程
- 进程启动后,马上执行入口ActivityThread.main并调用attachApplication将启动信息反馈到AMS,AMS通过pid找到对应的ProcessRecord并更新其数据
- 接着从前台ActivityStack中拿到栈顶的ActivityRecord,如果其proecssrecord为null,并且uid和processname跟新创建的ProcessRecord一致,则正式调用app.thread.scheduleLaunchActivity
- ActivityThread在scheduleLaunchActivity中创建ActivityClientRecord,用于跟AMS中的ActivityRecord对应,ActivityClientRecord最重要的两个字段是token和activityinfo,token用于关联ActivityRecord,activityinfo则包含activity的描述和所属包等信息
- 在scheduleLaunchActivity内部接着发送LAUNCH_ACTIVITY message到mH这个handler,mH收到LAUNCH_ACTIVITY message后的代码如下:
ActivityClientRecord r = (ActivityClientRecord)msg.obj; //通过activityinfo中包含的application信息创建loaedapk并保存于packageinfo r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null);
理解上面第1和第5步很重要,因为DroidPlugin的Activity hook就是基于这两个点来进行的,原理总结如下:
- DroidPlugin首先在host app的AndroidManifest预注册一堆stub
activity,这里只列出一部分,详细的可查看源码.stub.ActivityStub$P00$Standard00 .stub.ActivityStub$P00$SingleInstance00 .stub.ActivityStub$P00$SingleInstance01 .stub.ActivityStub$P00$SingleInstance02 .stub.ActivityStub$P00$SingleInstance03 .stub.ActivityStub$P00$SingleTask00 .stub.ActivityStub$P00$SingleTask01 .stub.ActivityStub$P00$SingleTask02 .stub.ActivityStub$P00$SingleTask03 .stub.ActivityStub$P00$SingleTop00 .stub.ActivityStub$P00$SingleTop01 .stub.ActivityStub$P00$SingleTop02 .stub.ActivityStub$P00$SingleTop03
- 通过动态代理和反射,hook ActivityManagerNative的接口,这个实现原理网上很多,这里不再赘述
- hook startActivity,相关代码在IActivityManagerHookHandle.startActivity中
ActivityInfo activityInfo = resolveActivity(intent); if (activityInfo != null && isPackagePlugin(activityInfo.packageName)) { ComponentName component = selectProxyActivity(intent); if (component != null) { Intent newIntent = new Intent(); try { ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(component.getPackageName()); setIntentClassLoader(newIntent, pluginClassLoader); } catch (Exception e) { Log.w(TAG, "Set Class Loader to new Intent fail", e); } newIntent.setComponent(component); newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent); newIntent.setFlags(intent.getFlags()); String callingPackage = (String) args[1]; if (TextUtils.equals(mHostContext.getPackageName(), callingPackage)) { newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); args[intentOfArgIndex] = newIntent; args[1] = mHostContext.getPackageName(); }
- 根据intent从DroidPlugin的packagemanager中拿到activityinfo(如果已安装插件包中有匹配的activty)
- 还是根据intent,根据目标activity的属性,去匹配一个最合适的stub activity,并将component信息保存到newIntent,同时将intent作为extra保存到newintent
- 最后将args中intent替换称newintent达到偷梁换柱的效果
经过上面的偷梁换柱后,系统实际上拿到的是newintent,进而启动stubactivity;DroidPlugin接下去要做的就是,将stubactivity还原成真正要启动的插件activity,这个是在上面启动流程第5步中完成的
- 上面启动流程第五部可以看出,ActivityThread在启动Activity的时候,最重要的两个参数就是ActivityClientRecord里的两个变量intent和activityinfo,activityinfo是用来创建packageinfo(loadedapk), intent是要在创建activity后传入的,所以DroidPlugin必须要在创建Acivity之前,也就是handleLaunchActivity(msg)之前将这两个变量替换成原始的插件intent,这就是DroidPlugin Hook mH的目的,下面是hook 也就是handleLaunchActivity的部分代码
先用intent中拿出之前保存到extra的插件intent//PluginCallback.java private boolean handleLaunchActivity(Message msg) { try { Object obj = msg.obj; Intent stubIntent = (Intent) FieldUtils.readField(obj, "intent"); //ActivityInfo activityInfo = (ActivityInfo) FieldUtils.readField(obj, "activityInfo", true); stubIntent.setExtrasClassLoader(mHostContext.getClassLoader()); Intent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT); // 这里多加一个isNotShortcutProxyActivity的判断,因为ShortcutProxyActivity的很特殊,启动它的时候, // 也会带上一个EXTRA_TARGET_INTENT的数据,就会导致这里误以为是启动插件Activity,所以这里要先做一个判断。 // 之前ShortcutProxyActivity错误复用了key,但是为了兼容,所以这里就先这么判断吧。 if (targetIntent != null && !isShortcutProxyActivity(stubIntent)) { IPackageManagerHook.fixContextPackageManager(mHostContext); ComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager()); ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0); if (targetActivityInfo != null) { if (targetComponentName != null && targetComponentName.getClassName().startsWith(".")) { targetIntent.setClassName(targetComponentName.getPackageName(), targetComponentName.getPackageName() + targetComponentName.getClassName()); } ResolveInfo resolveInfo = mHostContext.getPackageManager().resolveActivity(stubIntent, 0); ActivityInfo stubActivityInfo = resolveInfo != null ? resolveInfo.activityInfo : null; if (stubActivityInfo != null) { PluginManager.getInstance().reportMyProcessName(stubActivityInfo.processName, targetActivityInfo.processName, targetActivityInfo.packageName); } PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo); ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(targetComponentName.getPackageName()); setIntentClassLoader(targetIntent, pluginClassLoader); setIntentClassLoader(stubIntent, pluginClassLoader); boolean success = false; try { targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo); if (stubActivityInfo != null) { targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo); } success = true; } catch (Exception e) { Log.e(TAG, "putExtra 1 fail", e); } if (!success && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { try { ClassLoader oldParent = fixedClassLoader(pluginClassLoader); targetIntent.putExtras(targetIntent.getExtras()); targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo); if (stubActivityInfo != null) { targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo); } fixedClassLoader(oldParent); success = true; } catch (Exception e) { Log.e(TAG, "putExtra 2 fail", e); } } if (!success) { Intent newTargetIntent = new Intent(); newTargetIntent.setComponent(targetIntent.getComponent()); newTargetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo); if (stubActivityInfo != null) { newTargetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo); } FieldUtils.writeDeclaredField(msg.obj, "intent", newTargetIntent); } else { FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent); } FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo); Log.i(TAG, "handleLaunchActivity OK"); } else { Log.e(TAG, "handleLaunchActivity oldInfo==null"); } } else { Log.e(TAG, "handleLaunchActivity targetIntent==null"); } } catch (Exception e) { Log.e(TAG, "handleLaunchActivity FAIL", e); } if (mCallback != null) { return mCallback.handleMessage(msg); } else { return false; } }
接着根据targetIntent获取对应的activityinfoIntent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
最后将数据写回到ActivityClientRecord,完成最终的替换ComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager()); ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0);
FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent); FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo);
插件service启动分析
同样的,先来看看service的常规启动流程
- 调用contextimpl.startService/bindService/stopService -> AMS,AMS对应创建ServiceRecord和token后,通知ActivityThread
- ActivityThread收到startService后,会创建service并保存到mService map,key为token,接着调用oncreate
- ActivityThread接着收到handleServiceArgs, 根据token拿到service,接着调用onStartCommond并传入intent
- ActivityThread收到bindservice后,从根据token拿到service,接着调用onbind拿到native binder,接着调用publishService将native binder传到AMS
ActivityManagerNative.getDefault().publishService(
data.token, data.intent, binder);
Service跟Activity还是存在很大的区别的,service非常独立,也就是说,系统创建service后,除了调用规定的那些回调,传递intent外,剩下就是service自己玩自己的,跟系统一毛钱关系都没有了
Activity则不同,因为其涉及到窗口,所以会存在大量的交互,比如WMS,IMS等
对于DroidPlugin来说,插件service的hook,则会简单很多,只需要用一个stub service做为代理,在stubservice内部根据传入的intent去管理插件service对象即可:
.stub.ServiceStub$StubP00$P00
在startservice和bindservice时,只需要把目标sevice缓存stubservice,并将真实的intent作为extra传递到stub service就可以了
private static ServiceInfo replaceFirstServiceIntentOfArgs(Object[] args) throws RemoteException {
int intentOfArgIndex = findFirstIntentIndexInArgs(args);
if (args != null && args.length > 1 && intentOfArgIndex >= 0) {
Intent intent = (Intent) args[intentOfArgIndex];
ServiceInfo serviceInfo = resolveService(intent);
if (serviceInfo != null && isPackagePlugin(serviceInfo.packageName)) {
ServiceInfo proxyService = selectProxyService(intent);
if (proxyService != null) {
Intent newIntent = new Intent();
//FIXBUG:https://github.com/Qihoo360/DroidPlugin/issues/122
//如果插件中有两个Service:ServiceA和ServiceB,在bind ServiceA的时候会调用ServiceA的onBind并返回其IBinder对象,
// 但是再次bind ServiceA的时候还是会返回ServiceA的IBinder对象,这是因为插件系统对多个Service使用了同一个StubService
// 来代理,而系统对StubService的IBinder做了缓存的问题。这里设置一个Action则会穿透这种缓存。
newIntent.setAction(proxyService.name + new Random().nextInt());
newIntent.setClassName(proxyService.packageName, proxyService.name);
newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
newIntent.setFlags(intent.getFlags());
args[intentOfArgIndex] = newIntent;
return serviceInfo;
}
}
}
return null;
}
接着在stubservice会创建ServcesManager用于插件service管理,所有的stub service回调会同步到ServcesManager里:
public int onStart(Context context, Intent intent, int flags, int startId) throws Exception {
Intent targetIntent = intent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
if (targetIntent != null) {
ServiceInfo targetInfo = PluginManager.getInstance().resolveServiceInfo(targetIntent, 0);
if (targetInfo != null) {
Service service = mNameService.get(targetInfo.name);
if (service == null) {
handleCreateServiceOne(context, intent, targetInfo);
}
handleOnStartOne(targetIntent, flags, startId);
}
}
return -1;
}
看到没,ServcesManager自己管理mNameService map,service信息则是通过extr中中真实的插件intent来获得,onbind函数同样:
public IBinder onBind(Context context, Intent intent) throws Exception {
Intent targetIntent = intent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
if (targetIntent != null) {
ServiceInfo info = PluginManager.getInstance().resolveServiceInfo(targetIntent, 0);
Service service = mNameService.get(info.name);
if (service == null) {
handleCreateServiceOne(context, intent, info);
}
return handleOnBindOne(targetIntent);
}
return null;
}
这两个函数在mNameService未包含该service实例的时候,都会调用handleCreateServiceOne,通过反射调用ActivityThrea的方法创建service,从而达到调用oncreate的目地
插件receiver分析
在插件apk被启动的时候,会通过分析查看apk的receiver组件信息,然后动态注册
插件provider分析
先介绍ContentProvider的实现原理
- 本质肯定是基于binder,所以每一个ContentProvider都会实现Transport native binder
- 当我们调用getContentResolve.insert/delete等操作时,前提肯定是需要根据authority来拿到对应ContentProvider绑定的Transport对应binder proxy
- 拿到binder proxy后,数据连接建立
数据连接建立后,后续跟系统也没一毛钱关系了,那理论上provider跟service是一样的,只要能hook数据发送端,接收端用一个stubprovider做代理就可以搞定了
DroidPlugin定义的stubprovider
.stub.ContentProviderStub$StubP00
发送端hook,就是替换binder proxy的过程,看DroidPlugin的getContentProvider的hook代码:
@Override
protected boolean beforeInvoke(Object receiver, Method method, Object[] args) throws Throwable {
if (args != null) {
final int index = 1;
if (args.length > index && args[index] instanceof String) {
String name = (String) args[index];
mStubProvider = null;
mTargetProvider = null;
ProviderInfo info = mHostContext.getPackageManager().resolveContentProvider(name, 0);
mTargetProvider = PluginManager.getInstance().resolveContentProvider(name, 0);
//这里有个很坑爹的事情,就是当插件的contentprovider和host的名称一样,冲突的时候处理方式。
//在Android系统上,是不会出现这种事情的,因为系统在安装的时候做了处理。而我们目前没做处理。so,在出现冲突时候的时候优先用host的。
if (mTargetProvider != null && info != null && TextUtils.equals(mTargetProvider.packageName, info.packageName)) {
mStubProvider = PluginManager.getInstance().selectStubProviderInfo(name);
// PluginManager.getInstance().reportMyProcessName(mStubProvider.processName, mTargetProvider.processName);
// PluginProcessManager.preLoadApk(mHostContext, mTargetProvider);
if (mStubProvider != null) {
args[index] = mStubProvider.authority;
} else {
Log.w(TAG, "getContentProvider,fake fail 1");
}
} else {
mTargetProvider = null;
Log.w(TAG, "getContentProvider,fake fail 2=%s", name);
}
}
}
return super.beforeInvoke(receiver, method, args);
}
这里不管是什么请求,authority都会被改成stub provider的authority,在请求结束后,在将authority关联contentprovider对应的binder proxy设置成DroidPlugin自己的
Object provider = FieldUtils.readField(invokeResult, "provider");
if (provider != null) {
boolean localProvider = FieldUtils.readField(toObj, "provider") == null;
IContentProviderHook invocationHandler = new IContentProviderHook(mHostContext, provider, mStubProvider, mTargetProvider, localProvider);
invocationHandler.setEnable(true);
Class<?> clazz = provider.getClass();
List<Class<?>> interfaces = Utils.getAllInterfaces(clazz);
Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];
Object proxyprovider = MyProxy.newProxyInstance(clazz.getClassLoader(), ifs, invocationHandler);
FieldUtils.writeField(invokeResult, "provider", proxyprovider);
FieldUtils.writeField(toObj, "provider", proxyprovider);
}
接着在IContentProviderHook对发送uri做替换
if (!mLocalProvider && mStubProvider != null) {
final int index = indexFirstUri(args);
if (index >= 0) {
Uri uri = (Uri) args[index];
String authority = uri.getAuthority();
if (!TextUtils.equals(authority, mStubProvider.authority)) {
Uri.Builder b = new Builder();
b.scheme(uri.getScheme());
b.authority(mStubProvider.authority);
b.path(uri.getPath());
b.query(uri.getQuery());
b.appendQueryParameter(Env.EXTRA_TARGET_AUTHORITY, authority);
b.fragment(uri.getFragment());
args[index] = b.build();
}
}
}
将uri的authority替换成stub provider的,将插件provider的authority保存到Env.EXTRA_TARGET_AUTHORITY这个parameter中
stubprovider实现就很简单了,根据Env.EXTRA_TARGET_AUTHORITY的值来创建插件provider,接着做代理就好了,这里不就贴代码了
下面是contentprovider常规初始化流程,大家可以了解下
- ContextImpl.getContentResolver.insert->ApplicationContentResolver.acquireProvider->ActivityThread.acquireProvider->ActivityManagerNative.getContentProvider->AMS.getContentProvider
- 接着ActivityThread.scheduleInstallProvider->ActivityThread.installProvider
- 接着创建ContextProvider实例并获取内部native binder
try {
final java.lang.ClassLoader cl = c.getClassLoader();
localProvider = (ContentProvider)cl.
loadClass(info.name).newInstance();
provider = localProvider.getIContentProvider();
if (provider == null) {
Slog.e(TAG, "Failed to instantiate class " +
info.name + " from sourceDir " +
info.applicationInfo.sourceDir);
return null;
}
if (DEBUG_PROVIDER) Slog.v(
TAG, "Instantiating local provider " + info.name);
// XXX Need to create the correct context for this provider.
localProvider.attachInfo(c, info);
} catch (java.lang.Exception e) {
if (!mInstrumentation.onException(null, e)) {
throw new RuntimeException(
"Unable to get provider " + info.name
+ ": " + e.toString(), e);
}
return null;
}
从代码里可以看出getIContentProvider返回的native binder才是contentprovider数据传输的核心
- 接着调用ActivityManagerNative.publishContentProviders将新创建的provider同步到AMS
还有一点很重要,通过AMS.getContentProvider->ActivityThread.acquireProvider,由于ActivityThread处理都是发送消息到mH,所以它是异步的,AMS.getContentProvider如果立即返回,肯定是空的,所以它必须要等待后续ActivityManagerNative.publishContentProviders执行完成后才返回,看AMS.getContentProviderImpl部分代码:
//ActivityManagerService.getContentProviderImpl
//.....前面代码没贴
// Wait for the provider to be published...
synchronized (cpr) {
while (cpr.provider == null) {
if (cpr.launchingApp == null) {
return null;
}
try {
if (conn != null) {
conn.waiting = true;
}
cpr.wait();
} catch (InterruptedException ex) {
} finally {
if (conn != null) {
conn.waiting = false;
}
}
}
}
return cpr != null ? cpr.newHolder(conn) : null;
插件加载独立性
如果插件都在主进程启动运行,可能有人会有疑问,LoadedApk会不会乱掉?答案肯定是不会的,因为这个是DroidPlugin这个实现方案的前提,咱们看LoadedApk的生成代码
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode) {
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
packageInfo =
new LoadedApk(this, aInfo, compatInfo, this, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0);
if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}
ActivityThread会保存LoadeApk的map,key就是package name,所以各个插件的LoadedApk可以独立的存在ActivityThread中
插件resource获取
Android资源获取依赖
- resource id,即开发中用到R..
- 还有就是context.getResource()
由于四大组件和Application这五个入口类的创建使用的是插件的class loader,那他们使用过程中用到的R.java肯定是对应插件的,这个不会有任何问题
不过context本质是ContextImpl对象实例,这个对象不是基于插件的class loader创建的,这个要注意,但是它对插件resource独立获取没任何影响,因为
- context实例跟组件和Application都是一对一创建的,这就导致它不可能跟其他插件混淆
- context.getresource本质还是使用插件package res info创建AssertManager,它跟插件也是一对一绑定的
所以,只要完成了插件LoadedApk的创建,组件运行过程中的resource就可以正常获取
总结
DroidPlugin的设计真的很巧妙,作者能构思出这种方案,对组件的初始化肯定是非常熟悉的,这套插件化方案出来也很多年了,最近看一遍,主要还是想学习作者的实现思路,同时也加深自己对组件初始化相关代码的理解
组件实现能被偷天换日是基于Android这么一个设计前提,AMS只是保存组件的逻辑对象主体,ActivityThread只是基于逻辑主体token来创建本地组件对象并做后续跟踪,这就为修改本地组件对象提供了可能
不过这种方式对系统潜入太大了,兼容性会比较差