Android JAVA Hook机制实战

2,491 阅读14分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

什么是 Hook

Hook 又叫“钩子” ,它可以在事件传送的过程中截获并监控事件的传输,将自身的代码与系统方法进行融入。

这样当这些方法被调用时,也就可以执行我们自己的代码,这也是面向切面编程的思想(AOP)。

根据Android开发模式,Native模式(C/C++)和Java模式(Java)区分,在Android平台上

  • Java层级的Hook;
  • Native层级的Hook;

本章说说Java层级的Hook

学会 Hook 首先必须掌握的知识

反射 如果你对反射还不是很熟悉的话,建议你先复习一下 java 反射的相关知识。有兴趣的,可以看一下我的这一篇博客 Java 反射机制详解

Java 的动态代理 动态代理是指在运行时动态生成代理类,不需要我们像静态代理那个去手动写一个个的代理类。在 java 中,我们可以使用 InvocationHandler 实现动态代理,有兴趣的,可以查看我原来写的博客

Hook 选择的关键点

Hook 的选择点:尽量静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位

Hook 过程: 寻找 Hook 点,原则是尽量静态变量或者单例对象,尽量 Hook public 的对象和方法。 选择合适的代理方式,如果是接口可以用动态代理。 偷梁换柱——用代理对象替换原始对象。 Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。

Hook 使用实例

案例一: 使用 Hook 修改 View.OnClickListener 事件

首先,我们先分析 View.setOnClickListener 源码,找出合适的 Hook 点。可以看到 OnClickListener 对象被保存在了一个叫做 ListenerInfo 的内部类里,其中 mListenerInfo 是 View 的成员变量。ListeneInfo 里面保存了 View 的各种监听事件。因此,我们可以想办法 hook ListenerInfo 的 mOnClickListener 。

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

static class ListenerInfo {

     ---

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
    
    ---
}

接下来,让我们一起来看一下怎样 Hook View.OnClickListener 事件?

大概分为三步:

第一步:获取 ListenerInfo 对象 从 View 的源代码,我们可以知道我们可以通过 getListenerInfo 方法获取,于是,我们利用反射得到 ListenerInfo 对象

第二步:获取原始的 OnClickListener事件方法 从上面的分析,我们知道 OnClickListener 事件被保存在 ListenerInfo 里面,同理我们利用反射获取

第三步:偷梁换柱,用 Hook代理类 替换原始的 OnClickListener

public static void hookOnClickListener(View view) throws Exception {
    // 第一步:反射得到 ListenerInfo 对象
    Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
    getListenerInfo.setAccessible(true);
    Object listenerInfo = getListenerInfo.invoke(view);
    // 第二步:得到原始的 OnClickListener事件方法
    Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
    Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
    mOnClickListener.setAccessible(true);
    View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
    // 第三步:用 Hook代理类 替换原始的 OnClickListener
    View.OnClickListener hookedOnClickListener = new HookedClickListenerProxy(originOnClickListener);
    mOnClickListener.set(listenerInfo, hookedOnClickListener);
}
public class HookedClickListenerProxy implements View.OnClickListener {

    private View.OnClickListener origin;

    public HookedClickListenerProxy(View.OnClickListener origin) {
        this.origin = origin;
    }

    @Override
    public void onClick(View v) {
        Toast.makeText(v.getContext(), "Hook Click Listener", Toast.LENGTH_SHORT).show();
        if (origin != null) {
            origin.onClick(v);
        }
    }
    
}

执行以下代码,将会看到当我们点击该按钮的时候,会弹出 toast

“Hook Click Listener”
mBtn1 = (Button) findViewById(R.id.btn_1);
mBtn1.setOnClickListener(this);
try {
    HookHelper.hookOnClickListener(mBtn1);
} catch (Exception e) {
    e.printStackTrace();
}

案例二:HooK Notification

发送消息到通知栏的核心代码如下:

NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(id, builder.build());

跟踪 notify 方法发现最终会调用到 notifyAsUser 方法

public void notify(String tag, int id, Notification notification)
{
    notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
}

而在 notifyAsUser 方法中,我们惊喜地发现 service 是一个单例,因此,我们可以想方法 hook 住这个 service,而 notifyAsUser 最终会调用到 service的enqueueNotificationWithTag 方法。因此 hook 住 service 的 enqueueNotificationWithTag 方法即可

public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
    // 
    INotificationManager service = getService();
    String pkg = mContext.getPackageName();
    // Fix the notification as best we can.
    Notification.addFieldsFromContext(mContext, notification);
    if (notification.sound != null) {
        notification.sound = notification.sound.getCanonicalUri();
        if (StrictMode.vmFileUriExposureEnabled()) {
            notification.sound.checkFileUriExposed("Notification.sound");
        }
    }
    fixLegacySmallIcon(notification, pkg);
    if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
        if (notification.getSmallIcon() == null) {
            throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                    + notification);
        }
    }
    if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
    final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
    try {
        service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                copy, user.getIdentifier());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

