如何应对Andriod面试官 -> 如何 Hook Activity 的启动流程?

前言


本章继续讲解 AMS 相关的知识点,如何 Hook Activity 的启动流程;

attach


我们这样直接从 attach 入口讲起

java 复制代码
private void attach(boolean system, long startSeq) {
    // 省略部分代码
    // AMS
    final IActivityManager mgr = ActivityManager.getService();
    try {
        // 这里直接调用到了 AMS 的 attachApplication 方法;
        mgr.attachApplication(mAppThread, startSeq);
    } catch (RemoteException ex) {
        throw ex.rethrowFromSystemServer();
    } 
}

attach 中直接调用到了 AMS 的 attachApplication 方法,我们进入这个方法看下:

arduino 复制代码
public final void attachApplication(IApplicationThread thread, long startSeq) {
    if (thread == null) {
        throw new SecurityException("Invalid application interface");
    }
    synchronized (this) {
        // 获取调用端的 pid
        int callingPid = Binder.getCallingPid();
        // 获取调用端的 uid
        final int callingUid = Binder.getCallingUid();
        // 把 pid/uid 设置成 AMS 的
        final long origId = Binder.clearCallingIdentity();
        // 
        attachApplicationLocked(thread, callingPid, callingUid, startSeq);
        // 恢复为 app 的
        Binder.restoreCallingIdentity(origId);
    }
}

这里有一个比较关键的点就是:把 binder 的 id 设置成 AMS 的,这样做是为了权限验证;Binder.clearCallingIdentity() 最终调用到 IPCThreadState.cpp 中的同名方法,我们进入这个 cpp 文件的同名方法看下:

arduino 复制代码
int64_t IPCThreadState::clearCallingIdentity() { 
    int64_t token = ((int64_t)mCallingUid<<32) | mCallingPid; 
    clearCaller(); 
    return token; 
}

这里调用了 clearCaller 函数,我们进入这个函数看下:

ini 复制代码
// 清空远程调用端的uid和pid,用当前本地进程的uid和pid替代
void IPCThreadState::clearCaller() { 
    mCallingPid = getpid(); 
    mCallingUid = getuid(); 
}

restoreCallingIdentity 也是调用的同名方法;

arduino 复制代码
// 从token中解析出pid和uid,并赋值给相应变量,该方法正好是clearCallingIdentity的相反过程
void IPCThreadState::restoreCallingIdentity(int64_t token) { 
    mCallingUid = (int)(token>>32); 
    mCallingPid = (int)token; 
}

AndroidManifest.xml 中的 Activity 是在哪里注册的?


我们在启动一个 Activity 的时候,如果没有在 AndroidManifest 中进行注册的话,就会抛出一段异常,那么 AndroidManifest.xml 中的 Activity 是在哪里注册的呢?

我们直接进入启动 Activity 的代码中看下:

scss 复制代码
public ActivityResult execStartActivity(
    Context who, IBinder contextThread, IBinder token, String target,
    Intent intent, int requestCode, Bundle options) {
    // 省略部分代码
    
    // 调用 AMS 的 startActivity 方法并获取一个放回结果,然后检测这个结果
    int result = ActivityTaskManager.getService().startActivity(whoThread,
            who.getOpPackageName(), who.getAttributionTag(), intent,
            intent.resolveTypeIfNeeded(who.getContentResolver()), token, target,
            requestCode, 0, null, options);
    checkStartActivityResult(result, intent);
}

这里调用 AMS 的 startActivity 方法并获取一个返回结果,然后检测这个结果;

typescript 复制代码
public static void checkStartActivityResult(int res, Object intent) {
    if (!ActivityManager.isStartResultFatalError(res)) {
        return;
    }

    switch (res) {
        case ActivityManager.START_INTENT_NOT_RESOLVED:
        case ActivityManager.START_CLASS_NOT_FOUND:
            if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
                throw new ActivityNotFoundException(
                        "Unable to find explicit activity class "
                        + ((Intent)intent).getComponent().toShortString()
                        + "; have you declared this activity in your AndroidManifest.xml?");
            throw new ActivityNotFoundException(
                    "No Activity found to handle " + intent);
     }
}

可以看到,在这个检测结果方法中,抛出了 Activity 未注册的异常,到这里,我们的报错点找到了;

我们接着来看下启动流程中是在哪里返回的 result 的,根据上一章节的启动流程介绍,我们很容易找到在 ActivityStarter 的 executeRequest 方法中返回了一个 result;

