preface

Android Hook plug-in is not a new technology, I do not know if you have thought, alipay so many small software: tao tickets, train tickets and other software, is it written by alipay this software? It would have taken ten years, and the software would have reached tens of gigabytes, but it didn’t, and there were so many skin packs in the game that people would have to download whichever one they were using.

Can an Activity not registered in a configuration file be started?

An Activity must be registered in a configuration file. Otherwise, it will fail to start and an error will be reported. But Hook tells you that you can start an Activity without registering it in the configuration file. Isn’t that a surprise? Are you surprised?

You can learn from this article:

1. Hook the startActivity method to add logs to the startActivity method.

1.1 Hook Instrumentation

1.2 Hook AMN

2. How do I plug-in an Activity that is not registered in the configuration file

This article is based on Java reflection mechanism and App startup process analysis. It is recommended that those who are not familiar with it can move to these two articles first.

Hook the startActivity method

By looking at the startActivity source code, you can see that startActivity ends up in the startActivityFoResult method

public void startActivityForResult(Intent intent, int requestCode, Bundle options) { if(this.mParent == null) { ActivityResult ar = this.mInstrumentation.execStartActivity(this, this.mMainThread.getApplicationThread(), this.mToken, this, intent, requestCode, options); if(ar ! = null) { this.mMainThread.sendActivityResult(this.mToken, this.mEmbeddedID, requestCode, ar.getResultCode(), ar.getResultData()); } if(requestCode >= 0) { this.mStartedActivity = true; } } else if(options ! = null) { this.mParent.startActivityFromChild(this, intent, requestCode, options); } else { this.mParent.startActivityFromChild(this, intent, requestCode); }}Copy the code

Through mInstrumentation. ExecStartActivity calls (ps: detailed source code parsing has set up a file in the previous article), see mInstrumentation. ExecStartActivity method source code is as follows:

public Instrumentation.ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { IApplicationThread whoThread = (IApplicationThread)contextThread; if(this.mActivityMonitors ! = null) { Object e = this.mSync; synchronized(this.mSync) { int N = this.mActivityMonitors.size(); for(int i = 0; i < N; ++i) { Instrumentation.ActivityMonitor am = (Instrumentation.ActivityMonitor)this.mActivityMonitors.get(i); if(am.match(who, (Activity)null, intent)) { ++am.mHits; if(am.isBlocking()) { return requestCode >= 0? am.getResult():null; } break; } } } } try { intent.setAllowFds(false); intent.migrateExtraStreamToClipData(); int var16 = ActivityManagerNative.getDefault().startActivity(whoThread, intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target ! = null? target.mEmbeddedID:null, requestCode, 0, (String)null, (ParcelFileDescriptor)null, options); checkStartActivityResult(var16, intent); } catch (RemoteException var14) { ; } return null; }Copy the code