private static INotificationManager sService;

static public INotificationManager getService()
{
    if (sService != null) {
        return sService;
    }
    IBinder b = ServiceManager.getService("notification");
    sService = INotificationManager.Stub.asInterface(b);
    return sService;
}

综上,要 Hook Notification,大概需要三步:

第一步:得到 NotificationManager 的 sService 第二步:因为 sService 是接口,所以我们可以使用动态代理,获取动态代理对象 第三步:偷梁换柱,使用动态代理对象 proxyNotiMng 替换系统的 sService 于是,我们可以写出如下的代码

public static void hookNotificationManager(final Context context) throws Exception {
    NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

    Method getService = NotificationManager.class.getDeclaredMethod("getService");
    getService.setAccessible(true);
    // 第一步:得到系统的 sService
    final Object sOriginService = getService.invoke(notificationManager);

    Class iNotiMngClz = Class.forName("android.app.INotificationManager");
    // 第二步:得到我们的动态代理对象
    Object proxyNotiMng = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
            Class[]{iNotiMngClz}, new InvocationHandler() {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Log.d(TAG, "invoke(). method:" + method);
            String name = method.getName();
            Log.d(TAG, "invoke: name=" + name);
            if (args != null && args.length > 0) {
                for (Object arg : args) {
                    Log.d(TAG, "invoke: arg=" + arg);
                }
            }
            Toast.makeText(context.getApplicationContext(), "检测到有人发通知了", Toast.LENGTH_SHORT).show();
            // 操作交由 sOriginService 处理,不拦截通知
            return method.invoke(sOriginService, args);
            // 拦截通知,什么也不做
            //                    return null;
            // 或者是根据通知的 Tag 和 ID 进行筛选
        }
    });
    // 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 sService
    Field sServiceField = NotificationManager.class.getDeclaredField("sService");
    sServiceField.setAccessible(true);
    sServiceField.set(notificationManager, proxyNotiMng);

}

案例三:Hook ClipboardManager

方法1

大概需要三个步骤

  • 第一步:得到 ClipboardManager 的 mService
  • 第二步:初始化动态代理对象
  • 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 mService
public static void hookClipboardService(final Context context) throws Exception {
    ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
    Field mServiceFiled = ClipboardManager.class.getDeclaredField("mService");
    mServiceFiled.setAccessible(true);
    // 第一步:得到系统的 mService
    final Object mService = mServiceFiled.get(clipboardManager);
    
    // 第二步:初始化动态代理对象
    Class aClass = Class.forName("android.content.IClipboard");
    Object proxyInstance = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
            Class[]{aClass}, new InvocationHandler() {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Log.d(TAG, "invoke(). method:" + method);
            String name = method.getName();
            if (args != null && args.length > 0) {
                for (Object arg : args) {
                    Log.d(TAG, "invoke: arg=" + arg);
                }
            }
            if ("setPrimaryClip".equals(name)) {
                Object arg = args[0];
                if (arg instanceof ClipData) {
                    ClipData clipData = (ClipData) arg;
                    int itemCount = clipData.getItemCount();
                    for (int i = 0; i < itemCount; i++) {
                        ClipData.Item item = clipData.getItemAt(i);
                        Log.i(TAG, "invoke: item=" + item);
                    }
                }
                Toast.makeText(context, "检测到有人设置粘贴板内容", Toast.LENGTH_SHORT).show();
            } else if ("getPrimaryClip".equals(name)) {
                Toast.makeText(context, "检测到有人要获取粘贴板的内容", Toast.LENGTH_SHORT).show();
            }
            // 操作交由 sOriginService 处理,不拦截通知
            return method.invoke(mService, args);

        }
    });

    // 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 mService
    Field sServiceField = ClipboardManager.class.getDeclaredField("mService");
    sServiceField.setAccessible(true);
    sServiceField.set(clipboardManager, proxyInstance);

}

方法2

对 Android 源码有基本了解的人都知道,Android 中的各种 Manager 都是通过 ServiceManager 获取的。因此,我们可以通过 ServiceManager hook 所有系统 Manager,ClipboardManager 当然也不例外。

public final class ServiceManager {


    /**
     * Returns a reference to a service with the given name.
     * 
     * @param name the name of the service to get
     * @return a reference to the service, or <code>null</code> if the service doesn't exist
     */
    public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return getIServiceManager().getService(name);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }
}

老套路

  • 第一步:通过反射获取剪切板服务的远程Binder对象,这里我们可以通过 ServiceManager getService 方法获得
  • 第二步:创建我们的动态代理对象,动态代理原来的Binder对象
  • 第三步:偷梁换柱,把我们的动态代理对象设置进去