ini 复制代码
private int executeRequest(Request request) {
    // 省略部分代码
    
    // 可以看到 error 的初始值为 SUCCESS,默认初始化成功,我们接着往下看 err 的赋值的地方
    int err = ActivityManager.START_SUCCESS;
    
    // 权限拒绝的时候返回的 err 信息;
    if (caller != null) {
        callerApp = mService.getProcessController(caller);
        if (callerApp != null) {
            callingPid = callerApp.getPid();
            callingUid = callerApp.mInfo.uid;
        } else {
            // 权限拒绝返回 PERMISSION_DENIED
            err = ActivityManager.START_PERMISSION_DENIED;
        }
    }
    
    // 从 Intent 中无法找到相应的 Component
    if (err == ActivityManager.START_SUCCESS && intent.getComponent() == null) {
        err = ActivityManager.START_INTENT_NOT_RESOLVED;
    }
    // 从 Intent 中无法找到相应的 ActivityInfo
    if (err == ActivityManager.START_SUCCESS && aInfo == null) {
        // We couldn't find the specific class specified in the Intent.
        // Also the end of the line.
        err = ActivityManager.START_CLASS_NOT_FOUND;
    }
}

到这里的时候,我们就找到前面说的异常抛出的原因,是因为 aInfo == null,那么这个 aInfo 是怎么赋值的呢?它又是因为什么原因是 null 呢?我们接着往回看;

vbscript 复制代码
private int executeRequest(Request request) {
    // 是从 request 的 activityInfo 获取的,这个 request 是传递进来的,我们接着往上一层看
    ActivityInfo aInfo = request.activityInfo;
}

是从 request 的 activityInfo 获取的,这个 request 是传递进来的,我们接着往上一层看;

csharp 复制代码
int execute() {

    // 解析 activityInfo 
    if (mRequest.activityInfo == null) {
        mRequest.resolveActivity(mSupervisor);
    }
}

可以看到在 execute 中会执行 activityInfo 的解析逻辑,我们进入这个方法看下它在什么情况下会返回 null;

java 复制代码
void resolveActivity(ActivityTaskSupervisor supervisor) {
    // 如果 rInfo 不为 null 则获取 rInfo 的 activityInfo 赋值给 aInfo 并返回;
    final ActivityInfo aInfo = rInfo != null ? rInfo.activityInfo : null;
}

如果 rInfo 不为 null 则获取 rInfo 的 activityInfo 赋值给 aInfo 并返回;那么这个 rInfo 的 activityInfo 是什么情况在为空的呢?这个 rInfo 又是在哪里赋值并传递进来的呢?我们接着返回看;

javascript 复制代码
void resolveActivity(ActivityTaskSupervisor supervisor) {
    // 省略部分代码
    
    // 在这里获取 rInfo
    resolveInfo = supervisor.resolveIntent(intent, resolvedType, userId,
        0 /* matchFlags */,
        computeResolveFilterUid(callingUid, realCallingUid, filterCallingUid));
}

通过 resolveIntent 获取 rInfo,我们进入这个 resolveIntent 方法看下;

arduino 复制代码
ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId, int flags,
        int filterCallingUid) {
    // 省略部分代码
    // 返回 ResolveInfo
    try {
        return mService.getPackageManagerInternalLocked().resolveIntent(
                intent, resolvedType, modifiedFlags, privateResolveFlags, userId, true,
                filterCallingUid);
    } finally {
        Binder.restoreCallingIdentity(token);
    }
}

这里又回到了 AMS 中的 resolveIntent 方法,我们进入这个方法看下;

ini 复制代码
PackageManagerInternal getPackageManagerInternalLocked() {
    if (mPmInternal == null) {
        mPmInternal = LocalServices.getService(PackageManagerInternal.class);
    }
    return mPmInternal;
}

这里是先获取 PackageManagerInternal 然后调用其 resolveIntent 方法,这个 PackageManagerInternal 是一个抽象类,resolveIntent 是一个抽象方法,我们需要进入 PackageManagerInternal 的实现类看下, 它的具体实现就是 PackageManagerService,我们进入它的 resolveIntent 方法看下,它最终调用的是 resolveIntentInternal;

java 复制代码
private ResolveInfo resolveIntentInternal(Intent intent, String resolvedType, int flags,
        @PrivateResolveFlags int privateResolveFlags, int userId, boolean resolveForStart,
        int filterCallingUid) {
    // 省略部分代码
    // 获取 ResolveInfo 并返回
    final ResolveInfo bestChoice =
        chooseBestActivity(
                intent, resolvedType, flags, privateResolveFlags, query, userId,
                queryMayBeFiltered);
}

通过调用 chooseBestActivity 获取 ResolveInfo 并返回,我们进入这个方法看下;