Eventually to int var16 = ActivityManagerNative. GetDefault () startActivity (whoThread, intent,… So if we want to Hook the startActivity method, there are two places to start. (Actually, there are more than two places. We only cover two places, and the reflection wrapper class used below was also presented in the previous article.)

  • 2.1 Hook the mInstrumentation

Private variables are defined in the activity. class class

private Instrumentation mInstrumentation;
Copy the code

The first thing we need to do is change the value of this private variable and print a log line before executing the method. First we get the private variable by reflection.

Instrumentation instrumentation = (Instrumentation) Reflex.getFieldObject(Activity.class,MainActivity.this,"mInstrumentation");
Copy the code

So what we’re going to do is replace this Instrumentation with our own Instrumentation, so let’s create a new MyInstrumentation inherited from Instrumentation, And the execStartActivity method of MyInstrumentation remains unchanged.

public class MyInstrumentation extends Instrumentation { private Instrumentation instrumentation; public MyInstrumentation(Instrumentation instrumentation) { this.instrumentation = instrumentation; } public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent Intent, int requestCode, Bundle options) {log. d("-----", ); Class[] classes = {Context.class,IBinder.class,IBinder.class,Activity.class,Intent.class,int.class,Bundle.class}; Object[] objects = {who,contextThread,token,target,intent,requestCode,options}; Log.d("-----"," la la la I hook in!!" ); return (ActivityResult) Reflex.invokeInstanceMethod(instrumentation,"execStartActivity",classes,objects); }Copy the code

We call this method directly through reflection with the Class[] classes = argument {the Context class, IBinder. Class, IBinder. Class, Activity, class, Intent. Class, int. J class, Bundle. The class} in agreement with the method name

(ActivityResult) Reflex.invokeInstanceMethod(instrumentation,"execStartActivity",classes,objects)
Copy the code

StartActivity is invalid if we don’t call its own execStartActivity method here.

Then we replace the custom with the original Instrumentation

Reflex.setFieldObject(Activity.class,this,"mInstrumentation",instrumentation1);
Copy the code

The complete code is

Instrumentation instrumentation = (Instrumentation) Reflex.getFieldObject(Activity.class,MainActivity.this,"mInstrumentation");
MyInstrumentation instrumentation1 = new MyInstrumentation(instrumentation);
Reflex.setFieldObject(Activity.class,this,"mInstrumentation",instrumentation1);
 
Copy the code

Run logs are as follows:

At this point we successfully Hook the startActivity method

2.2 Hook the AMN

ExecStartActivity method will eventually go ActivityManagerNative. GetDefault () startActivity method

try { intent.setAllowFds(false); intent.migrateExtraStreamToClipData(); int var16 = ActivityManagerNative.getDefault().startActivity(whoThread, intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target ! = null? target.mEmbeddedID:null, requestCode, 0, (String)null, (ParcelFileDescriptor)null, options); checkStartActivityResult(var16, intent); } catch (RemoteException var14) { ; }Copy the code

You might say the above said ah, replace ActivityManagerNative. GetDefault (), rewrite startActivity method, we see ActivityManagerNative getDefault ()

public static IActivityManager getDefault() {
    return (IActivityManager)gDefault.get();
}
Copy the code
public final T get() { synchronized(this) { if(this.mInstance == null) { this.mInstance = this.create(); } return this.mInstance; }}Copy the code

You can see that IActivityManager is an interface, and gdefault.get () returns a generic. We can’t do that, so we’ll use a dynamic proxy

We define an AmsHookHelperUtils class that handles reflection code in the AmsHookHelperUtils class

GDefault is a final statically typed field. First we get the gDefault field

Object gDefault = Reflex.getStaticFieldObject("android.app.ActivityManagerNative","gDefault");
Copy the code

GDefault is an object of type Singleton<IActivityManager>, Singleton is a Singleton pattern

public abstract class Singleton<T>{ private T mInstance; public Singleton() { } protected abstract T create(); public final T get() { synchronized(this) { if(this.mInstance == null) { this.mInstance = this.create(); } return this.mInstance; }}}Copy the code

Next we take out the mInstance field

Object mInstance = Reflex.getFieldObject("android.util.Singleton",gDefault,"mInstance");
Copy the code

Then create a proxy object

Class<? > classInterface = Class.forName("android.app.IActivityManager"); Object proxy = Proxy.newProxyInstance(classInterface.getClassLoader(), new Class<? >[]{classInterface},new AMNInvocationHanlder(mInstance));Copy the code

Our proxy object is new AMNInvocationHanlder(mInstance). (PS: The proxy mode is divided into static proxy and dynamic proxy. If you do not know the proxy mode, you can baidu wave, or pay attention to me and wait for my proxy mode related articles.)

public class AMNInvocationHanlder implements InvocationHandler { private String actionName = "startActivity"; private Object target; public AMNInvocationHanlder(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (method.getName().equals(actionName)){log.d ("-- "," I hook AMN "); return method.invoke(target,args); } return method.invoke(target,args); }}Copy the code

All proxy classes implement the InvocationHandler interface, in the invoke method. Invoke (Target,args); Represents the method that executes the proxied object. Because AMN Singleton does a lot of work, only the startActivity method is hooked here

If (method.getName().equals(actionName)){log.d ("-- "," hook AMN "); return method.invoke(target,args); }Copy the code

Then we replace the gDefault field with our proxy class

Reflex.setFieldObject("android.util.Singleton",gDefault,"mInstance",proxy);
Copy the code

The AmsHookHelperUtils method is as follows:

public class AmsHookHelperUtils { public static void hookAmn() throws ClassNotFoundException { Object gDefault = Reflex.getStaticFieldObject("android.app.ActivityManagerNative","gDefault"); Object mInstance = Reflex.getFieldObject("android.util.Singleton",gDefault,"mInstance"); Class<? > classInterface = Class.forName("android.app.IActivityManager"); Object proxy = Proxy.newProxyInstance(classInterface.getClassLoader(), new Class<? >[]{classInterface},new AMNInvocationHanlder(mInstance)); Reflex.setFieldObject("android.util.Singleton",gDefault,"mInstance",proxy); }}Copy the code

We call AmsHookHelperUtils. HookAmn (); Then start a new Activity with the following run log:

We have successfully hooked AMN’s getDefault method.

2.3 How Do I Start an Unregistered Activity

How to start an unregistered Activity? First of all, we understand the process of starting an Activity. The process of starting an App has been explained in the previous article. Suppose now MainActivity, Main2Activity, Main3Activity, where Main3Activity is not registered, we start Main3Activity in MainActivity, and when we start Main3Activity, AMS will check the configuration file to see if there is Main3Activity configuration information. If there is no configuration information, an error will be reported. If there is, Main3Activity will be started.

So what we can do is, before we send the Activity that’s going to be started to AMS, replace the Activity that’s not registered with the Activity Main2Activity, so that AMS can verify that it passes, Replace Main2Activity with the actual Activity when AMS starts the target Activity.

First, we Hook the startActivity method according to the above logic. Here, we Hook the method of AMN Hook. As with the code above, the difference is that mInstance has a different proxy class.

Create a new AMNInvocationHanlder1 object that also inherits from InvocationHandler and intercepts only the startActivity method.

if (method.getName().equals(actionName)){}
Copy the code

What we need to do here is to replace the Main3Activity we started with the Main2Activity to bypass AMS’s verification. First, we retrieve the target Activity from the target method.

Intent intent; int index = 0; for (int i = 0; i<args.length; i++){ if (args[i] instanceof Intent){ index = i; break; }}Copy the code

You might ask how do you know that args must have an Intent class parameter because the Invoke method will eventually execute it

return method.invoke(target,args);
Copy the code

The original startActivity method is executed as follows:

int var16 = ActivityManagerNative.getDefault().startActivity(whoThread, intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target ! = null? target.mEmbeddedID:null, requestCode, 0, (String)null, (ParcelFileDescriptor)null, options);Copy the code

So we say there must be an intent type parameter in args. After we get the actual target Activity, we get the package name of the target

intent = (Intent) args[index];
String packageName = intent.getComponent().getPackageName();
Copy the code

Create an Intent and set the Intent to information about the impostor Main2Activity

Intent newIntent = new Intent();
ComponentName componentName = new ComponentName(packageName,Main2Activity.class.getName());
newIntent.setComponent(componentName);
Copy the code
args[index] = newIntent;
Copy the code

The target Activity is then replaced with Main2Activity, but the impostor must carry the original target and wait until it is actually opened before replacing it back, otherwise the impostor will actually start

newIntent.putExtra(AmsHookHelperUtils.TUREINTENT,intent);
Copy the code

At this point we call this method and do nothing, and at this point we start Main3Activity

startActivity(new Intent(this,Main3Activity.class));
Copy the code

This is actually Main2Activity, as shown:

This means that our impostor has successfully replaced the real target, so we will then replace the impostor with the target again at startup, and the ActivityThread sends a message to the AMS via mH

synchronized(this) {
    Message msg = Message.obtain();
    msg.what = what;
    msg.obj = obj;
    msg.arg1 = arg1;
    msg.arg2 = arg2;
    this.mH.sendMessage(msg);
}
Copy the code

AMS receives the message and processes it

public void handleMessage(Message msg) {
    ActivityThread.ActivityClientRecord data;
    switch(msg.what) {
    case 100:
        Trace.traceBegin(64L, "activityStart");
        data = (ActivityThread.ActivityClientRecord)msg.obj;
        data.packageInfo = ActivityThread.this.getPackageInfoNoCheck(data.activityInfo.applicationInfo, data.compatInfo);
        ActivityThread.this.handleLaunchActivity(data, (Intent)null);
        Trace.traceEnd(64L);
Copy the code

MH is a message handling class of the Handler type, so sendMessage calls callback. See my previous blog for the Handler message handling mechanism

With a deeper understanding of the Android messaging mechanism, we can Hook the callback field. What if you don’t understand? Pay attention to me!!!!!!!

To create a hookActivityThread method, first we get the current ActivityThread object

Object currentActivityThread = Reflex.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");
Copy the code

Then get the mH object of the object

Handler mH = (Handler) Reflex.getFieldObject(currentActivityThread, "mH");
Copy the code

Replace mH with our own custom MyCallback.

Reflex.setFieldObject(Handler.class, mH, "mCallback", new MyCallback(mH));
Copy the code

Customizing MyCallback starts with the Handler.Callback interface, which reprocesses the handleMessage method

@Override
public boolean handleMessage(Message msg) {

    switch (msg.what) {
      
        case 100:
            handleLaunchActivity(msg);
            break;
        default:
            break;

    }

    mBase.handleMessage(msg);
    return true;
}
Copy the code

We get the target object passed in

Object obj = msg.obj;
Intent intent = (Intent) Reflex.getFieldObject(obj, "intent");
Copy the code

The actual target Activity can then be started by fetching the actual object from the target object and modifying the intent to contain information about the actual target object

Intent targetIntent = intent.getParcelableExtra(AmsHookHelperUtils.TUREINTENT);
intent.setComponent(targetIntent.getComponent());
Copy the code
MyCallbackt as followsCopy the code
** * Created by Huanglinqing on 2019/4/30. */ public class MyCallback implements Handler.Callback { Handler mBase; public MyCallback(Handler base) { mBase = base; } @Override public boolean handleMessage(Message msg) { switch (msg.what) { case 100: handleLaunchActivity(msg); break; default: break; } mBase.handleMessage(msg); return true; } private void handleLaunchActivity(Message msg) { Object obj = msg.obj; Intent intent = (Intent) Reflex.getFieldObject(obj, "intent"); Intent targetIntent = intent.getParcelableExtra(AmsHookHelperUtils.TUREINTENT); intent.setComponent(targetIntent.getComponent()); }}Copy the code

At this point, start the unregistered Main3Activity, and it will start successfully

startActivity(new Intent(this,Main3Activity.class));
Copy the code

We have successfully started the unregistered Activity