public static void hookClipboardService() throws Exception {

    //通过反射获取剪切板服务的远程Binder对象
    Class serviceManager = Class.forName("android.os.ServiceManager");
    Method getServiceMethod = serviceManager.getMethod("getService", String.class);
    IBinder remoteBinder = (IBinder) getServiceMethod.invoke(null, Context.CLIPBOARD_SERVICE);

    //新建一个我们需要的Binder,动态代理原来的Binder对象
    IBinder hookBinder = (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),
            new Class[]{IBinder.class}, new ClipboardHookRemoteBinderHandler(remoteBinder));

    //通过反射获取ServiceManger存储Binder对象的缓存集合,把我们新建的代理Binder放进缓存
    Field sCacheField = serviceManager.getDeclaredField("sCache");
    sCacheField.setAccessible(true);
    Map<String, IBinder> sCache = (Map<String, IBinder>) sCacheField.get(null);
    sCache.put(Context.CLIPBOARD_SERVICE, hookBinder);

}
public class ClipboardHookRemoteBinderHandler implements InvocationHandler {

    private IBinder remoteBinder;
    private Class iInterface;
    private Class stubClass;

    public ClipboardHookRemoteBinderHandler(IBinder remoteBinder) {
        this.remoteBinder = remoteBinder;
        try {
            this.iInterface = Class.forName("android.content.IClipboard");
            this.stubClass = Class.forName("android.content.IClipboard$Stub");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.d("RemoteBinderHandler", method.getName() + "() is invoked");
        if ("queryLocalInterface".equals(method.getName())) {
            //这里不能拦截具体的服务的方法,因为这是一个远程的Binder,还没有转化为本地Binder对象
            //所以先拦截我们所知的queryLocalInterface方法,返回一个本地Binder对象的代理
            return Proxy.newProxyInstance(remoteBinder.getClass().getClassLoader(),
                    new Class[]{this.iInterface},
                    new ClipboardHookLocalBinderHandler(remoteBinder, stubClass));
        }

        return method.invoke(remoteBinder, args);
    }
}

案例四:Hook Activity

方法1

我们先来看一下 Activity startActivity 方法的调用流程。

@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
    if (options != null) {
        startActivityForResult(intent, -1, options);
    } else {
        // Note we want to go through this call for compatibility with
        // applications that may have overridden the method.
        startActivityForResult(intent, -1);
    }
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
    startActivityForResult(intent, requestCode, null);
}


public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
        @Nullable Bundle options) {
    if (mParent == null) {
        options = transferSpringboardActivityOptions(options);
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options);
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                ar.getResultData());
        }
        if (requestCode >= 0) {
            // If this start is requesting a result, we can avoid making
            // the activity visible until the result is received.  Setting
            // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
            // activity hidden during this time, to avoid flickering.
            // This can only be done when a result is requested because
            // that guarantees we will get information back when the
            // activity is finished, no matter what happens to it.
            mStartedActivity = true;
        }

        cancelInputsAndStartExitTransition(options);
        // TODO Consider clearing/flushing other event sources and events for child windows.
    } else {
        if (options != null) {
            mParent.startActivityFromChild(this, intent, requestCode, options);
        } else {
            // Note we want to go through this method for compatibility with
            // existing applications that may have overridden it.
            mParent.startActivityFromChild(this, intent, requestCode);
        }
    }
}

首先,我们先来看一下 startActivityForResult 方法,当 mParent 为 null 的时候,会调用到 mInstrumentation.execStartActivity 方法。当 mParent 不为 null 时,都会调用到 mParent.startActivityFromChild 方法。而 mParent 为 Activity 实例,接下来我们一起看一下 startActivityFromChild 方法。

public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
        int requestCode, @Nullable Bundle options) {
    options = transferSpringboardActivityOptions(options);
    Instrumentation.ActivityResult ar =
        mInstrumentation.execStartActivity(
            this, mMainThread.getApplicationThread(), mToken, child,
            intent, requestCode, options);
    if (ar != null) {
        mMainThread.sendActivityResult(
            mToken, child.mEmbeddedID, requestCode,
            ar.getResultCode(), ar.getResultData());
    }
    cancelInputsAndStartExitTransition(options);
}

可以看到 startActivityFromChild 中也会调用 mInstrumentation.execStartActivity 方法。因此,即我们通过 Activity startActivity 的方法启动 activity,最终都会调用到 mInstrumentation.execStartActivity 方法。因此,如果我们想要拦截的话,可以 hook 住 mInstrumentation。

由于 mInstrumentation 是类,不是 interface,不能使用动态代理的方式,因此,这里我们使用静态代理的方式。

下面让我们一起看一下 怎样 hook activity 的 mInstrumentation

第一步:拿到当前 activity 的 mInstrumentation 第二步:创建代理对象 第三步:将我们的代理替换原 activity 的 mInstrumentation

