14. Source Reading (Start an Active that is not registered in Android Manifest)

Keywords: Java Android Attribute Windows

In the last blog, I've analyzed how to start an unregistered Activeness bypassing Android Manifest checks, and this time I'll do it.

Analyse the overall implementation process:

There are three hook points in the startup, and the first one is in Instrumentation.

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

ActivityManagerNative.getDefault() obtains the implementation class of the interface IActivtyMangager. Here we use dynamic proxy to intercept the method startActivity. When calling this method, we modify the Intent parameter in this method, because this Intent contains the unregistered Active information that we want to start, so it is impossible to use it directly. Start, because there is no registration, so we changed to a pre-registered puppet Activity here, just to bypass the check

@Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            LogUtils.d(TAG, "method --> " + method.getName());
            if ("startActivity".equals(method.getName())) {
                //Get the original intent (the third parameter of the startActivity method in IActivityManager is intent)
                Intent originIntent = (Intent)args[2];
                //Create a secure intent (which contains an Active registered locally as a temporary replacement). clazz is the class file of the puppet Activity
                Intent safeIntent = new Intent(context,clazz);
                //Replace this intent with the original intent
                args[2] = safeIntent;
                //Bind the original intent to this intent for launch activity
                safeIntent.putExtra(EXTRA_ORIGIN_INTENT,originIntent);
            }
            //Reflection injection
            return method.invoke(object,args);
        }

Second hook point
In ActivityThread, when launch ing the activity, sending and receiving messages through Handler is started in the handleMessage callback, and the obj passed by is final ActivityClientRecord r = (ActivityClientRecord) msg.obj; this object stores a series of relevant information to start the activity, including a member variable Intent intent; this intent is the final activity ClientRecord r = (ActivityClientRecord) msg.obj. It's the intent we replaced above, and of course it also has the original Intent inside. What we need to do is to replace the intent here with the original Intent. When handleMessage is handled, the mCallback member in Handler calls back. Also, we use dynamic proxy to intercept the handleMessage method here.

@Override
        public boolean handleMessage(Message msg) {
            //This is the way to go once without sending a message.
            //LaunchActivity Constant in the Internal Class Handler in ActivityThread = 100
            //public static final int LAUNCH_ACTIVITY = 100;
            if (msg.what == 100){
                handleLaunchActivity(msg);
            }
            return false;
        }

private void handleLaunchActivity(Message msg) {
        //Looking at the source code, you can see that this Object is the ActivityClientRecord class.
        //final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
        Object record = msg.obj;
        //Get intent from it
        try {
            Field intentField = record.getClass().getDeclaredField("intent");
            intentField.setAccessible(true);
            Intent safeIntent = (Intent) intentField.get(record);
            //Get the original intent we bind from this intent
            Intent originIntent = safeIntent.getParcelableExtra(EXTRA_ORIGIN_INTENT);
            //Replace the puppet intent with the original intent and open the unregistered activity we need
            if (originIntent != null){
                intentField.set(record,originIntent);
            }
}

At this point, if the unregistered activity we want to start is inherited from Activity, it will be no problem to start, but if it is inherited from AppCompatActivity, there will still be exceptions. The specific reason has been explained in detail in the annotation. Here is only the third hook point. In the getParentActivity Name method of NavUtils class, you can find it first. To the onCreate method in AppCompat Delegate Impl V7, intercept IPackage Manager's getActivityInfo method and replace the parameter componentName with the puppet componentName

@Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // Log.e("TAG", "methodName = " + method.getName());
            if (method.getName().startsWith("getActivityInfo")) {
                ComponentName componentName = new ComponentName(context, clazz);
                //Replace the first parameter in the getActivityInfo method, componentName, with a monitorable one, the one we used above.
                args[0] = componentName;
            }
            return method.invoke(mActivityManagerObject, args);
        }

Usage method

/**
 * Created by rzm on 2017/10/21.
 */