java 复制代码
private ResolveInfo chooseBestActivity(Intent intent, String resolvedType,
        int flags, int privateResolveFlags, List<ResolveInfo> query, int userId,
        boolean queryMayBeFiltered) {
    
    // 省略部分的代码
    // 获取 ResolveInfo,如果不为空,直接返回
    ResolveInfo ri = findPreferredActivityNotLocked(intent, resolvedType,
        flags, query, r0.priority, true, false, debug, userId, queryMayBeFiltered);
    
    if (ri != null) {
        return ri;
    }
}

我们接着进入这个 findPreferredActivityNotLocked 方法看下;

arduino 复制代码
ResolveInfo findPreferredActivityNotLocked(Intent intent, String resolvedType, int flags,
        List<ResolveInfo> query, int priority, boolean always,
        boolean removeMatches, boolean debug, int userId, boolean queryMayBeFiltered) {
    // 省略部分代码
    
    // 这里主要是获取 ActivityInfo
    final ActivityInfo ai = getActivityInfo(
        pa.mPref.mComponent, flags | MATCH_DISABLED_COMPONENTS
                | MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE,
        userId);
}

findPreferredActivityNotLocked 主要是为了获取 ActivityInfo 并将 ActivityInfo 赋值给 ResolveInfo 的 resolveInfo;

arduino 复制代码
public final ActivityInfo getActivityInfo(ComponentName component, int flags, int userId) {
    return getActivityInfoInternal(component, flags, Binder.getCallingUid(), userId);
}
arduino 复制代码
public final ActivityInfo getActivityInfoInternal(ComponentName component, int flags,
        int filterCallingUid, int userId) {
    // 省略部分代码
    return getActivityInfoInternalBody(component, flags, filterCallingUid, userId);
}
java 复制代码
protected ActivityInfo getActivityInfoInternalBody(ComponentName component, int flags,
        int filterCallingUid, int userId) {
    ParsedActivity a = mComponentResolver.getActivity(component);

    if (DEBUG_PACKAGE_INFO) Log.v(TAG, "getActivityInfo " + component + ": " + a);

    AndroidPackage pkg = a == null ? null : mPackages.get(a.getPackageName());
    if (pkg != null && mSettings.isEnabledAndMatchLPr(pkg, a, flags, userId)) {
        PackageSetting ps = mSettings.getPackageLPr(component.getPackageName());
        if (ps == null) return null;
        if (shouldFilterApplicationLocked(
                ps, filterCallingUid, component, TYPE_ACTIVITY, userId)) {
            return null;
        }
        return PackageInfoUtils.generateActivityInfo(pkg,
                a, flags, ps.readUserState(userId), userId, ps);
    }
    if (resolveComponentName().equals(component)) {
        return PackageParser.generateActivityInfo(
                mResolveActivity, flags, new PackageUserState(), userId);
    }
    return null;
}

可以看到往下的逻辑,就是解析 AndroidManifest.xml 文件来获取对应的目标 Activity 信息并返回,如果解析不到,则抛出相应的异常,关于解析 AndroidManifest.xml 会在 PackageManagerService 的讲解中重点讲解;

Hook Activity


Hook Activity 本质就是绕过 AMS 的检查,可以启动我们并没有在 AndroidManifest 中声明的 Activity;

本章只是讲解下 Hook 的原理,具体实战可以关注我的后续文章,后面在插件化实战的时候,会具体实现 Hook AMS;

在 startActivity 的时候,会和 AMS 通信,经过 AMS 的验证之后,返回 app 所在进程,通过 Instrumention 创建目标 Activity,AMS 控制 Instrumention 执行 Activity 的生命周期;

也就说,我们不能传递真正的 Activity 给 AMS,我们需要传递一个假的(也就是在 AndroidManifest 中注册的 Activity),当 AMS 验证通过并返回到 app 的时候,我们需要再替换回目标 Activity 来完成,目标 Activity 的启动;

我们需要寻找至少两个 Hook 点;

  1. startActivity 的时候,将目标 Activity 替换成假的 Activity;
  2. AMS 验证通过之后,再替换回去;

整体的启动流程如上图,我们需要在这几个流程中找到比较容易 hook 的点;

找 hook 点,我们一般找 public static 的,这样比较容易反射修改;

从图中以及结合我们前面讲的启动流程,

  1. Instrumentation 是一个 hook 点(滴滴 VirtualAPK 开源框架方案);
  2. IActivityManager 是一个 hook 点(360 DroidPlugin 开源框架方案);

这里我贴了一下 360 的具体实现方案(仅 Hook IActivityManager 的地方)