public static void replaceInstrumentation(Activity activity) throws Exception {
    Class<?> k = Activity.class;
    //通过Activity.class 拿到 mInstrumentation字段
    Field field = k.getDeclaredField("mInstrumentation");
    field.setAccessible(true);
    //根据activity内mInstrumentation字段 获取Instrumentation对象
    Instrumentation instrumentation = (Instrumentation) field.get(activity);
    //创建代理对象
    Instrumentation instrumentationProxy = new ActivityProxyInstrumentation(instrumentation);
    //进行替换
    field.set(activity, instrumentationProxy);
}
public  class ActivityProxyInstrumentation extends Instrumentation {

    private static final String TAG = "ActivityProxyInstrumentation";

    // ActivityThread中原始的对象, 保存起来
    Instrumentation mBase;

    public ActivityProxyInstrumentation(Instrumentation base) {
        mBase = base;
    }

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

        // Hook之前, 可以输出你想要的!
        Log.d(TAG,"xxxx: 执行了startActivity, 参数如下: " + "who = [" + who + "], " +
                "contextThread = [" + contextThread + "], token = [" + token + "], " +
                "target = [" + target + "], intent = [" + intent +
                "], requestCode = [" + requestCode + "], options = [" + options + "]");

        // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
        // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mBase, who,
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            // rom修改了 需要手动适配
            throw new RuntimeException("do not support!!! pls adapt it");
        }
    }


}

在 ActivityProxyInstrumentation 里面,我们打印相应的 log。

运行以下测试代码

try {
    HookHelper.replaceInstrumentation(this);
} catch (Exception e) {
    e.printStackTrace();
}
startActivity(new Intent(this,TestActivityStart.class));

将会看到我们插入的代码

方法2

我们先来看一下 getApplicationContext startActivity 的调用关系

图片.png 因此,这里我们要 hook 的是 ActivityThread 的 mInstrumentation

public static void attachContext() throws Exception {
    Log.i(TAG, "attachContext: ");
    // 先获取到当前的ActivityThread对象
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    //currentActivityThread是一个static函数所以可以直接invoke,不需要带实例参数
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);

    // 拿到原始的 mInstrumentation字段
    Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
    mInstrumentationField.setAccessible(true);
    Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
    // 创建代理对象
    Instrumentation evilInstrumentation = new ApplicationInstrumentation(mInstrumentation);
    // 偷梁换柱
    mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}
public class ApplicationInstrumentation extends Instrumentation {

    private static final String TAG = "ApplicationInstrumentation";

    // ActivityThread中原始的对象, 保存起来
    Instrumentation mBase;

    public ApplicationInstrumentation(Instrumentation base) {
        mBase = base;
    }

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

        // Hook之前, 可以输出你想要的!
        Log.d(TAG, "xxxx: 执行了startActivity, 参数如下: " + "who = [" + who + "], " + "contextThread = " +
                "" + "" + "[" + contextThread + "], token = [" + token + "], " + "target = [" + 
                target + "], intent = [" + intent + "], requestCode = [" + requestCode + "], " +
                "options = " + "[" + options + "]");

        // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
        // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod
                    ("execStartActivity", Context.class, IBinder.class, IBinder.class, Activity
                            .class, Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mBase, who, contextThread, token, 
                    target, intent, requestCode, options);
        } catch (Exception e) {
            // rom修改了 需要手动适配
            throw new RuntimeException("do not support!!! pls adapt it");
        }
    }
    
}

可以看到在 ApplicationInstrumentation 里面,我们只是打印出 startActivity 中各个方法参数的值。

运行以下测试代码

try {
    HookHelper.attachContext();
} catch (Exception e) {
    e.printStackTrace();
}
Intent intent = new Intent(this, TestActivityStart.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getApplicationContext().startActivity(intent);

将看到以下 log

方法3 hook AMS

上面 hook activity 的两种方法其实都有一定缺陷,比如,第一种方法,只能 hook 住通过 Activity startActivity 的 activity。第二种方法,只能 hook 住通过 getApplicationContext().startActivity 启动的 activity。那有没有一种方法能 hook 上述两种的,其实是有的,那就是 hook AMS。下面让我们一起来看一下。

上面 hook startActivity 其实都是 hook 相应的 mInstrumentation.execStartActivity 方法,因此,我们可以从这里下手,看 mInstrumentation.execStartActivity 里面有没有一些共性的东西,可以 hook。

我们先来 mInstrumentation.execStartActivity 方法

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    Uri referrer = target != null ? target.onProvideReferrer() : null;
    if (referrer != null) {
        intent.putExtra(Intent.EXTRA_REFERRER, referrer);
    }
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                ActivityResult result = null;
                if (am.ignoreMatchingSpecificIntents()) {
                    result = am.onStartActivity(intent);
                }
                if (result != null) {
                    am.mHits++;
                    return result;
                } else if (am.match(who, null, intent)) {
                    am.mHits++;
                    if (am.isBlocking()) {
                        return requestCode >= 0 ? am.getResult() : null;
                    }
                    break;
                }
            }
        }
    }
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        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);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

这里我们留意 ActivityManager.getService().startActivity 这个方法

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

可以看到 IActivityManagerSingleton 是一个单例对象,因此,我们可以 hook 它。