public class TestHookActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_hook_test);
        //TestHook Registered Activity is a puppet activity registered in Android Manifest. It is empty and has no actual function.
        //Just to avoid checking
        HookActivityUtil hookActivityUtil = new HookActivityUtil(this,TestHookRegisteredActivity.class);
        try {
            hookActivityUtil.hookStartActivity();
            hookActivityUtil.hookLaunchActivity();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void open(View view) {
        //TestHookUnRegistered Activity is Activeness that is not registered. This is Activeness that we really want to start.
        startActivity(new Intent(getApplicationContext(),TestHookUnRegisteredActivity.class));
    }
}

Tools:

package com.rzm.commonlibrary.general.hook;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import com.rzm.commonlibrary.utils.LogUtils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * Created by rzm on 2017/10/21.
 */

public class HookActivityUtil {

    public static final String EXTRA_ORIGIN_INTENT = "EXTRA_ORIGIN_INTENT";
    private static final String TAG = "HookActivityUtil";
    private final Context context;
    private final Class clazz;

    public HookActivityUtil(Context context,Class clazz) {
        this.context = context.getApplicationContext();
        this.clazz = clazz;
    }

    /**
     * This method can skip the activity initiation to check Android Manifest files, and successfully replace the checking process with a registered puppet Activity. The startActivity process succeeds.
     * But this step only guarantees that the program won't crash, but the activated activity is really the puppet, not our target activity.
     * So the next step is to restart the target Activity, which is not registered, when launching the Activity, in the hookLaunchActivity method.
     * @throws Exception
     * ActivityManagerNative
     */
    public void hookStartActivity() throws Exception {
        Class<?> aClass = Class.forName("android.app.IActivityManager");

        //Dynamic proxy import requires the proxy interface. Dynamic proxy takes the interface as the entry point. In callback, an instance of the interface implementation class is imported. These two parameters are the premise of dynamic proxy.
        //An instance of this implementation class can be found in the source code as the mInstance object in Singleton, so we need to get this instance through a series of means.

        //Get gDefault in Activity Manager Native
        Class<?> amClass = Class.forName("android.app.ActivityManagerNative");
        Field gDefault = amClass.getDeclaredField("gDefault");
        gDefault.setAccessible(true);
        Object gDefaultObj = gDefault.get(null);//Static null transmission

        //Get the mInstance attribute in gDefault
        Class<?> singleTonClass = Class.forName("android.util.Singleton");
        Field mInstance = singleTonClass.getDeclaredField("mInstance");
        mInstance.setAccessible(true);
        Object iamInstance = mInstance.get(gDefaultObj);

        Object o = Proxy.newProxyInstance(HookActivityUtil.class.getClassLoader(), new Class[]{aClass}, new StartActivityInvocationHandler(iamInstance));

        //Reflection replaces the mInstance instance with the proxy object we get
        mInstance.set(gDefaultObj, o);
    }

    /**
     * startActivity Then there is a process of checking the registration file. After checking, the activity will be started. The process of starting is executed in ActivityThread, and the message will be sent through handler.
     * A series of processes such as startup and so on. After receiving the message, the startup action will be executed in handler's callback. We can hook the previous intent here and reset it to start a new one.
     * The purpose of unregistered activity
     * @throws Exception
     */
    public void hookLaunchActivity() throws Exception {
        //Get an ActivityThread instance
        Class<?> aClass = Class.forName("android.app.ActivityThread");
        //One sCurrentActivityThread variable in the ActivityThread class is his example.
        Field aClassDeclaredField = aClass.getDeclaredField("sCurrentActivityThread");
        aClassDeclaredField.setAccessible(true);
        Object sCurrentActivityThread = aClassDeclaredField.get(null);

        //Get Handler mH in ActivityThread
        Field mhField = aClass.getDeclaredField("mH");
        mhField.setAccessible(true);
        Object mHandler = mhField.get(sCurrentActivityThread);

        //Setting callback to handler by reflection
        Class<?> handlerClass = Class.forName("android.os.Handler");

        //Handler's handleMessage method is invoked through the mCallback inside the handler, which is the callback normally passed in.
        //We intercepted the handleMessage method, where we replaced the fake intent
        Field mCallback = handlerClass.getDeclaredField("mCallback");
        mCallback.setAccessible(true);
        mCallback.set(mHandler,new HandlerCallBack());
    }

