Bytecode substitution

Android provides several Gradle plug-in development kits, including a Transform Api that can intervene in the build process of a project and perform some transformations to the code after the bytecode is generated and before the dex file is generated

There are two ways to plug-in:

ProxyActivity agent

The key points of the proxy approach are summarized as follows:

1,ProxyActivity needs to rewrite getResouces, getAssets, getClassLoader methods to return the corresponding object of the plug-in. Lifecycle functions and user-related functions such as onResume, onStop, onBackPressedon, KeyUponWindow, FocusChanged, and so on need to be forwarded to the plug-in. 2. All context related methods in PluginActivity, such as setContentView, getLayoutInflater and getSystemService, need to call the corresponding methods of ProxyActivity.Copy the code

This approach has several obvious drawbacks:

Activities in plug-ins must inherit pluginActivities, making development intrusive. If you want to support the Activity singleTask, singleInstance and other launchmodes, you need to manage the Activity stack, which is cumbersome to implement. Context needs to be handled carefully in plug-ins, which are error prone. It takes a lot of extra work to turn a module into a plug-in.Copy the code

Hook system startActivityThe process of

Based on the Activity startup process, using the hook thought proxy startActivity method, using the trap method, in the AndroidManifest fixed write an Activity, we start our plug-in APK when using it to check the legitimacy of the system layer. Then, when the Activity is actually created, the hook idea is used to intercept the Activity creation method, and the information is replaced back in advance to create a real plug-in APK.

Hook IActivityManager to implement the Activity plug-in

2.1 AndroidManifest. XMLRegistered pitActivity

2.2The use of pitActivitythroughAMScheck

If you think about it, our goal is simple: to replace the plug-in Activity that starts with a pit Activity before AMS executes the startActivity() method. Calling the AMS startActivity() method is done by the AMS local proxy object, so we focused on this AMS local proxy object.

As mentioned in the source code parsing of the Activity startup process, there are some differences between Android 8.0 and previous versions in how to obtain AMS proxy objects. Is the implementation of an AIDL methods used by the Android 8.0 for AMS proxy objects, and before the Android 8.0 is through ActivityManagerNative getDefault () to retrieve AMS proxy objects. However, this is not a big impact on us, do a good job of compatibility processing.

The Android 8.0 source

public static IActivityManager getService() { return IActivityManagerSingleton.get(); } private static final Singleton<IActivityManager> IActivityManagerSingleton = new Singleton<IActivityManager>() { @Override protected IActivityManager create() { final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE); final IActivityManager am = IActivityManager.Stub.asInterface(b); return am; }};Copy the code

Source code before Android 8.0

static public IActivityManager getDefault() { return gDefault.get(); } private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() { protected IActivityManager create() { IBinder b = ServiceManager.getService("activity"); if (false) { Log.v("ActivityManager", "default service binder = " + b); } IActivityManager am = asInterface(b); if (false) { Log.v("ActivityManager", "default service = " + am); } return am; }};Copy the code

As you can see from the above source code, whether Android 8.0 or before Android, the AMS local proxy object returned is of type IActivityManager. Therefore, IActivityManager is a good Hook point. We just need to intercept its startActivity() method and replace the plug-in Activity that starts with a pit Activity. For simplicity, we omit the plug-in Activity loading process and create a PluginActivity to represent the plug-in Activity that has already been loaded, without registering it in androidmanifest.xml. Also, since IActivityManager is an interface, we can use a dynamic proxy to intercept its startActivity() method, as follows:

public class IActivityManagerProxy implements InvocationHandler { private static final String TAG = "IActivityManagerProxy"; private Object mActivityManager; public IActivityManagerProxy(Object activityManager) { this.mActivityManager = activityManager; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("startActivity".equals(method.getName())) { Log.e(TAG, "invoke startActivity"); int index = 0; for (int i = 0; i < args.length; i++) { if (args[i] instanceof Intent) { index = i; break; Intent Intent = (Intent) args[index]; Intent stubIntent = new Intent(); // New Intent stubIntent = new Intent(); stubIntent.setClassName("com.lxbnjupt.pluginactivitydemo", "com.lxbnjupt.pluginactivitydemo.activity.StubActivity"); PutExtra (hookhelper.plugin_intent, pluginIntent); Args [index] = stubIntent; args[index] = stubIntent; } return method.invoke(mActivityManager, args); }}Copy the code

Create the proxy class IActivityManagerProxy and replace the IActivityManager with IActivityManagerProxy:

/** * Hook IActivityManager * * @throws Exception */ public static void hookAMS() throws Exception { Log.e(TAG, "hookAMS"); Object singleton = null; if (Build.VERSION.SDK_INT >= 26) { Class<? > activityManageClazz = Class.forName("android.app.ActivityManager"); / / get ActivityManager IActivityManagerSingleton Field in Field iActivityManagerSingletonField = ReflectUtils.getField(activityManageClazz, "IActivityManagerSingleton"); singleton = iActivityManagerSingletonField.get(activityManageClazz); } else { Class<? > activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative"); / / get ActivityManagerNative gDefault Field in Field gDefaultField = ReflectUtils. GetField (activityManagerNativeClazz, "gDefault"); singleton = gDefaultField.get(activityManagerNativeClazz); } Class<? > singletonClazz = Class.forName("android.util.Singleton"); Field mInstanceField = reflectutils. getField(singletonClazz, "mInstance"); IActivityManager = mInstanceField. Get (singleton); Class<? > iActivityManagerClazz = Class.forName("android.app.IActivityManager"); Object proxy = proxy.newProxyInstance (thread.currentThread ().getContextClassLoader(), new Class<? >[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager)); // Assign the IActivityManager proxy object to mInstance field minstanceField. set(Singleton, proxy); }Copy the code

2.3Restore the pluginActivity

Before AMS executes the startActivity() method, we pass the AMS verification by replacing the plug-in Activity with a pit Activity. However, if what we really want to start is the plug-in Activity, we will definitely have to replace it. So, if you recall the Activity launch process, our goal is to replace the trap Activity back to the plug-in Activity before the ActivityThread executes the handleLaunchActivity() method. The ActivityThread switches the logic of the code to the main thread through the Handler inner class H. The handleMessage method rewritten in H handles LAUNCH_ACTIVITY messages. We can Hook the mCallback of H.

public class HCallback implements Handler.Callback { private static final int LAUNCH_ACTIVITY = 100; Handler mHandler; public HCallback(Handler handler) { mHandler = handler; } @Override public boolean handleMessage(Message msg) { if (msg.what == LAUNCH_ACTIVITY) { Object obj = msg.obj; Intent stubIntent = (Intent) reflectutils.getField (obj.getClass(), "Intent ", obj); // Retrieve the Intent that started the PluginActivity (previously stored in the Intent that started the SubActivity) Intent pluginIntent = stubIntent.getParcelableExtra(HookHelper.PLUGIN_INTENT); / / will start SubActivity Intent to replace to start PluginActivity Intent stubIntent. SetComponent (pluginIntent. GetComponent ()); } catch (Exception e) { e.printStackTrace(); } } mHandler.handleMessage(msg); return true; }}Copy the code

HCallback implements handler. Callback and overwrites the handleMessage method. When a LAUNCH_ACTIVITY message is received, Replace the Intent that starts the spotty Activity with the Intent that starts the plug-in Activity. After that, we create an instance of HCallback and replace H’s mCallback with it:

/** * Hook ActivityThread Handler member mH ** @throws Exception */ public static void hookHandler() throws Exception { Log.e(TAG, "hookHandler"); Class<? > activityThreadClazz = Class.forName("android.app.ActivityThread"); / / get a member variable sCurrentActivityThread Field in ActivityThread Field sCurrentActivityThreadField = ReflectUtils.getField(activityThreadClazz, "sCurrentActivityThread"); / / get ActivityThread main thread Object Object currentActivityThread = sCurrentActivityThreadField. Get (activityThreadClazz); MH Field mHField = reflectutils. getField(activityThreadClazz, "mH"); Handler mH = (Handler) MHField. get(currenvityThread); // Obtain the Handler object of ActivityThread. // Assign our own HCallback object to mH's mCallback reflectutils. setField(handler. class, "mCallback", mH, new HCallback(mH)); }Copy the code

twoHook InstrumentationimplementationActivitypluggable

3.1Replace and restore plug-insActivity

Hook Instrumentation to implement the idea of Activity plug-in is also the use of pit Activity, and Hook IActivityManager is different place replacement and restore is different, and relatively will be a little more concise.

From the Activity start process, the start of an Activity will call the execStartActivity() method, in this method we can replace the plug-in Activity with pit Activity, in order to pass the AMS verification. Then, after returning to the performLaunchActivity method of the ActivityThread main thread, the newActivity method of the Instrumentation is called to create the Activity. In this method we can replace the trap Activity with a plug-in Activity.

public class InstrumentationProxy extends Instrumentation { private Instrumentation mInstrumentation; private PackageManager mPackageManager; public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) { this.mInstrumentation = instrumentation; this.mPackageManager = packageManager; } public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, List<ResolveInfo> infos = // Check whether the Activity to start is registered in androidmanifest.xml mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL); If (infos = = null | | infos. The size () = = 0) {/ / to start the Activity not registered, will launch its Intent to save in the Intent, Intent.putextra (hookhelper.plugin_intent, intent.getComponent().getClassName()); / / replace to launch the Activity for StubActivity intent. SetClassName (who, "com. Lxbnjupt. Pluginactivitydemo. Activity. StubActivity"); } try { Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class); // Call the execStartActivity method with reflection, Return (ActivityResult) Execmethod. invoke(mInstrumentation, who, contextThread, token, target, intent, requestCode, options); } catch (Exception e) { e.printStackTrace(); } return null; } public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { String intentName = intent.getStringExtra(HookHelper.PLUGIN_INTENT); if (! Return super.newActivity(cl, intentName, intent); } return super.newActivity(cl, className, intent); }}Copy the code

Then we need to create the InstrumentationProxy object and replace it with the Instrumentation object in the main thread:

/** /** * Hook Instrumentation ** @param context * @throws Exception */ public static void hookInstrumentation(Context context) throws Exception { Log.e(TAG, "hookInstrumentation"); Class<? > activityThreadClazz = Class.forName("android.app.ActivityThread"); / / get a member variable sCurrentActivityThread Field in ActivityThread Field sCurrentActivityThreadField = ReflectUtils.getField(activityThreadClazz, "sCurrentActivityThread"); // Retrieve the mInstrumentationField of ActivityThread = reflectutils. getField(activityThreadClazz, "mInstrumentation"); // Get the ActivityThread main thread Object (assigned in attach method after the application starts) Object currentActivityThread = sCurrentActivityThreadField.get(activityThreadClazz); // Get Instrumentation object Instrumentation Instrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread); PackageManager packageManager = context.getPackageManager(); InstrumentationProxy InstrumentationProxy = new InstrumentationProxy(Instrumentation, packageManager); InstrumentationProxy instrument to replace the original Instrumentation object reflectutils. setField(activityThreadClazz, "mInstrumentation", currentActivityThread, instrumentationProxy); }Copy the code