public static void hookAMSAfter26() throws Exception {
    // 第一步:获取 IActivityManagerSingleton
    Class<?> aClass = Class.forName("android.app.ActivityManager");
    Field declaredField = aClass.getDeclaredField("IActivityManagerSingleton");
    declaredField.setAccessible(true);
    Object value = declaredField.get(null);
    
    Class<?> singletonClz = Class.forName("android.util.Singleton");
    Field instanceField = singletonClz.getDeclaredField("mInstance");
    instanceField.setAccessible(true);
    Object iActivityManagerObject = instanceField.get(value);
    
    // 第二步:获取我们的代理对象,这里因为 IActivityManager 是接口,我们使用动态代理的方式
    Class<?> iActivity = Class.forName("android.app.IActivityManager");
    InvocationHandler handler = new AMSInvocationHandler(iActivityManagerObject);
    Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new
            Class<?>[]{iActivity}, handler);
    
    // 第三步:偷梁换柱,将我们的 proxy 替换原来的对象
    instanceField.set(value, proxy);
}

public class AMSInvocationHandler implements InvocationHandler {

    private static final String TAG = "AMSInvocationHandler";

    Object iamObject;

    public AMSInvocationHandler(Object iamObject) {
        this.iamObject = iamObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //            Log.e(TAG, method.getName());
        if ("startActivity".equals(method.getName())) {
            Log.i(TAG, "ready to startActivity");
            for (Object object : args) {
                Log.d(TAG, "invoke: object=" + object);
            }
        }
        return method.invoke(iamObject, args);
    }
}

执行以下测试代码

try {
    HookHelper.hookAMS();
} catch (Exception e) {
    e.printStackTrace();
}
startActivity(new Intent(this,TestActivityStart.class));

将会看到以下 log

I/AMSInvocationHandler: ready to startActivity

接下来我们一起来看一下 API 25 Instrumentation 的代码(自 API 26 开始 ,Instrumentation execStartActivity 方法有所改变)

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    Uri referrer = target != null ? target.onProvideReferrer() : null;
    if (referrer != null) {
        intent.putExtra(Intent.EXTRA_REFERRER, referrer);
    }
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                if (am.match(who, null, intent)) {
                    am.mHits++;
                    if (am.isBlocking()) {
                        return requestCode >= 0 ? am.getResult() : null;
                    }
                    break;
                }
            }
        }
    }
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        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;
}

可以看到这里启动 activity 是调用 ActivityManagerNative.getDefault().startActivity 启动的。

public abstract class ActivityManagerNative extends Binder implements IActivityManager
{
   
    /**
     * Retrieve the system's default/global activity manager.
     */
    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;
        }
    };
    
}

同理我们看到 ActivityManagerNativegDefault 是一个静态变量,因此,我们可以尝试 hook gDefault.

public static void hookAmsBefore26() throws Exception {
    // 第一步:获取 IActivityManagerSingleton
    Class<?> forName = Class.forName("android.app.ActivityManagerNative");
    Field defaultField = forName.getDeclaredField("gDefault");
    defaultField.setAccessible(true);
    Object defaultValue = defaultField.get(null);

    Class<?> forName2 = Class.forName("android.util.Singleton");
    Field instanceField = forName2.getDeclaredField("mInstance");
    instanceField.setAccessible(true);
    Object iActivityManagerObject = instanceField.get(defaultValue);

    // 第二步:获取我们的代理对象,这里因为 IActivityManager 是接口,我们使用动态代理的方式
    Class<?> iActivity = Class.forName("android.app.IActivityManager");
    InvocationHandler handler = new AMSInvocationHandler(iActivityManagerObject);
    Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivity}, handler);

    // 第三步:偷梁换柱,将我们的 proxy 替换原来的对象
    instanceField.set(defaultValue, proxy);
}

到此,hook Activity 的三种方式已讲解完毕

启动一个没有在 AndroidManifest 声明的 Activity

我们知道,当我们启动一个没有在 AndroidManifest 中声明的 activity,会抛出 ActivityNotFoundException 异常。

Caused by: android.content.ActivityNotFoundException: Unable to find explicit activity class {com.xj.hookdemo/com.xj.hookdemo.activityhook.TargetAppCompatActivity}; have you declared this activity in your AndroidManifest.xml?