ini 复制代码
public static void hookIActivityTaskManager(){
    try{
        Field singletonField = null;
        // 这里要适配不同的系统版本,后面插件化的时候会具体讲解
        Class<?> actvityManager = Class.forName("android.app.ActivityTaskManager");
        singletonField = actvityManager.getDeclaredField("IActivityTaskManagerSingleton");
        singletonField.setAccessible(true);
        Object singleton = singletonField.get(null);
        // 拿IActivityManager对象
        Class<?> singletonClass = Class.forName("android.util.Singleton");
        Field mInstanceField = singletonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);
        // 原始的IActivityTaskManager
        final Object IActivityTaskManager = mInstanceField.get(singleton);

        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader()
                , new Class[]{Class.forName("android.app.IActivityTaskManager")}
                , new InvocationHandler() {
                    @Override
                    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {

                        Intent raw = null;
                        int index = -1;
                        if ("startActivity".equals(method.getName())) {
                            for (int i = 0; i < args.length; i++) {
                                if(args[i] instanceof  Intent){
                                    raw = (Intent)args[i];
                                    index = i;
                                }
                            }
                            // 代替的Intent
                            Intent newIntent = new Intent();
                            newIntent.setComponent(new ComponentName("com.example.hook", StubActivity.class.getName()));
                            newIntent.putExtra(EXTRA_TARGET_INTENT,raw);
                            args[index] = newIntent;

                        }

                        return method.invoke(IActivityTaskManager, args);
                    }
                });
        mInstanceField.set(singleton, proxy);
    } catch (Exception e){
        e.printStackTrace();
    }
}

AMS 验证之前的 Hook,接下来是 AMS 验证之后的 Hook;

java 复制代码
public static void hookHandler() {
    try {
        // 这里要适配不同的系统版本,后面插件化的时候会具体讲解
        Class<?> atClass = Class.forName("android.app.ActivityThread");
        Field sCurrentActivityThreadField = atClass.getDeclaredField("sCurrentActivityThread");
        sCurrentActivityThreadField.setAccessible(true);
        Object sCurrentActivityThread = sCurrentActivityThreadField.get(null);
        // ActivityThread 一个app进程 只有一个,获取它的mH
        Field mHField = atClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        final Handler mH = (Handler) mHField.get(sCurrentActivityThread);

        //获取mCallback
        Field mCallbackField = Handler.class.getDeclaredField("mCallback");
        mCallbackField.setAccessible(true);

        mCallbackField.set(mH, new Handler.Callback() {

            @Override
            public boolean handleMessage(Message msg) {
                switch (msg.what) {
                    case 100: {

                    }
                    break;
                    case 159: {
                        Object obj = msg.obj;
                        Log.i(TAG, "handleMessage: obj=" + obj);
                        try {
                            Field mActivityCallbacksField = obj.getClass().getDeclaredField("mActivityCallbacks");
                            mActivityCallbacksField.setAccessible(true);
                            List mActivityCallbacks = (List) mActivityCallbacksField.get(obj);
                                
                            if (mActivityCallbacks.size() > 0) {
                                Log.i(TAG, "handleMessage: size= " + mActivityCallbacks.size());
                                String className = "android.app.servertransaction.LaunchActivityItem";
                                if (mActivityCallbacks.get(0).getClass().getCanonicalName().equals(className)) {
                                    Object object = mActivityCallbacks.get(0);
                                    Field intentField = object.getClass().getDeclaredField("mIntent");
                                    intentField.setAccessible(true);
                                    Intent intent = (Intent) intentField.get(object);
                                    Intent targetIntent = intent.getParcelableExtra(EXTRA_TARGET_INTENT);
                                    intent.setComponent(targetIntent.getComponent());
                                }
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }

                    }
                    break;
                }
                mH.handleMessage(msg);
                return true;
            }
        });

    } catch (Exception e) {
        Log.e(TAG, "hookHandler: " + e.getMessage());
        e.printStackTrace();
    }
}

没有贴上不同系统版本的兼容处理,感兴趣的,可以自己查看不同版本的具体实现,从而针对性的进行修改适配;

下一章预告


PackageManagerService

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~~

相关推荐
zh_xuan41 分钟前
Android Looper源码阅读
android
坐吃山猪2 小时前
SpringBoot01-配置文件
java·开发语言
我叫汪枫3 小时前
《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
java·开发语言·nio
yaoxtao3 小时前
java.nio.file.InvalidPathException异常
java·linux·ubuntu
Swift社区4 小时前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT5 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy5 小时前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss7 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续7 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升