Hands-on explanation of the startup process of Android Hook-Activity

foreword

I explained the series of articles hand-in-hand, and I wrote them to you, as well as to myself.
The article may be too detailed, but this is to help as many people as possible. After all, I have worked for 5 or 6 years, and I can't suck blood all the time. It's time to give back to open source.
Articles in this series:
1. Explain the practical value of a technology in an easy-to-understand way
2. Write source code traces in detail, source source code screenshots, draw class structure diagrams, and explain the principle exploration process in detail as much as possible
3. Provide Github's runnable Demo project, but the code I provide is more to provide ideas and attract ideas, please cv as appropriate
4. Some pits in the process of exploring the principle of collection sorting, or precautions during the operation of the demo
5. Use gif image to show the demo running effect most intuitively

If you feel that the details are too detailed, just skip to the conclusion.
My ability is limited, if you find any inappropriate descriptions, please leave a message to criticize and correct.

Learning to live until old age is a long way to go. Share with you!

Introduction

previous post Hands-on introduction to Android Hook Demo , I used the simplest case to explain what a hook is. Activity startup process, people who do Android development can't get around it, but it's not easy to really know its source code logic.
first give the Code Demo , those who are interested can download it

thanks

I have read a lot of blogs about the hook Activity startup process. This big guy's article inspired me the most.
https://blog.csdn.net/gdutxiaoxu/article/details/81459910
However, maybe the blog post of the big guy is not friendly enough for some junior and intermediate Android engineers with insufficient foundation, so I will show the big guy's thoughts in a more popular and figurative way. Also, when reading the source code, there are some pitfalls, I Solutions will be given in detail.

body outline

1. Two ways to start Activity source code tracking sample code, program execution trend diagram.
2. The hook scheme of the first startup method
3. The hook scheme of the second startup method
4. Analysis of the disadvantages of the current scheme
5. Final Solution
6. Possible pits for HOOK development

text

1. Two ways to start Activity source code tracking (source code is based on SDK 28 ~ android-9.0)

Method 1: Use the startActivity that comes with the Activity

sample code

 

private void startActivityByActivity() {
        Intent i = new Intent(MainActivity.this, Main2Activity.class);
        startActivity(i);
    }

Program execution trend diagram.

Code tracking:

 

image.png

 

image.png

 

image.png

There is an if(mParent==null) judgment, first look at the true branch:

Found a pit, mInstrumentation.execStartActivity can't continue to index down here? It's strange, but it's not important, we go directly to Instrumentation.java to find this method:

 

image.png


In this execStartActivity, you can find Key code:

 

int result = ActivityManager.getService()
              .startActivity(whoThread, who.getBasePackageName(), intent,
                       intent.resolveTypeIfNeeded(who.getContentResolver()),
                      token, target != null ? target.mEmbeddedID : null,
                     requestCode, 0, null, options);
checkStartActivityResult(result, intent);

Start the Activity in this way, and the final execution authority is given to the activitymanager Getservice () (AMS) is used to start an Activity and return a result, and then checkStartActivityResult(result, intent); The jump intent intent is detected;

image.png

have you declared this activity in your AndroidManifest.xml This exception should be familiar? Error when starting an Activity that is not registered.

Look at the false branch of if(mParent==null):

 

image.png

 

image.png


Control remains in the hands of mInstrumentation.execStartActivity(), the rest of the code index is the same as above.

 

Therefore, the conclusion of the code index, according to a picture, is:

Code Index Conclusion Figure 1.png

Method 2: Use startActivity of applictonContext

 