从报错的堆栈中,我们非常定位到 Instrumentation.execStartActivity 方法

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, String resultWho,
        Intent intent, int requestCode, Bundle options, UserHandle user) {
       -----  // 省略若干代码
       
      
       
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        int result = ActivityManager.getService()
            .startActivityAsUser(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, resultWho,
                    requestCode, 0, null, options, user.getIdentifier());
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

在该方法中,调用 startActivityAsUser 方法通过传入的 intent 获取 result,再通过 checkStartActivityResult 方法,判断 result 是否合法。

而我们知道我们启动的 activity 信息都储存在 intent 中,那么我们若想要 启动一个没有在 AndroidManifest 声明的 Activity,那我们只需要在 某个时机,即调用 startActivity 方法之前欺骗 AMS 我们的 activity 已经注册(即替换 intent),这样就不会抛出 ActivityNotFoundException 异常。

在前面的时候,我们已经讲解到如何 hook ams,这里我们不再具体讲述,主要步骤如下

第一步, API 26 以后,hook android.app.ActivityManager.IActivityManagerSingleton, API 25 以前,hook android.app.ActivityManagerNative.gDefault 第二步,获取我们的代理对象,这里因为是接口,所以我们使用动态代理的方式 第三步:设置为我们的代理对象

private static void hookAMS(Context context) throws ClassNotFoundException,
        NoSuchFieldException, IllegalAccessException {
    // 第一步,  API 26 以后,hook android.app.ActivityManager.IActivityManagerSingleton,
    //  API 25 以前,hook android.app.ActivityManagerNative.gDefault
    Field gDefaultField = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        Class<?> activityManager = Class.forName("android.app.ActivityManager");
        gDefaultField = activityManager.getDeclaredField("IActivityManagerSingleton");
    } else {
        Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
        gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
    }
    gDefaultField.setAccessible(true);
    Object gDefaultObj = gDefaultField.get(null); //所有静态对象的反射可以通过传null获取。如果是实列必须传实例
    Class<?> singletonClazz = Class.forName("android.util.Singleton");
    Field amsField = singletonClazz.getDeclaredField("mInstance");
    amsField.setAccessible(true);
    Object amsObj = amsField.get(gDefaultObj);

    //
    String pmName = getPMName(context);
    String hostClzName = getHostClzName(context, pmName);

    // 第二步,获取我们的代理对象,这里因为是接口,所以我们使用动态代理的方式
    amsObj = Proxy.newProxyInstance(context.getClass().getClassLoader(), amsObj.getClass()
            .getInterfaces(), new AMSHookInvocationHandler(amsObj, pmName, hostClzName));

    // 第三步:设置为我们的代理对象
    amsField.set(gDefaultObj, amsObj);
}

接着,我们在动态代理对象中,当调用 startActivity 方法的时候,我们把 intent 信息替换,校验的时候就可以绕过系统对 activity 的校验,这样就不会跑出 ActivityNotFoundException 异常。

public class AMSHookInvocationHandler implements InvocationHandler {

    public static final String ORIGINALLY_INTENT = "originallyIntent";
    private Object mAmsObj;
    private String mPackageName;
    private String cls;

    public AMSHookInvocationHandler(Object amsObj, String packageName, String cls) {
        this.mAmsObj = amsObj;
        this.mPackageName = packageName;
        this.cls = cls;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //  对 startActivity进行Hook
        if (method.getName().equals("startActivity")) {
            int index = 0;
            //  找到我们启动时的intent
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            
            // 取出在真实的Intent
            Intent originallyIntent = (Intent) args[index];
            Log.i("AMSHookUtil", "AMSHookInvocationHandler:" + originallyIntent.getComponent()
                    .getClassName());
            // 自己伪造一个配置文件已注册过的Activity Intent
            Intent proxyIntent = new Intent();
            //  因为我们调用的Activity没有注册,所以这里我们先偷偷换成已注册。使用一个假的Intent
            ComponentName componentName = new ComponentName(mPackageName, cls);
            proxyIntent.setComponent(componentName);
            // 在这里把未注册的Intent先存起来 一会儿我们需要在Handle里取出来用
            proxyIntent.putExtra(ORIGINALLY_INTENT, originallyIntent);
            args[index] = proxyIntent;
        }
        return method.invoke(mAmsObj, args);
    }
}

但是,如果仅仅这样做,会存在一个问题,因为 intent 信息在校验的时候被我们替换了,但是我们并没有将其还原,这样,启动的 activity 就不是我们想要的 activity。

那么,我们要在哪个实际将 intent 信息还原呢?

我们回过头再来看一下 Activity 的 startActivityForResult 方法

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
        @Nullable Bundle options) {
    if (mParent == null) {
        options = transferSpringboardActivityOptions(options);
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options);
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                ar.getResultData());
        }
        if (requestCode >= 0) {
            // If this start is requesting a result, we can avoid making
            // the activity visible until the result is received.  Setting
            // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
            // activity hidden during this time, to avoid flickering.
            // This can only be done when a result is requested because
            // that guarantees we will get information back when the
            // activity is finished, no matter what happens to it.
            mStartedActivity = true;
        }

        cancelInputsAndStartExitTransition(options);
        // TODO Consider clearing/flushing other event sources and events for child windows.
    } else {
        if (options != null) {
            mParent.startActivityFromChild(this, intent, requestCode, options);
        } else {
            // Note we want to go through this method for compatibility with
            // existing applications that may have overridden it.
            mParent.startActivityFromChild(this, intent, requestCode);
        }
    }
}

