Chapter 1 of Android Plug-in Series - Interception of Activity Startup Process of Hook Technology

Keywords: Android Java Attribute

This article mainly explains how to use dynamic proxy technology Hook to remove AMS services from the system to achieve the start-up process of intercepting Activity. The amount of code is not very large. In order to understand it more easily, we need to master the reflection of JAVA, dynamic proxy technology, and the start-up process of Activity.
If you have forgotten some of the above knowledge points, it is recommended to read the following three articles on demand, otherwise skip.

1. The Principle of Finding Hook Points

Android mainly relies on the analysis of system source classes to do so, first we have to find the object that is Hook, which I call Hook point; what kind of object is better Hook? Naturally, it's easy to find objects. What kind of objects are easy to find? Static variables and instances; within a process, static variables and instance variables are relatively difficult to change, so they are very easy to locate, while ordinary objects are either unable to mark or easy to change. We find the so-called Hook point according to this principle.

2. Finding Hook Points

Usually when you click on a Button, you start the Activity jump. What happens in the process? How can we Hook to intercept the Activity start?

    public void start(View view) {
        Intent intent = new Intent(this, OtherActivity.class);
        startActivity(intent);
    }

Our goal is to intercept the startActivity method, track the source code, and find that the final startup Activity is done by the execStartActivity of the Instrumentation class. Actually, this class is equivalent to the middleman of launching Activity, which operates in the middle of launching Activity.

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        ....
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);

        //Get an object through ActivityManagerNative.getDefault() and start a new Activity
            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) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }

For Activity Manager Native, you are familiar with the start-up process of Activity/Service.

public abstract class ActivityManagerNative extends Binder implements IActivityManager

It inherits Binder and implements an IActivityManager interface, which is the Stub class for remote service communication. A complete AID L has two parts, one is a Stub communicating with the server, the other is a Proxy communicating with the client. Activity Manager Native is Stub. Read the source code and find that there is also an Activity Manager Proxy in the Activity Manager Native file, so let's not talk about it here.

static public IActivityManager getDefault() {
      return gDefault.get();
  }

Activity Manager Native. getDefault () gets an IActivityManager object, which is started by IActivityManager. The implementation class of IActivityManager is ActivityManagerService. ActivityManagerService is in another process. All activities start is a cross-process communication process, so the real initiation of activity is through the remote service ActivityMana. GerService to start.

    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;
        }

In fact, gDefalut implements the singleton pattern with Singleton. Internally, we can see that the Binder object of AMS remote service is obtained from Service Manager, and then the cost localization object is transformed by using asInterface method. Our purpose is to intercept startActivity, so changing the IActivityManager object can achieve this point. Here gDefault is static, and according to Hook principle, this point is achieved by changing the IActivityManager object. It's a better Hook point.

3. Hook drops startActivity and outputs logs

Let's first implement a small requirement to print a log when we start Activity.

public class HookUtil {

    private Class<?> proxyActivity;

    private Context context;

    public HookUtil(Class<?> proxyActivity, Context context) {
        this.proxyActivity = proxyActivity;
        this.context = context;
    }

    public void hookAms() {

        //Reflect all the way until you get the object of IActivityManager
        try {
            Class<?> ActivityManagerNativeClss = Class.forName("android.app.ActivityManagerNative");
            Field defaultFiled = ActivityManagerNativeClss.getDeclaredField("gDefault");
            defaultFiled.setAccessible(true);
            Object defaultValue = defaultFiled.get(null);
            //Reflective SingleTon
            Class<?> SingletonClass = Class.forName("android.util.Singleton");
            Field mInstance = SingletonClass.getDeclaredField("mInstance");
            mInstance.setAccessible(true);
            //You've got the ActivityManager object here.
            Object iActivityManagerObject = mInstance.get(defaultValue);


            //Start dynamic proxy, replace the real Activity Manager with proxy objects, hide the truth and cross the sea
            Class<?> IActivityManagerIntercept = Class.forName("android.app.IActivityManager");

            AmsInvocationHandler handler = new AmsInvocationHandler(iActivityManagerObject);

            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{IActivityManagerIntercept}, handler);

            //Now replace this object
            mInstance.set(defaultValue, proxy);


        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private class AmsInvocationHandler implements InvocationHandler {

        private Object iActivityManagerObject;

        private AmsInvocationHandler(Object iActivityManagerObject) {
            this.iActivityManagerObject = iActivityManagerObject;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            Log.i("HookUtil", method.getName());
            //I'm going to do something here.
            if ("startActivity".contains(method.getName())) {
                Log.e("HookUtil","Activity Start-up has begun.");
                Log.e("HookUtil","My little brother is here!!!");
            }
            return method.invoke(iActivityManagerObject, args);
        }
    }
}

Combining annotations should be easy to understand and configure in Application

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        HookUtil hookUtil=new HookUtil(SecondActivity.class, this);
        hookUtil.hookAms();
    }
}

