Analysis of DroidPlugin Principle
From the point of system design, the logical entity of components and windows exists in system services, such as Activity, whose logical control body is in AMS. For windows, the logical control body is in WMS.
android places the logical principal in the system service, so that the system can have a strong control over the life cycle of components and windows and display state, so that it can call back notifications in time when various state changes.
So to create any component, you need to communicate to AMS via RPC - the first hook point
Once that logical body is determined, AMS needs to create a process to run real Activity objects (also known as puppets_)
After the Android process starts, the entry to JAVA is ActivityThread.main
ActivityThread does two main things
- Create IApplicationThread native binder to communicate with AMS
- After receiving RPC events from AMS, create and save data about each component - the second hook point
Component-related data consists of two main components
- Package information of the component and corresponding loadedApk - saved in mPackages
- Save progressively logical principal object token in AMS with real component object for subsequent tracking operations - such as Activity-related saving in mActivities, service-related saving in mServices
Also, the design of ActivityThread itself seems to support loading multiple applications, and multiple applications are saved to mAllApplications
Plug-in Package Installation
DroidPlugin implements a simple IPluginManagerImpl for the installation and parsing of plug-in APK packages. Of course, this part of the code is implemented by the PMS of the reference system and has the main responsibilities:
- Install plug-in APK to local directory
- Parsing data such as components of plug-in APK
Plugin Activity Start Resolution
Let's start with the Android General Activity startup process
- Call Context.startActivity -> ActivityManagerNative -> AMS, AMS gets ActivityInfo from PMS via Intent, creates ActivityRecord and token, puts them into foreground ActivityStack, and then starts the process to which the Activity belongs on demand
- Immediately after the process starts, execute the entry ActivityThread.main and call attachApplication to feed the startup information back to AMS, which finds the corresponding ProcessRecord through pid and updates its data
- The ActivityRecord at the top of the stack is then taken from the foreground ActivityStack, and if its proecssrecord is null and the uid and processname match the newly created ProcessRecord, app.thread.scheduleLaunchActivity is formally called
- ActivityThread creates an ActivityClientRecord in the scheduleLaunchActivity that corresponds to the ActivityRecord in AMS. The two most important fields of the ActivityClientRecord are token and activityinfo, token is used to associate the ActivityRecord, and activityinfo contains the description of the activity and the package it belongs to.
- Inside the scheduleLaunchActivity, a LAUNCH_ACTIVITY message is sent to the handler mH, which receives the following code:
ActivityClientRecord r = (ActivityClientRecord)msg.obj; //Create loaedapk from the application information contained in activityinfo and save it in packageinfo r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null);
It is important to understand the first and fifth steps above, because the Activity hook of DroidPlugin is based on these two points, and the principles are summarized below:
- DroidPlugin first pre-registered a bunch of stub s with AndroidManifest for host app
activity, only a part listed here, detailed source code can be viewed.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
- Through dynamic proxy and reflection, hook Activity Manager Native's interface, this implementation principle is many on the web, and will not be repeated here
- hook startActivity, related code in 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(); }
- Get activityinfo from the package manager of DroidPlugin according to intent (if there is a matching activty in the installed plug-in package)
- Or according to intent, match the most appropriate stub activity according to the properties of the target activity, save the component information to newIntent, and save intent as an extra to newintent
- Finally, replace intent with newintent in args to achieve the effect of stealing beams and changing columns
After the above change, the system actually gets newintent and starts stubactivity; what Droid Plugin will do next is to restore stubactivity to the plug-in activity that really needs to be started, which is done in step 5 of the above start-up process
- The fifth part of the start process above shows that the two most important parameters of ActivityThread when starting an activity are the intent and activityinfo variables in the ActivityClientRecord. Actityinfo is used to create the packageinfo (loadedapk). intent is passed in after the activity is created, so DroidPlugin must be before the Acivity is created, that is, handleLaunchActivPreviously, MSG replaced these two variables with the original plug-in intent, which is what DroidPlugin Hook mH does. Here's the hook, which is part of the handleLaunchActivity code
Use intent to remove the plug-in intent you saved to extra s//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); // Add an additional judgment here, isNotShortcutProxyActivity, because ShortcutProxyActivity is so special that when you start it, // You will also be bringing an EXTRA_TARGET_INTENT with you, which will cause you to mistake it for launching plug-in activities, so make a judgment here first. // ShortcutProxyActivity used key s incorrectly before, but for compatibility's sake, let's make that judgment first. 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; } }
Next, get the corresponding activityinfo from targetIntentIntent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
Finally, write the data back to the ActivityClientRecord to complete the final replacementComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager()); ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0);
FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent); FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo);
Plug-in service startup analysis
Again, let's first look at the general startup process for a service
- Call contextimpl.startService/bindService/stopService -> AMS, which notifies ActivityThread after creating ServiceRecord and token
- When ActivityThread receives the startService, it creates the service and saves it to the mService map with the key token, then calls oncreate
- ActivityThread then receives handleServiceArgs, gets the service from token, then calls onStartCommond and passes in intent
- ActivityThread receives the bindservice, gets the service from token, then calls onbind to get the native binder, and then calls publishService to pass the native binder to AMS
ActivityManagerNative.getDefault().publishService( data.token, data.intent, binder);
There is still a big difference between Service and Activity. Service is very independent, that is, after the system creates service, in addition to calling those callbacks and passing intent as specified, the rest of the service is to play with itself and have nothing to do with the system for a penny.
Activities are different because they involve windows, so there is a lot of interaction, such as WMS, IMS, etc.
For DroidPlugin, the hook for the plug-in service is much simpler, just use a stub service as the proxy, and manage the plug-in service object according to the incoming intent within the stub service:
.stub.ServiceStub$StubP00$P00
In startservice and bindservice, you only need to cache the target sevice stub service and pass the real intent as an extra to the 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 //If there are two services in the plug-in: ServiceA and ServiceB, the onBind of ServiceA is called and its IBinder object is returned when bind ServiceA. // However, the IBinder object for ServiceA is returned when bind ServiceA is again because the plug-in system uses the same StubService for multiple Services // To proxy, but the system cached the IBinder of StubService.Setting an Action here will penetrate the cache. 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; }
The ServcesManager is then created in the stub service for plug-in service management, and all stub service callbacks are synchronized into the 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; }
See, ServcesManager manages mNameService map by itself, and service information is obtained through the real plug-in intent in extr, the onbind function is the same:
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; }
Both functions call handleCreateServiceOne when the mNameService does not contain the service instance, and create the service by reflecting the method that calls ActivityThrea, where oncreate is called
Plugin Reciver Analysis
When the plug-in apk is started, the receiver component information of the apk is analyzed and registered dynamically
Plug-in provider Analysis
First introduce the implementation principle of ContentProvider
- The essence must be binder based, so every ContentProvider implements Transport native binder
- When we call operations such as getContentResolve.insert/delete, the premise is that we need to get the Transport corresponding binder proxy for the corresponding ContentProvider binding according to the authority
- Once you get the binder proxy, the data connection is established
After the data connection is established, there is no money to follow up with the system, so in theory, the provider is the same as the service. As long as the data sender can be hook ed, the receiver can be proxied by a stubprovider.
stubprovider defined by DroidPlugin
.stub.ContentProviderStub$StubP00
The sender hook is the process of replacing the binder proxy. Look at the hook code of Droid Plugin's getContentProvider:
@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); //One of the most conflicting things here is how to handle conflicts when the plug-in's contentprovider has the same name as the host. //On an Android system, this doesn't happen because the system is handled at installation time.And we haven't done anything yet.so, in case of conflict, use host first. 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); }
Regardless of the request, authority is changed to the authority of the stub provider, and after the request is completed, the corresponding binder proxy of the authority associated contentprovider is set to DroidPlugin's own
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); }
The sending uri is then replaced in IContentProviderHook
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(); } } }
Replace uri's authority with stub provider's and save plug-in provider's authority to Env.EXTRA_TARGET_AUTHORITY, a parameter
The stubprovider implementation is simple. Create a plug-in provider based on the value of Env.EXTRA_TARGET_AUTHORITY, then proxy it. No code will be pasted here.
Here is the general initialization process for contentprovider, which you can learn about
- ContextImpl.getContentResolver.insert->ApplicationContentResolver.acquireProvider->ActivityThread.acquireProvider->ActivityManagerNative.getContentProvider->AMS.getContentProvider
- Next, ActivityThread.scheduleInstallProvider->ActivityThread.installProvider
- Next, create the ContextProvider instance and get the internal 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; }
The code shows that the native binder returned by getIContentProvider is the core of contentprovider data transfer
- Then call ActivityManagerNative.publishContentProviders to synchronize the newly created provider to AMS
It is also important that, through AMS.getContentProvider->ActivityThread.acquireProvider, since ActivityThread processing is all about sending messages to mH, it is asynchronous and AMS.getContentProvider must be empty if it returns immediately, so it must wait for subsequent ActivityManagerNative.publishContentProviders to execute before returning. See AMS.getContentProviderImpl Part Code:
//ActivityManagerService.getContentProviderImpl //.....The previous code was not pasted // 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;
summary
The design of DroidPlugin is really clever. The author has been able to conceive this kind of scheme. It must be very familiar with the initialization of components. This set of plug-in schemes has been around for many years. Recently, when I looked at it, I mainly wanted to learn the author's implementation ideas, but also to deepen my understanding of the code related to component initialization.
Component implementations can be stolen from day to day based on the design premise of Android. AMS only saves the logical object body of the component, ActivityThread only creates the local component object based on the logical body token for subsequent tracking, which makes it possible to modify the local component object.
However, this way of sneaking into the system is too big, and compatibility will be poor