该方法主要分为两个逻辑,当 mParent 为空的时候即不为空的时候

第一种情况,mParent 不为空的时候,调用到 mInstrumentation.execStartActivity 方法之后,会调用 mMainThread.sendActivityResult 方法 第二种情况,当 mParent 为空的时候,会调用 mParent.startActivityFromChild

public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
        int requestCode, @Nullable Bundle options) {
    options = transferSpringboardActivityOptions(options);
    Instrumentation.ActivityResult ar =
        mInstrumentation.execStartActivity(
            this, mMainThread.getApplicationThread(), mToken, child,
            intent, requestCode, options);
    if (ar != null) {
        mMainThread.sendActivityResult(
            mToken, child.mEmbeddedID, requestCode,
            ar.getResultCode(), ar.getResultData());
    }
    cancelInputsAndStartExitTransition(options);
}

在 startActivityFromChild 方法里面,又会调用到 mMainThread.sendActivityResult 方法。因此,我们只需看一下该方法是怎样 send ActivityResult 的。

public final class ActivityThread {

    ---

    public final void sendActivityResult(
            IBinder token, String id, int requestCode,
            int resultCode, Intent data) {
        if (DEBUG_RESULTS) Slog.v(TAG, "sendActivityResult: id=" + id
                + " req=" + requestCode + " res=" + resultCode + " data=" + data);
        ArrayList<ResultInfo> list = new ArrayList<ResultInfo>();
        list.add(new ResultInfo(id, requestCode, resultCode, data));
        mAppThread.scheduleSendResult(token, list);
    }
    public final void scheduleSendResult(IBinder token, List<ResultInfo> results) {
        ResultData res = new ResultData();
        res.token = token;
        res.results = results;
        sendMessage(H.SEND_RESULT, res);
    }
    private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
        if (DEBUG_MESSAGES) Slog.v(
            TAG, "SCHEDULE " + what + " " + mH.codeToString(what)
            + ": " + arg1 + " / " + obj);
        Message msg = Message.obtain();
        msg.what = what;
        msg.obj = obj;
        msg.arg1 = arg1;
        msg.arg2 = arg2;
        if (async) {
            msg.setAsynchronous(true);
        }
        mH.sendMessage(msg);
    }
    
    final H mH = new H();
}

跟踪 ActivityThread 的代码发现 sendActivityResult 方法会调用 scheduleSendResult 方法发送,而 scheduleSendResult 方法又会调用 sendMessage 方法,在 sendMessage 方法里面,会调用 mH 发送消息(即 Handler)

因此,我们只需要在回调 H 的 handleMessage 消息之前还原我们的 intent 信息即可。

private class H extends Handler {
    