private void startActivityByApplicationContext() {
        Intent i = new Intent(MainActivity.this, Main2Activity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        getApplicationContext().startActivity(i);
    }

In Method 1, the source code index method has been shown, so I won't repeat the texture here. The code index conclusion diagram is directly given:

Code index conclusion Figure 2.png

 

Comparing the two figures, we can easily draw a conclusion:
The final execution right to start the Activity is handed over to the Instrumentation.java class,
Mode 1: The final executor of Activity.startActivity is its mInstrumentation member, and the holder of mInstrumentation is the Activity itself.
Mode 2: The final executor of getApplicationContext().startActivity(i); is: the mInstrumentation member of ActivityThread, and the holder is the main thread of ActivityThread.
Either way, mInstrumentation can be used as a hook entry point to "steal" it from its holder.

Let's start trying:

2. The hook scheme of the first startup method

Create a HookActivityHelper.java, then three steps:

  1. Find the hook point and the holder of the hook object, as explained above: the hook point is the mInstrumentation member of the Activity, and the holder is the Activity
           Field mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation");
           mInstrumentationField.setAccessible(true);
           Instrumentation base = (Instrumentation) mInstrumentationField.get(activity);

base is the original execution logic of the system, which is stored for later use.

  1. Create an Instrumentation proxy class, inherit Instrumentation, then override the execStartActivity method, add your own logic, and then execute the system's logic.

 

private static class ProxyInstrumentation extends Instrumentation {
        public ProxyInstrumentation(Instrumentation base) {
            this.base = base;
        }

        Instrumentation base;

        public ActivityResult execStartActivity(
                Context who, IBinder contextThread, IBinder token, Activity target,
                Intent intent, int requestCode, Bundle options) {

            Log.d("ProxyInstrumentation", "our own logic");

            //The original logic of the system needs to be executed here, but suddenly I found that this execStartActivity is actually hide and can only be reflected.
            try {
                Class<?> InstrumentationClz = Class.forName("android.app.Instrumentation");
                Method execStartActivity = InstrumentationClz.getDeclaredMethod("execStartActivity",
                        Context.class, IBinder.class, IBinder.class, Activity.class,
                        Intent.class, int.class, Bundle.class);
                return (ActivityResult) execStartActivity.invoke(base, 
                            who, contextThread, token, target, intent, requestCode, options);
            } catch (Exception e) {
                e.printStackTrace();
            }

            return null;
        }

    }
  1. Replace the hook object with a proxy class object.

 

  ProxyInstrumentation proxyInstrumentation = new ProxyInstrumentation(base);
  mInstrumentationField.set(activity, proxyInstrumentation);

How to use: Add a line of ActivityHookHelper.hook(this) to onCreate of MainActivity

 

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ActivityHookHelper.hook(this);
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivityByActivity();
            }
        });

    }

    private void startActivityByActivity() {
        Intent i = new Intent(MainActivity.this, Main2Activity.class);
        startActivity(i);
    }

   
}

Effect: The jump is still normal, and the following logs can be found in logcat.

image.png

ok, insert your own logic, success

3. The hook scheme of the second startup method

Create ApplicationContextHookHelper.java, and then the same three steps:

1. Determine the object of the hook and the holder of the object
Lock the mInstrumentation member of the ActivityThread.

 

            //1. The mInstrumentation object inside the main thread ActivityThread, take it out first
            Class<?> ActivityThreadClz = Class.forName("android.app.ActivityThread");
            //Then get sCurrentActivityThread
            Field sCurrentActivityThreadField = ActivityThreadClz.getDeclaredField("sCurrentActivityThread");
            sCurrentActivityThreadField.setAccessible(true);
            Object activityThreadObj = sCurrentActivityThreadField.get(null);//The property get of a static variable does not require parameters, just pass null.
            //go get its mInstrumentation
            Field mInstrumentationField = ActivityThreadClz.getDeclaredField("mInstrumentation");
            mInstrumentationField.setAccessible(true);
            Instrumentation base = (Instrumentation) mInstrumentationField.get(activityThreadObj);// OK, get it

2. Create a proxy object is exactly the same as the above proxy class, so I won't repeat the code

 

            //2. Build your own proxy object, where Instrumentation is a class, not an interface, so it can only be done by creating an inner class
            ProxyInstrumentation proxyInstrumentation = new ProxyInstrumentation(base);

3. Replace the original object

 

            //3. Steal the beam and replace the column
            mInstrumentationField.set(activityThreadObj, proxyInstrumentation);

How to use: Add a line of ApplicationContextHookHelper.hook() in onCreate of Main4Activity;

 

public class Main4Activity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main4);

        ApplicationContextHookHelper.hook();
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivityByApplicationContext();
            }
        });
    }

    private void startActivityByApplicationContext() {
        Intent i = new Intent(Main4Activity.this, Main5Activity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        getApplicationContext().startActivity(i);
    }
}

Effect

image.png

 

OK, the second startup method, we can also add our own logic. The hook is successful!

4. Analysis of the disadvantages of the current scheme

