前言 在过完了四大组件的容器内实现后,最后我们打算着重看看之前只是模糊提了几句的重定向问题,看看容器是如何把相关的资源和路径进行重定向的。
路径重定向和 Xposed 注入时机分析 在之前分析 Activity 时有这么一行代码用来支持路径的重定向:
1 IOCore.get().enableRedirect(packageContext);
代码注释中可以看到,应用本身的资源其实已经通过 Application 完成重定向了,但是仍有一些通过硬编码路径来访问文件的情况,为了避免这类访问引发崩溃,因此需要把它们重定向到新路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public void enableRedirect (Context context) { Map<String, String> rule = new LinkedHashMap <>(); String packageName = context.getPackageName(); try { ApplicationInfo packageInfo = SandBoxCore.getBPackageManager() .getApplicationInfo( packageName, PackageManager.GET_META_DATA, BActivityThread.getUserId()); int systemUserId = SandBoxCore.getHostUserId(); rule.put(String.format("/data/data/%s/lib" , packageName), packageInfo.nativeLibraryDir); rule.put( String.format("/data/user/%d/%s/lib" , systemUserId, packageName), packageInfo.nativeLibraryDir); rule.put(String.format("/data/data/%s" , packageName), packageInfo.dataDir); rule.put(String.format("/data/user/%d/%s" , systemUserId, packageName), packageInfo.dataDir); if (SandBoxCore.getContext().getExternalCacheDir() != null && context.getExternalCacheDir() != null ) { File external = BEnvironment.getExternalUserDir(BActivityThread.getUserId()); File sdcardAndroidFile = new File ("/sdcard/Android" ); String androidDir = String.format("/storage/emulated/%d/Android" , systemUserId); if (!sdcardAndroidFile.exists()) { sdcardAndroidFile = new File (androidDir); } if (sdcardAndroidFile.exists()) { File[] childDirs = sdcardAndroidFile.listFiles(pathname -> pathname.isDirectory()); if (childDirs != null ) { for (File childDir : childDirs) { Log.d(TAG, childDir.getAbsolutePath()); rule.put( "/sdcard/Android/" + childDir.getName(), external.getAbsolutePath() + "/Android/" + childDir.getName()); rule.put( androidDir + "/" + childDir.getName(), external.getAbsolutePath() + "/Android/" + childDir.getName()); } } else { rule.put("/sdcard/Android" , external.getAbsolutePath() + "/Android" ); rule.put(androidDir, external.getAbsolutePath() + "/Android" ); } } else { rule.put("/sdcard/Android" , external.getAbsolutePath()); rule.put(androidDir, external.getAbsolutePath()); } } if (SandBoxCore.get().isHideRoot()) { hideRoot(rule); } proc(rule); } catch (Exception e) { e.printStackTrace(); } for (String key : rule.keySet()) { get().addRedirect(key, rule.get(key)); } NativeCore.enableIO(); }
总的来说,先初始化了一些重定向规则,包括:
1 2 3 4 5 6 7 8 9 10 11 12 rule.put(String.format("/data/data/%s/lib" , packageName), packageInfo.nativeLibraryDir); rule.put( String.format("/data/user/%d/%s/lib" , systemUserId, packageName), packageInfo.nativeLibraryDir); rule.put(String.format("/data/data/%s" , packageName), packageInfo.dataDir); rule.put(String.format("/data/user/%d/%s" , systemUserId, packageName), packageInfo.dataDir); rule.put( "/sdcard/Android/" + childDir.getName(), external.getAbsolutePath() + "/Android/" + childDir.getName()); rule.put( androidDir + "/" + childDir.getName(), external.getAbsolutePath() + "/Android/" + childDir.getName());
除此之位还有对进程路径的重定向:
1 2 3 4 5 6 7 8 9 10 private void proc (Map<String, String> rule) { int appPid = BActivityThread.getAppPid(); int pid = Process.myPid(); String selfProc = "/proc/self/" ; String proc = "/proc/" + pid + "/" ; String cmdline = new File (BEnvironment.getProcDir(appPid), "cmdline" ).getAbsolutePath(); rule.put(proc + "cmdline" , cmdline); rule.put(selfProc + "cmdline" , cmdline); }
相当于把对这些路径的访问全部过滤成后面的那个参数,然后再把它们注入到 Native 层去:
1 2 3 4 5 6 7 8 9 10 11 12 13 public void addRedirect (String origPath, String redirectPath) { if (TextUtils.isEmpty(origPath) || TextUtils.isEmpty(redirectPath) || mRedirectMap.get(origPath) != null ) return ; mTrieTree.add(origPath); mRedirectMap.put(origPath, redirectPath); File redirectFile = new File (redirectPath); if (!redirectFile.exists()) { FileUtils.mkdirs(redirectPath); } NativeCore.addIORule(origPath, redirectPath); }
最后激活一下这些规则:
1 2 3 4 5 6 7 8 9 10 11 12 void enableIO (JNIEnv *env, jclass clazz) { IO::init(env); nativeHook(env); }void nativeHook (JNIEnv *env) { BaseHook::init(env); UnixFileSystemHook::init(env); VMClassLoaderHook::init(env); BinderHook::init(env); }
可以看到,容器对三个对象做了重定向,分别是文件系统、ClassLoader 以及 Binder。
文件系统 Hook 我们从文件系统看起:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 void UnixFileSystemHook::init(JNIEnv *env) { const char *className = "java/io/UnixFileSystem" ; JniHook::HookJniFun(env, className, "canonicalize0" , "(Ljava/lang/String;)Ljava/lang/String;" , (void *) new_canonicalize0, (void **) (&orig_canonicalize0), false ); JniHook::HookJniFun(env, className, "getBooleanAttributes0" , "(Ljava/lang/String;)I" , (void *) new_getBooleanAttributes0, (void **) (&orig_getBooleanAttributes0), false ); JniHook::HookJniFun(env, className, "getLastModifiedTime0" , "(Ljava/io/File;)J" , (void *) new_getLastModifiedTime0, (void **) (&orig_getLastModifiedTime0), false ); JniHook::HookJniFun(env, className, "setPermission0" , "(Ljava/io/File;IZZ)Z" , (void *) new_setPermission0, (void **) (&orig_setPermission0), false ); JniHook::HookJniFun(env, className, "createFileExclusively0" , "(Ljava/lang/String;)Z" , (void *) new_createFileExclusively0, (void **) (&orig_createFileExclusively0), false ); JniHook::HookJniFun(env, className, "list0" , "(Ljava/io/File;)[Ljava/lang/String;" , (void *) new_list0, (void **) (&orig_list0), false ); JniHook::HookJniFun(env, className, "createDirectory0" , "(Ljava/io/File;)Z" , (void *) new_createDirectory0, (void **) (&orig_createDirectory0), false ); JniHook::HookJniFun(env, className, "setLastModifiedTime0" , "(Ljava/io/File;J)Z" , (void *) new_setLastModifiedTime0, (void **) (&orig_setLastModifiedTime0), false ); JniHook::HookJniFun(env, className, "setReadOnly0" , "(Ljava/io/File;)Z" , (void *) new_setReadOnly0, (void **) (&orig_setReadOnly0), false ); JniHook::HookJniFun(env, className, "getSpace0" , "(Ljava/io/File;I)J" , (void *) new_getSpace0, (void **) (&orig_getSpace0), false ); }
可以看到主要就是对这些函数做了 Hook,以及 Hook 的方式也很朴素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 void JniHook::HookJniFun (JNIEnv *env, const char *class_name, const char *method_name, const char *sign, void *new_fun, void **orig_fun, bool is_static) { if (HookEnv.art_method_native_offset == 0 ) { return ; } jclass clazz = env->FindClass (class_name); if (!clazz) { ALOGD ("findClass fail: %s %s" , class_name, method_name); env->ExceptionClear (); return ; } jmethodID method = nullptr ; if (is_static) { method = env->GetStaticMethodID (clazz, method_name, sign); } else { method = env->GetMethodID (clazz, method_name, sign); } if (!method) { env->ExceptionClear (); ALOGD ("get method id fail: %s %s" , class_name, method_name); return ; } JNINativeMethod gMethods[] = { {method_name, sign, (void *) new_fun}, }; auto artMethod = reinterpret_cast <uintptr_t *>(GetArtMethod (env, clazz, method)); if (!CheckFlags (artMethod)) { ALOGE ("check flags error. class:%s, method:%s" , class_name, method_name); return ; } *orig_fun = reinterpret_cast <void *>(artMethod[HookEnv.art_method_native_offset]); if (env->RegisterNatives (clazz, gMethods, 1 ) < 0 ) { ALOGE ("jni hook error. class:%s, method:%s" , class_name, method_name); return ; } if (HookEnv.api_level == __ANDROID_API_O__ || HookEnv.api_level == __ANDROID_API_O_MR1__) { AddAccessFlag ((char *) artMethod, kAccFastNative); } ALOGD ("register class:%s, method:%s success!" , class_name, method_name); }
先用 GetStaticMethodID 或 GetMethodID 来获取原始函数的位置,然后把它回填到 orig_fun 中,最后调用 RegisterNatives 用新函数注册进去,替换结束。
而新函数基本上都是这样实现的:
1 2 3 4 HOOK_JNI (jboolean, getSpace0, JNIEnv *env, jobject obj, jobject file, jint t) { jobject redirect = IO::redirectPath (env, file); return orig_getSpace0 (env, obj, redirect, t); }
先过滤一遍路径,然后调用原函数。过滤流程最终会调用这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 const char *IO::redirectPath(const char *__path) { list<IO::RelocateInfo>::iterator iterator; for (iterator = relocate_rule.begin(); iterator != relocate_rule.end(); ++iterator) { IO::RelocateInfo info = *iterator; if (strstr(__path, info.targetPath) && !strstr(__path, "/sandbox/" )) { char *ret = replace(__path, info.targetPath, info.relocatePath); ALOGD("redirectPath %s => %s" , __path, ret); return ret; } } return __path; }
就是遍历一下找到合适的规则然后根据规则做替换,没啥别的内容了。不过这里有一个疑问,看起来程序并没有对 open、stat 之类的 libc 函数做 Hook,那如果后续有硬编码写死去调用 open 的情况是不是就会出错呢?
同样的,除此之外还有很多各种各样的函数被调用时参数中都会附带路径,这些函数不需要一起过滤吗?以及同时过滤这么多函数也很容易被检查发现也是一个问题。或许后续需要完善。
ClassLoader Hook 1 2 3 4 5 6 void VMClassLoaderHook::init(JNIEnv *env) { const char *className = "java/lang/VMClassLoader" ; JniHook::HookJniFun(env, className, "findLoadedClass" , "(Ljava/lang/ClassLoader;Ljava/lang/String;)Ljava/lang/Class;" , (void *) new_findLoadedClass, (void **) (&orig_findLoadedClass), true ); }
挺朴素的,跟前文的方式是一样的,只是目标改成了 findLoadedClass
函数。
新函数的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 HOOK_JNI(jobject, findLoadedClass, JNIEnv *env, jobject obj, jobject class_loader, jstring name) { const char * nameC = env->GetStringUTFChars(name, JNI_FALSE); if (hideXposedClass) { if (strstr(nameC, "de/robv/android/xposed/" ) || strstr(nameC, "me/weishu/epic" ) || strstr(nameC, "me/weishu/exposed" ) || strstr(nameC, "de.robv.android" ) || strstr(nameC, "me.weishu.epic" ) || strstr(nameC, "me.weishu.exposed" )) { env->ReleaseStringUTFChars(name, nameC); return nullptr; } } jobject result = orig_findLoadedClass(env, obj, class_loader, name); env->ReleaseStringUTFChars(name, nameC); return result; }
可以看到其实是在做一些过滤,把一些不希望被发现的 Class 过滤掉。
Binder Hook 同上,这次的目标是 getCallingUid 函数:
1 2 3 4 5 void BinderHook::init (JNIEnv *env) { const char *clazz = "android/os/Binder" ; JniHook::HookJniFun (env, clazz, "getCallingUid" , "()I" , (void *) new_getCallingUid, (void **) (&orig_getCallingUid), true ); }
最终会先调用一遍原生函数,然后再用下面这个函数做处理:
1 2 3 4 5 6 7 8 9 10 11 12 @Keep public static int getCallingUid (int origCallingUid) { if (origCallingUid > 0 && origCallingUid < Process.FIRST_APPLICATION_UID) return origCallingUid; if (origCallingUid > Process.LAST_APPLICATION_UID) return origCallingUid; if (origCallingUid == SandBoxCore.getHostUid ()) { return BActivityThread.getCallingBUid (); } return origCallingUid; }
如果是系统 UID 或不是用户应用的话就直接返回原本的值,而如果返回的结果是容器的 Uid ,那就要过滤一下替换成容器内目标应用的 Uid 了:
1 2 3 public static int getCallingBUid () { return getAppConfig() == null ? SandBoxCore.getHostUid() : getAppConfig().callingBUid; }
seccomp Hook 除此之位,Blackbox 还基于 seccomp 实现了对系统调用的 Hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void init_seccomp (JNIEnv *env, jclass clazz) { struct sock_filter filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 0 , 2 ), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[4 ])), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SECMAGIC, 0 , 1 ), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRAP) }; struct sock_fprog prog; prog.filter = filter; prog.len = (unsigned short ) (sizeof(filter) / sizeof(filter[0 ])); struct sigaction sa; sigset_t sigset; sigfillset(&sigset); sa.sa_sigaction = sig_callback; sa.sa_mask = sigset; sa.sa_flags = SA_SIGINFO; if (sigaction(SIGSYS, &sa, NULL) == -1 ) { return ; } if (prctl(PR_SET_NO_NEW_PRIVS, 1 , 0 , 0 , 0 ) == -1 ) { return ; } if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1 ) { return ; } ALOGE("InitCvmSeccomp Successes" ); }
触发上述的过滤条件时,会通过信号来回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 void sig_callback (int signo, siginfo_t *info, void *data) { int my_signo = info->si_signo; unsigned long syscall_number; unsigned long SYSARG_1, SYSARG_2, SYSARG_3, SYSARG_4, SYSARG_5, SYSARG_6; #if defined (__aarch64__) syscall_number = ((ucontext_t *) data)->uc_mcontext.regs[8 ]; SYSARG_1 = ((ucontext_t *) data)->uc_mcontext.regs[0 ]; SYSARG_2 = ((ucontext_t *) data)->uc_mcontext.regs[1 ]; SYSARG_3 = ((ucontext_t *) data)->uc_mcontext.regs[2 ]; SYSARG_4 = ((ucontext_t *) data)->uc_mcontext.regs[3 ]; SYSARG_5 = ((ucontext_t *) data)->uc_mcontext.regs[4 ]; SYSARG_6 = ((ucontext_t *) data)->uc_mcontext.regs[5 ];#elif defined (__arm__) syscall_number = ((ucontext_t *) data)->uc_mcontext.arm_r7; SYSARG_1 = ((ucontext_t *) data)->uc_mcontext.arm_r0; SYSARG_2 = ((ucontext_t *) data)->uc_mcontext.arm_r1; SYSARG_3 = ((ucontext_t *) data)->uc_mcontext.arm_r2; SYSARG_4 = ((ucontext_t *) data)->uc_mcontext.arm_r3; SYSARG_5 = ((ucontext_t *) data)->uc_mcontext.arm_r4; SYSARG_6 = ((ucontext_t *) data)->uc_mcontext.arm_r5; #else #error "Unsupported architecture" #endif switch (syscall_number) { case __NR_openat:{ int fd = (int ) SYSARG_1; const char *pathname = (const char *) SYSARG_2; int flags = (int ) SYSARG_3; int mode = (int ) SYSARG_4; ALOGE("测试%s" ,pathname); #if defined (__aarch64__) ((ucontext_t *) data)->uc_mcontext.regs[0 ] = (uint64_t)fd; ((ucontext_t *) data)->uc_mcontext.regs[1 ] = (uint64_t)pathname; ((ucontext_t *) data)->uc_mcontext.regs[2 ] = (uint64_t)flags; ((ucontext_t *) data)->uc_mcontext.regs[3 ] = (uint64_t)mode;#elif defined (__arm__) ((ucontext_t *) data)->uc_mcontext.arm_r0 = (uint32_t)fd; ((ucontext_t *) data)->uc_mcontext.arm_r1 = (uint32_t)pathname; ((ucontext_t *) data)->uc_mcontext.arm_r2 = (uint32_t)flags; ((ucontext_t *) data)->uc_mcontext.arm_r3 = (uint32_t)mode; #endif #if defined (__aarch64__) ((ucontext_t *) data)->uc_mcontext.regs[0 ] = OriSyscall(__NR_openat, fd, (uint64_t)pathname, flags, mode, SECMAGIC, SECMAGIC); #elif defined (__arm__) ((ucontext_t *) data)->uc_mcontext.arm_r0 = OriSyscall(__NR_openat, fd, (uint32_t)pathname, flags, mode, SECMAGIC, SECMAGIC); #endif break ; } default : break ; } }
可以看到,在这类对 openat 的系统调用做了拦截,不过目前还并没有做什么过滤,未来应该会有更好的支持。
Xposed 支持 算是最后一个笔者好奇的点。笔者之前并没有使用过 Xposed 的经历,因此对相关的内容其实不是很了解。通过网上的一些资料了解到,Xposed 是通过修改 app_process 的方式向进程里注入代码实现的,而这些操作需要它能够对 zygote 实现 Hook,那么对于 VirtualApp 来说要如何支持呢?
入口从创建应用时的 newApplication
开始:
1 2 3 4 5 6 7 8 9 10 11 12 @Override public Application newApplication (ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { ContextCompat.fix(context); fixSharedLibraryLoaders(cl); BActivityThread.currentActivityThread().loadXposed(context); delegateAppClassLoader = context.getClassLoader(); if (RomUtil.isHuawei()) { hookHwEnvironment$UserEnvironment(); } return super .newApplication(cl, className, context); }
因为之前创建启动 Activity
时已经用 AppInstrumentation
替换掉了 Instrumentation
,而在正常的创建流程中会调用该对象的 newApplication
函数,而 Xposed 的注入就发生在这个时机。这个时候应用对应的进程已经创建好了,后续就是等待绑定的流程了,而应用还尚且没有开始运行,因此算是合适的时机之一。
可以看到代码中在此刻加载了 Xposed,然后再带调用原生的 newApplication
恢复创建流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public void loadXposed (Context context) { String vPackageName = getAppPackageName(); String vProcessName = getAppProcessName(); if (!TextUtils.isEmpty(vPackageName) && !TextUtils.isEmpty(vProcessName) && BXposedManager.get().isXPEnable()) { assert vPackageName != null ; assert vProcessName != null ; boolean isFirstApplication = vPackageName.equals(vProcessName); List<InstalledModule> installedModules = BXposedManager.get().getInstalledModules(); for (InstalledModule installedModule : installedModules) { if (!installedModule.enable) { continue ; } try { PineXposed.loadModule(new File (installedModule.getApplication().sourceDir)); } catch (Throwable e) { e.printStackTrace(); } } try { PineXposed.onPackageLoad( vPackageName, vProcessName, context.getApplicationInfo(), isFirstApplication, context.getClassLoader()); } catch (Throwable ignored) { } } if (SandBoxCore.get().isHideXposed()) { NativeCore.hideXposed(); } }
代码中遍历了容器内安装的所有模块,并通过 PineXposed.loadModule
来加载,这似乎是一个框架的内部实现,翻到了作者的原文笔记。里面提到的说是直接套了 SandHook 的 API ,继续往下翻似乎就是 SandVXposed ,不过最后也没咋找到我想看的,于是只好自己翻起其他的资料对照着源码试着啃啃看了。
首先是加载 Xposed 的相关代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 final String filename = "assets/xposed_init" ; if (mcl instanceof ModuleClassLoader) { URL url = ((ModuleClassLoader) mcl).findResource(filename); initIs = url != null ? url.openStream() : null ; } else { initIs = mcl.getResourceAsStream(filename); } if (initIs == null ) { Log.e(TAG, " Failed to load module " + modulePath); Log.e(TAG, " assets/xposed_init not found in the module APK" ); return ; } } catch (IOException e) { Log.e(TAG, " Failed to load module " + modulePath); Log.e(TAG, " Cannot open assets/xposed_init in the module APK" , e); return ; } BufferedReader xposedInitReader = new BufferedReader (new InputStreamReader (initIs));
然后从资源里读取代码然后加载:
1 Class<?> c = mcl.loadClass(className);
然后下面有一个初始化 Xposed 的操作:
1 ((IXposedHookZygoteInit) callback).initZygote(param);
起初还以为是要注入 Zygote ,但是没有 root 也不能注入才对,翻了下源代码似乎是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 @Override public void initZygote (StartupParam startupParam) throws Throwable { hookXposedFrameworkApi(startupParam); } private void hookXposedFrameworkApi (StartupParam param) { hookXpFindAndHookMethod(); hookXpFindAndHookConstructor(); hookXpHookAllMethods(); hookXpHookAllConstructors(); }private void hookXpFindAndHookMethod () { try { Class cls = XposedHelpers.findClass( "de.robv.android.xposed.XposedHelpers" , HookUtils.class.getClassLoader()); XposedHelpers.findAndHookMethod(cls, "findAndHookMethod" , Class.class, String.class, Object[].class, new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { super .beforeHookedMethod(param); XposedBridge.log("findAndHookMethod: cls = " + param.args[0 ] + ", method = " + param.args[1 ]); } }); } catch (Exception e) { XposedBridge.log(e); } } private void hookXpFindAndHookConstructor () { try { Class cls = XposedHelpers.findClass( "de.robv.android.xposed.XposedHelpers" , HookUtils.class.getClassLoader()); XposedHelpers.findAndHookMethod(cls, "findAndHookConstructor" , Class.class, Object[].class, new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { super .beforeHookedMethod(param); XposedBridge.log("findAndHookConstructor: cls = " + param.args[0 ]); } }); } catch (Exception e) { XposedBridge.log(e); } } private void hookXpHookAllMethods () { try { Class cls = XposedHelpers.findClass( "de.robv.android.xposed.XposedBridge" , HookUtils.class.getClassLoader()); Class clsXC_MethodHook = XposedHelpers.findClass( "de.robv.android.xposed.XC_MethodHook" , HookUtils.class.getClassLoader() ); XposedHelpers.findAndHookMethod(cls, "hookAllMethods" , Class.class, String.class, clsXC_MethodHook, new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { super .beforeHookedMethod(param); XposedBridge.log("hookAllMethods: cls = " + param.args[0 ] + ", method = " + param.args[1 ]); } }); } catch (Exception e) { XposedBridge.log(e); } } private void hookXpHookAllConstructors () { try { Class cls = XposedHelpers.findClass( "de.robv.android.xposed.XposedBridge" , HookUtils.class.getClassLoader()); Class clsXC_MethodHook = XposedHelpers.findClass( "de.robv.android.xposed.XC_MethodHook" , HookUtils.class.getClassLoader() ); XposedHelpers.findAndHookMethod(cls, "hookAllConstructors" , Class.class, clsXC_MethodHook, new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { super .beforeHookedMethod(param); XposedBridge.log("hookAllConstructors: cls = " + param.args[0 ]); } }); } catch (Exception e) { XposedBridge.log(e); } }
看起来似乎不是注入 Zygote ,只是对其中的一些方法做了 Hook,既然如此就不需要真的去注入 Zygote 了,只需要在加载应用时把调一下这个方法把它们拦截下来就行了。
完成 Hook 以后再加载对应模块:
1 2 3 4 5 hookLoadPackage((IXposedHookLoadPackage) callback);public static void hookLoadPackage (IXposedHookLoadPackage callback) { sLoadedPackageCallbacks.add(new XC_LoadPackage .Wrapper(callback)); }
这个 sLoadedPackageCallbacks
只是暂存。在对每个模块做完上述操作以后,下面还有一段:
1 2 3 4 5 6 7 8 9 try { PineXposed.onPackageLoad( vPackageName, vProcessName, context.getApplicationInfo(), isFirstApplication, context.getClassLoader()); } catch (Throwable ignored) { }
往下跟一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static void onPackageLoad ( String packageName, String processName, ApplicationInfo appInfo, boolean isFirstApp, ClassLoader classLoader) { XC_LoadPackage.LoadPackageParam param = new XC_LoadPackage .LoadPackageParam(sLoadedPackageCallbacks); param.packageName = packageName; param.processName = processName; param.appInfo = appInfo; param.isFirstApplication = isFirstApp; param.classLoader = classLoader; XC_LoadPackage.callAll(param); } public static void callAll (Param param) { if (param.callbacks == null ) throw new IllegalStateException ("This object was not created for use with callAll" ); for (int i = 0 ; i < param.callbacks.length; i++) { try { ((XCallback) param.callbacks[i]).call(param); } catch (Throwable t) { XposedBridge.log(t); } } }
这里最后会去调用 XC_LoadPackage
下的方法:
1 2 3 4 5 @Override protected void call (Param param) throws Throwable { if (param instanceof LoadPackageParam) handleLoadPackage((LoadPackageParam) param); }
最后这个 handleLoadPackage
方法就会调用到模块里那个重载后的 handleLoadPackage
来实现模块功能的加载了。
不过这里有一点很奇怪的是,为什么需要每个模块都调用一遍 initZygote
呢?岂不是在重复 Hook 那些方法么?看起来似乎每次都把模块信息传进去了,但是好像没有用到这个信息,那又为什么需要每次都调用一遍呢?
没想明白,如果有大佬知道的话还请多多指教。
重定向 Hook 总结和疑问 在最后一部分我们可以看到,如果通过 Seccomp 对 open 等系统调用做拦截的话,应该上层的很多重定向都可以放掉了?毕竟不管上层用了什么函数,底层对文件的访问都是需要系统调用去支持的,从这个层面说,只要在系统调用层面把路径全部做好过滤,是不是上层的重定向可以直接删掉?
不过 Seccomp 这一策略首先支持的版本比较新,老设备肯定是没有的,以及另一方面是,检测 Seccomp 是不是较为简单呢?笔者目前对这些问题尚没有答案,还需要请教各位大佬。
参考文章 https://blog.canyie.top/2020/04/27/dynamic-hooking-framework-on-art/ https://wufengxue.github.io/2019/11/01/get-3rd-xp-module-hookers.html https://blog.canyie.top/2020/02/03/a-new-xposed-style-framework/