    public void handleMessage(Message msg) {
        if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
        switch (msg.what) {
            case LAUNCH_ACTIVITY: {
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                r.packageInfo = getPackageInfoNoCheck(
                        r.activityInfo.applicationInfo, r.compatInfo);
                handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            } break;

}

ok,这里我们重新理一下 Activity 大概的启动流程:

app 调用 startActivity 方法 -> Instrumentation 类通过 ActivityManagerNative 或者 ActivityManager( API 26以后)将启动请求发送给 AMS -> AMS 进行一系列检查并将此请求通过 Binder 派发给所属 app -> app 通过 Binder 收到这个启动请求 -> ActivityThread 中的实现将收到的请求进行封装后送入 Handler -> 从 Handler 中取出这个消息,开始 app 本地的 Activity 初始化和启动逻辑。

hook ActivityThread 的 mH

/**
 *
 * @param context
 * @param isAppCompatActivity 表示是否是 AppCompatActivity
 * @throws Exception
 */
private static void hookLaunchActivity(Context context, boolean isAppCompatActivity) throws
        Exception {
    Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
    Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
    sCurrentActivityThreadField.setAccessible(true);
    Object sCurrentActivityThreadObj = sCurrentActivityThreadField.get(null);

    Field mHField = activityThreadClazz.getDeclaredField("mH");
    mHField.setAccessible(true);
    Handler mH = (Handler) mHField.get(sCurrentActivityThreadObj);
    Field callBackField = Handler.class.getDeclaredField("mCallback");
    callBackField.setAccessible(true);
    callBackField.set(mH, new ActivityThreadHandlerCallBack(context, isAppCompatActivity));
}

public static class ActivityThreadHandlerCallBack implements Handler.Callback {

    private final boolean mIsAppCompatActivity;
    private final Context mContext;

    public ActivityThreadHandlerCallBack(Context context, boolean isAppCompatActivity) {
        mIsAppCompatActivity = isAppCompatActivity;
        mContext = context;
    }

    @Override
    public boolean handleMessage(Message msg) {
        int LAUNCH_ACTIVITY = 0;
        try {
            Class<?> clazz = Class.forName("android.app.ActivityThread$H");
            Field field = clazz.getField("LAUNCH_ACTIVITY");
            LAUNCH_ACTIVITY = field.getInt(null);
        } catch (Exception e) {
        }
        if (msg.what == LAUNCH_ACTIVITY) {
            handleLaunchActivity(mContext, msg, mIsAppCompatActivity);
        }
        return false;
    }
}

private static void handleLaunchActivity(Context context, Message msg, boolean
        isAppCompatActivity) {
    try {
        Object obj = msg.obj;
        Field intentField = obj.getClass().getDeclaredField("intent");
        intentField.setAccessible(true);
        Intent proxyIntent = (Intent) intentField.get(obj);
        //拿到之前真实要被启动的Intent 然后把Intent换掉
        Intent originallyIntent = proxyIntent.getParcelableExtra(ORIGINALLY_INTENT);
        if (originallyIntent == null) {
            return;
        }
        proxyIntent.setComponent(originallyIntent.getComponent());

        Log.e(TAG, "handleLaunchActivity:" + originallyIntent.getComponent().getClassName());

        // 如果不需要兼容 AppCompatActivity
        if (!isAppCompatActivity) {
            return;
        }

        //兼容AppCompatActivity,假如不加上该方法,当 activity instanceOf AppCompatActivity 时,会抛出  PackageManager$NameNotFoundException 异常。
        hookPM(context);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

运行以上代码,当你启动一个没有在 AndroidManifest 注册的 Activity,你会发现是可以正常启动的。但是,当未注册的 Activity 是 AppCompatActivity 的子类的时候,会抛出以下异常

Caused by: java.lang.IllegalArgumentException: android.content.pm.PackageManager$NameNotFoundException: ComponentInfo{com.xj.hookdemo/com.xj.hookdemo.activityhook.TargetAppCompatActivity}
at android.support.v4.app.NavUtils.getParentActivityName(NavUtils.java:215)
at android.support.v7.app.AppCompatDelegateImplV9.onCreate(AppCompatDelegateImplV9.java:155)
at android.support.v7.app.AppCompatDelegateImplV14.onCreate(AppCompatDelegateImplV14.java:59)
at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:72)
at com.xj.hookdemo.activityhook.TargetAppCompatActivity.onCreate(TargetAppCompatActivity.java:12)
at android.app.Activity.performCreate(Activity.java:7026)
at android.app.Activity.performCreate(Activity.java:7017)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1231)

从上面的异常信息来看,主要是在 NavUtils.getParentActivityName 方法中抛出异常。

@Nullable
public static String getParentActivityName(Context context, ComponentName componentName)
        throws NameNotFoundException {
    PackageManager pm = context.getPackageManager();
    ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA);
    if (Build.VERSION.SDK_INT >= 16) {
        String result = info.parentActivityName;
        if (result != null) {
            return result;
        }
    }
    if (info.metaData == null) {
        return null;
    }
    String parentActivity = info.metaData.getString(PARENT_ACTIVITY);
    if (parentActivity == null) {
        return null;
    }
    if (parentActivity.charAt(0) == '.') {
        parentActivity = context.getPackageName() + parentActivity;
    }
    return parentActivity;
}

而该方法中,是调用 PackageManager getActivityInfo 中去查询。因此,我们只需要 hook PackageManager


private static void hookPM(Context context) throws ClassNotFoundException,
        NoSuchFieldException, IllegalAccessException, NoSuchMethodException,
        InvocationTargetException {
    String pmName = getPMName(context);
    String hostClzName = getHostClzName(context, pmName);
    
    Class<?> forName = Class.forName("android.app.ActivityThread");
    Field field = forName.getDeclaredField("sCurrentActivityThread");
    field.setAccessible(true);
    Object activityThread = field.get(null);
    Method getPackageManager = activityThread.getClass().getDeclaredMethod("getPackageManager");
    Object iPackageManager = getPackageManager.invoke(activityThread);
    PackageManagerHandler handler = new PackageManagerHandler(iPackageManager, pmName, hostClzName);
    Class<?> iPackageManagerIntercept = Class.forName("android.content.pm.IPackageManager");
    Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new
            Class<?>[]{iPackageManagerIntercept}, handler);
    // 获取 sPackageManager 属性
    Field iPackageManagerField = activityThread.getClass().getDeclaredField("sPackageManager");
    iPackageManagerField.setAccessible(true);
    iPackageManagerField.set(activityThread, proxy);
}

运行以上代码,可以看到我们可以正常启动没有在 AndroidManifest 的 activity。已经完美兼容 Android 8.0,AppCompactActivity。

小结

启动没有在 AndroidManifest 注册的 Activity 课改可以分为2个步骤

在 AMS 通过 intent 校验 activity 是否注册的时候,用已经在 AndroidManifet 注册的 Activity 欺骗 AMS,绕过 原有 activity 的校验,并将原有的 intent 信息储存起来 在 AMS 校验完毕的时候,通过 binder 告知我们的应用启动相应 activity 的时候,我们将 intent 的信息取出来,还原。