The hook of startup mode 1: It is only for a single Activity class to perform hook, and multiple Activity needs to be written multiple times, or written in BaseActivity.
Hook of startup mode 2: you can hook global. No matter how many activities, you only need to call applicationcontexthookhelper once hook(); Function, but it can only be used for getapplicationcontext () startActivity(i); Normal Activity startActivity doesn't work.

So is there a complete solution: it can work globally and can be hook ed in both startup modes.
Looking back at the previous two code index conclusion diagrams, we will find that the two ways of starting Activity are finally executed inside AMS.
Next, try hooking AMS.

5. Final Solution

Code Index: Based on SDK 28 ~ android9.0

The part marked by the red box below is the code to obtain the AMS (ActivityManagerService instance).

image.png


If you can put the AMS instance before the system receives it Cut, can we achieve our goal?
Go in and see the code of getService:

 

image.png

The real AMS instance comes from the create() method of a Singleton singleton auxiliary class, and this Singleton singleton class provides the get method to obtain the real instance.

image.png

Then, from this singleton, we can get the current AMS instance of the system, take it out, and save it.
OK, confirm:
hook object: The singleton mInstance in the IActivityManagerSingleton member variable of ActivityManager.
Holder of hook object: IActivityManagerSingleton member variable of ActivityManager

So, hands on:

  1. Find the hook object and save it

 

            //1. Take out the hook object and save it
            //Short oil, static yeah, happy.
            Class<?> ActivityManagerClz = Class.forName("android.app.ActivityManager");
            Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getService");
            final Object IActivityManagerObj = getServiceMethod.invoke(null);//OK, the system's own AMS instance has been obtained
  1. Create your own proxy class object. IActivityManager is a dynamic interface class generated by AIDL, so at compile time, androidStudio will not find this class. Therefore, reflect first, and then use Proxy to create the proxy.

 

            //2. Now create our AMS instance
            //Since IActivityManager is an interface, we can use the Proxy class to create proxy objects
            // The result was put together, IActivityManager is still an AIDL, a dynamically generated class, the compiler does not know this class, what should I do? reflex
            Class<?> IActivityManagerClz = Class.forName("android.app.IActivityManager");
            Object proxyIActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 
                new Class[]{IActivityManagerClz}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //proxy is the created proxy class, method is the method in the interface, and args is the actual parameter when the interface is executed
                    if (method.getName().equals("startActivity")) {
                        Log.d("GlobalActivityHook", "global hook Arrived startActivity");
                    }
                    return method.invoke(IActivityManagerObj, args);
                }
            });
  1. Stealing the beam and changing the column: This time it is a bit complicated, it is no longer a simple field.set, because this time the hook object is wrapped in a Singleton.

 

           //3. Stealing beams and replacing columns, it's a bit tangled here, this instance is actually hidden in a singleton auxiliary class
            Field IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("IActivityManagerSingleton");
            IActivityManagerSingletonField.setAccessible(true);
            Object IActivityManagerSingletonObj = IActivityManagerSingletonField.get(null);
            //Reflection creates a Singleton class
            Class<?> SingletonClz = Class.forName("android.util.Singleton");
            Field mInstanceField = SingletonClz.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            mInstanceField.set(IActivityManagerSingletonObj, proxyIActivityManager);

How to use: As usual, add GlobalActivityHookHelper.hook() in your own Activity onCreate;
When running, the expected result should be: can see the log in logcat:
GlobalActivityHook - global hook to startActivity;
However, you may not see this line when you run it.

If you don't see this log, then the reason is:

The program reports an error,

 

Report an error!


There is no such method, what's going on?
debug to find the reason:

image.png


why not getService this method! ?
Checked the system version number of my current device

image.png


it is Version 23, 6.0.
Therefore, it suddenly dawned on me that the hook code we wrote was not compatible, and it would fail when encountering a lower version of the device.

 

solution:

1. Find the source code of SDK 23
(Note that there is a pit ahead, androidStudio, if you directly change the combineSDK to 23, there will be many location problems, so it is not recommended to do so. But we must look at the source code of SDK 23, what should we do?

2. Check the reason why the getService method does not exist, what is the difference between the two versions 28 and 23 in this piece of code.
3. Modify GlobalActivityHookHelper.java to determine the system version number of the current device, making it compatible with all versions.

Follow the steps above:
I found inside SDK 23:
In the execStartActivitiesAsUser(Context who, IBinder contextThread, IBinder token, Activity target, Intent[] intents, Bundle options, int userId) method of the Instrumentation class, the way to obtain an AMS instance is completely different.

 

image.png


it is using ActivityManagerNative.getDefault() to get, continue to look down to see if there is any difference.
go in ActivityManagerNative look for it:

image.png

 

image.png

 

OK, found the difference and confirmed the conclusion: the difference between SDK 28 and 23 in this code is:
The class name and method name for obtaining an AMS instance are different. In addition, after checking Du Niang, I found that this change was modified in SDK 26, so after 26 and 26, ActivityManager.getService() was used to obtain it. Before 26, ActivityManagerNative was used. getDefault() to get
Adjust the current hook method to the following:

 

public class GlobalActivityHookHelper {

    //Whether the device system version is greater than or equal to 26
    private static boolean ifSdkOverIncluding26() {
        int SDK_INT = Build.VERSION.SDK_INT;
        if (SDK_INT > 26 || SDK_INT == 26) {
            return true;
        } else {
            return false;
        }
    }

    public static void hook() {

        try {
            Class<?> ActivityManagerClz;
            final Object IActivityManagerObj;
            if (ifSdkOverIncluding26()) {
                ActivityManagerClz = Class.forName("android.app.ActivityManager");
                Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getService");
                IActivityManagerObj = getServiceMethod.invoke(null);//OK, the system's own AMS instance has been obtained
            } else {
                ActivityManagerClz = Class.forName("android.app.ActivityManagerNative");
                Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getDefault");
                IActivityManagerObj = getServiceMethod.invoke(null);//OK, the system's own AMS instance has been obtained
            }

            //2. Now create our AMS instance
            //Since IActivityManager is an interface, we can actually use the Proxy class to create proxy objects
            // The result was put together, IActivityManager is still an AIDL, a dynamically generated class, the compiler does not know this class, what should I do? reflex
            Class<?> IActivityManagerClz = Class.forName("android.app.IActivityManager");
            Object proxyIActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{IActivityManagerClz}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //proxy is the created proxy class, method is the method in the interface, and args is the actual parameter when the interface is executed
                    if (method.getName().equals("startActivity")) {
                        Log.d("GlobalActivityHook", "overall situation hook Arrived startActivity");
                    }
                    return method.invoke(IActivityManagerObj, args);
                }
            });

            //3. Stealing beams and replacing columns, it's a bit tangled here, this instance is actually hidden in a singleton auxiliary class
            Field IActivityManagerSingletonField;
            if (ifSdkOverIncluding26()) {
                IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("IActivityManagerSingleton");
            } else {
                IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("gDefault");
            }

            IActivityManagerSingletonField.setAccessible(true);
            Object IActivityManagerSingletonObj = IActivityManagerSingletonField.get(null);
            Class<?> SingletonClz = Class.forName("android.util.Singleton");//Reflection creates a Singleton class
            Field mInstanceField = SingletonClz.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            mInstanceField.set(IActivityManagerSingletonObj, proxyIActivityManager);

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

Try again:

image.png

Success, the hook of the startActivity action in the global scope is implemented.

6. Possible pits for HOOK development

1. Many classes in androidStudio cannot be indexed when reading the source code. This is because some classes belong to @hide and cannot be Ctrl-clicked.
Solution: Ctrl+shift+R enter the class name and enter the .

2.androidStudio directly reports red after reading the source code: or some interfaces are dynamically generated by AIDL, which cannot be directly viewed, compared to IActivityManager.
Solution: Don't care about this interface, if you have to use it, use the package name of this class + IActivityManager as the fully qualified name to create it by reflection.

3. hook development is to learn the source code idea and change the source code execution process. Therefore, when running on multiple versions of the device, it is easy to cause incompatibility.
Solution: Find the incompatible device version, and make corresponding compatibility changes with reference to the version transition of the source code according to the reported exception.

Epilogue

It took 3 days and I was busy, and I finally finished writing it.
If you like it, please give me a thumbs up, your encouragement is my biggest motivation, and I will update more dry goods in the future.
Finally ~ of this article Code Demo offer.



Author: turbulent
Link: https://www.jianshu.com/p/efce746836f5
Source: Jianshu
Copyright belongs to the author. For commercial reprints, please contact the author for authorization, and for non-commercial reprints, please indicate the source.

Tags: Android hook Activity

Posted by ADLE on Thu, 30 Jun 2022 21:15:32 +0930