    class HandlerCallBack implements Handler.Callback {

        @Override
        public boolean handleMessage(Message msg) {
            //This is the way to go once without sending a message.
            //LaunchActivity Constant in the Internal Class Handler in ActivityThread = 100
            //public static final int LAUNCH_ACTIVITY = 100;
            if (msg.what == 100){
                handleLaunchActivity(msg);
            }
            return false;
        }
    }

    private void handleLaunchActivity(Message msg) {
        //Looking at the source code, you can see that this Object is the ActivityClientRecord class.
        //final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
        Object record = msg.obj;
        //Get intent from it
        try {
            Field intentField = record.getClass().getDeclaredField("intent");
            intentField.setAccessible(true);
            Intent safeIntent = (Intent) intentField.get(record);
            //Get the original intent we bind from this intent
            Intent originIntent = safeIntent.getParcelableExtra(EXTRA_ORIGIN_INTENT);
            //Replace the puppet intent with the original intent and open the unregistered activity we need
            if (originIntent != null){
                intentField.set(record,originIntent);
            }

            /**
             * Programs written here are still incomplete. Currently, they are only valid when the activated activity inherits from Activity, if inherited from Activity.
             * AppCompatActivity They will make mistakes.
             *
             * Caused by: java.lang.IllegalArgumentException: android.content.pm.PackageManager$NameNotFoundException: ComponentInfo{com.app.rzm/com.app.rzm.test.TestHookUnRegisteredActivity}
             * at android.support.v4.app.NavUtils.getParentActivityName(NavUtils.java:285)
             * at android.support.v7.app.AppCompatDelegateImplV9.onCreate(AppCompatDelegateImplV9.java:158)
             * at android.support.v7.app.AppCompatDelegateImplV14.onCreate(AppCompatDelegateImplV14.java:58)
             * at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:72)
             * at com.app.rzm.test.TestHookUnRegisteredActivity.onCreate(TestHookUnRegisteredActivity.java:12)
             * at android.app.Activity.performCreate(Activity.java:6366)
             * at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1126)
             * at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2661)
             * at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2779) 
             * at android.app.ActivityThread.-wrap11(ActivityThread.java) 
             * at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593) 
             * at android.os.Handler.dispatchMessage(Handler.java:111) 
             * at android.os.Looper.loop(Looper.java:207) 
             * at android.app.ActivityThread.main(ActivityThread.java:5979) 
             * at java.lang.reflect.Method.invoke(Native Method) 
             * at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:939) 
             * at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:800) 
             *
             * Find the AppCompatDelegateImplV9 onCreate method. Here's a NavUtils. getParentActivityName ((Activity) mOriginal Windows Callback)
             *
             * Finding the location of the source code in the NavUtils method
             *
             * Return the fully qualified class name of sourceActivity's parent activity as specified by
             * a {@link #PARENT_ACTIVITY} &lt;meta-data&gt; element within the activity element in
             * the application's manifest.
             *
             * @param sourceActivity Activity to fetch a parent class name for
             * @return The fully qualified class name of sourceActivity's parent activity or null if
             *         it was not specified
             *
             * @Nullable
             *   public static String getParentActivityName(Activity sourceActivity) {
             *       try {
             *           return getParentActivityName(sourceActivity, sourceActivity.getComponentName());
             *       } catch (PackageManager.NameNotFoundException e) {
             *           // Component name of supplied activity does not exist...?
             *           throw new IllegalArgumentException(e);
             *       }
             *   }
             * Return the fully qualified class name of a source activity's parent activity as specified by
             * a {@link #PARENT_ACTIVITY} &lt;meta-data&gt; element within the activity element in
             * the application's manifest. The source activity is provided by componentName.
             *
             * @param context Context for looking up the activity component for the source activity
             * @param componentName ComponentName for the source Activity
             * @return The fully qualified class name of sourceActivity's parent activity or null if
             *         it was not specified
             *
             *   @Nullable
             *  public static String getParentActivityName(Context context, ComponentName componentName)
             *   throws PackageManager.NameNotFoundException {
             *       PackageManager pm = context.getPackageManager();
             *       ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA);
             *       String parentActivity = IMPL.getParentActivityName(context, info);
             *       return parentActivity;
             *   }
             *
             *   You can see that pm.getActivityInfo executes again if it inherits from AppCompatActivity, which was originally used at startActivity.
             *   It has been invoked once to find the Activity from the collection that stores the Activity based on the full class name, if it cannot be found that the thrown Activity is not in Android Manifest
             *   The exception information registered in PackageManagerService, which is actually the same as the previous one, will call the getActivityInfo method of PackageManagerService again because
             *   Failure to register causes no activity in the collection to fail again
             *
             *   PackageManager pm = context.getPackageManager();
             *   That's where the mistake happened.
             *   ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA);
             */
            //Errors occur in the getActivityInfo method, so we can do dynamic proxy again and continue to throw puppets to him when the method is executed.
            //To get activity info, we need to proxy IPackageManger and get an instance of its implementation class, here through ActivityThread.
            //The getPackageManager method in the

            // Compatible with AppCompatActivity Error Reporting Problem
            Class<?> forName = Class.forName("android.app.ActivityThread");
            Field field = forName.getDeclaredField("sCurrentActivityThread");
            field.setAccessible(true);
            Object activityThread = field.get(null);
            // Once I execute it myself, I will create Package Manager, which is the following iPackage Manager when the system is retrieved
            Method getPackageManager = activityThread.getClass().getDeclaredMethod("getPackageManager");
            Object iPackageManager = getPackageManager.invoke(activityThread);

            PackageManagerHandler handler = new PackageManagerHandler(iPackageManager);
            Class<?> iPackageManagerIntercept = Class.forName("android.content.pm.IPackageManager");
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class<?>[]{iPackageManagerIntercept}, handler);

            // Get the sPackageManager property
            Field iPackageManagerField = activityThread.getClass().getDeclaredField("sPackageManager");
            iPackageManagerField.setAccessible(true);
            iPackageManagerField.set(activityThread, proxy);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    class PackageManagerHandler implements InvocationHandler {
        private Object mActivityManagerObject;

        public PackageManagerHandler(Object iActivityManagerObject) {
            this.mActivityManagerObject = iActivityManagerObject;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // Log.e("TAG", "methodName = " + method.getName());
            if (method.getName().startsWith("getActivityInfo")) {
                ComponentName componentName = new ComponentName(context, clazz);
                //Replace the first parameter in the getActivityInfo method, componentName, with a monitorable one, the one we used above.
                args[0] = componentName;
            }
            return method.invoke(mActivityManagerObject, args);
        }
    }

    class StartActivityInvocationHandler implements InvocationHandler {

        private final Object object;

        public StartActivityInvocationHandler(Object instance) {
            this.object = instance;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            LogUtils.d(TAG, "method --> " + method.getName());
            if ("startActivity".equals(method.getName())) {
                //Get the original intent (the third parameter of the startActivity method in IActivityManager is intent)
                Intent originIntent = (Intent)args[2];
                //Create a secure intent (this intent contains an Active registered locally as a temporary replacement)
                Intent safeIntent = new Intent(context,clazz);
                //Replace this intent with the original intent
                args[2] = safeIntent;
                //Bind the original intent to this intent for launch activity
                safeIntent.putExtra(EXTRA_ORIGIN_INTENT,originIntent);
            }
            return method.invoke(object,args);
        }
    }
}

Posted by jgetner on Sat, 19 Jan 2019 05:00:14 -0800