Look at the results of the implementation:

As you can see, our successful Hook dropped startActivity and output a log. With the above foundation, now let's start with something useful. Activity can be started without registering in the manifest file. What's wrong with this?

4. Start Activity without registration

As follows, Target Activity is not registered in the manifest file. How to start Target Activity?

   public void start(View view) {
        Intent intent = new Intent(this, TargetActivity.class);
        startActivity(intent);
    }

This idea can be as follows: above has intercepted the activation activity process, in invoke we can get the activation parameter intent information, so here we can construct an intent of the false activation information ourselves, which is registered in the manifest file when the Intent starts (after the Activity Manager Service checks the manifest file). The agent's Intent is swapped over with the real Intent and then started.

First, get the intent information of the real startup parameter

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("startActivity".contains(method.getName())) {
                //Change
                Intent intent = null;
                int index = 0;
                for (int i = 0; i < args.length; i++) {
                    Object arg = args[i];
                    if (arg instanceof Intent) {
                        //The Intent parameter of startActivity has been found.
                        intent = (Intent) args[i];
                        //This intent cannot be started because Acitivity is not registered in the manifest file
                        index = i;
                    }
                }

               //Forge an agent's Intent, which starts proxy Activity
                Intent proxyIntent = new Intent();
                ComponentName componentName = new ComponentName(context, proxyActivity);
                proxyIntent.setComponent(componentName);
                proxyIntent.putExtra("oldIntent", intent);
                args[index] = proxyIntent;
            }

            return method.invoke(iActivityManagerObject, args);
        }

With the above two steps, the agent's Intent can be verified by Activity Manager Service because I registered it in the manifest file.

      <activity android:name=".ProxyActivity" />

In order not to start Proxy Activity, now we need to find a suitable time to replace the real Intent and start Activeness that we really want to start. Friends who have seen Activity's start-up process know that this process is implemented by Handler sending messages. However, according to the code of Handler processing messages, the distribution and processing of messages are orderly. The following is the code of Handler processing messages:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

When handler processes messages, it first checks whether the callback interface is implemented. If it is implemented, it will directly execute the interface method, then the handleMessage method, and finally the handleMessage method, which is rewritten most of the time. The main thread of ActivityThread uses the rewritten method. The priority of the method is the lowest. We can completely implement the interface to replace the handler process. See here for details. Android Source Parsing Handler Series Part 1 - Message Global Pool

    public void hookSystemHandler() {
        try {

            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
            currentActivityThreadMethod.setAccessible(true);
            //Get the main thread object
            Object activityThread = currentActivityThreadMethod.invoke(null);
            //Get the mH field
            Field mH = activityThreadClass.getDeclaredField("mH");
            mH.setAccessible(true);
            //Get Handler
            Handler handler = (Handler) mH.get(activityThread);
            //Get the original mCallBack field
            Field mCallBack = Handler.class.getDeclaredField("mCallback");
            mCallBack.setAccessible(true);
            //Here we set up the CallBack object that we implemented the interface ourselves.
            mCallBack.set(handler, new ActivityThreadHandlerCallback(handler)) ;

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Custom Callback Class

 private class ActivityThreadHandlerCallback implements Handler.Callback {

        private Handler handler;

        private ActivityThreadHandlerCallback(Handler handler) {
            this.handler = handler;
        }

        @Override
        public boolean handleMessage(Message msg) {
            Log.i("HookAmsUtil", "handleMessage");
            //Replace the previous Intent
            if (msg.what ==100) {
                Log.i("HookAmsUtil","lauchActivity");
                handleLauchActivity(msg);
            }

            handler.handleMessage(msg);
            return true;
        }

        private void handleLauchActivity(Message msg) {
            Object obj = msg.obj;//ActivityClientRecord
            try{
                Field intentField = obj.getClass().getDeclaredField("intent");
                intentField.setAccessible(true);
                Intent proxyInent = (Intent) intentField.get(obj);
                Intent realIntent = proxyInent.getParcelableExtra("oldIntent");
                if (realIntent != null) {
                    proxyInent.setComponent(realIntent.getComponent());
                }
            }catch (Exception e){
                Log.i("HookAmsUtil","lauchActivity falied");
            }

        }
    }

Finally, it is injected into application

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        //This ProxyActivity has been registered in the manifest file, and all Activities can use ProxyActivity without declaration to bypass monitoring.
        HookAmsUtil hookAmsUtil = new HookAmsUtil(ProxyActivity.class, this);
        hookAmsUtil.hookSystemHandler();
        hookAmsUtil.hookAms();
    }
}

To execute, click the button in MainActivity and successfully jump to Target Activity. This is the first in a series of plug-in blogs.

Please accept mybest wishes for your happiness and success !

Posted by Syranide on Mon, 25 Mar 2019 05:00:29 -0700