From a260e99090e74ca7ca3b5b9f7ef74e8cf3e93d43 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Wed, 11 Aug 2021 00:00:21 -0700 Subject: [PATCH] Support code injection on Android 12 --- native/jni/inject/entry.cpp | 8 +- native/jni/inject/hook.cpp | 158 +++++++++++++++++++++++++++++++---- native/jni/inject/inject.hpp | 8 +- native/jni/inject/memory.cpp | 3 +- native/jni/inject/ptrace.cpp | 9 +- native/jni/inject/utils.cpp | 47 +++++------ 6 files changed, 180 insertions(+), 53 deletions(-) diff --git a/native/jni/inject/entry.cpp b/native/jni/inject/entry.cpp index c6b45b596..acea4b51d 100644 --- a/native/jni/inject/entry.cpp +++ b/native/jni/inject/entry.cpp @@ -87,9 +87,8 @@ static void inject_cleanup_wait() { __attribute__((constructor)) static void inject_init() { - inject_logging(); - if (getenv(INJECT_ENV_2)) { + inject_logging(); LOGD("zygote: inject 2nd stage\n"); active_threads = 1; unsetenv(INJECT_ENV_2); @@ -97,6 +96,7 @@ static void inject_init() { // Get our own handle self_handle = dlopen(INJECT_LIB_2, RTLD_LAZY); dlclose(self_handle); + unlink(INJECT_LIB_2); hook_functions(); @@ -105,6 +105,7 @@ static void inject_init() { active_threads++; new_daemon_thread(&unload_first_stage); } else if (char *env = getenv(INJECT_ENV_1)) { + inject_logging(); LOGD("zygote: inject 1st stage\n"); if (env[0] == '1') @@ -117,6 +118,7 @@ static void inject_init() { cp_afc(INJECT_LIB_1, INJECT_LIB_2); dlopen(INJECT_LIB_2, RTLD_LAZY); + unlink(INJECT_LIB_1); unsetenv(INJECT_ENV_1); } } @@ -128,7 +130,7 @@ int app_process_main(int argc, char *argv[]) { return 1; int in = xopen(buf, O_RDONLY); - int out = xopen(INJECT_LIB_1, O_CREAT | O_WRONLY | O_TRUNC, 0777); + int out = xopen(INJECT_LIB_1, O_WRONLY | O_CREAT, 0644); sendfile(out, in, nullptr, INT_MAX); close(in); close(out); diff --git a/native/jni/inject/hook.cpp b/native/jni/inject/hook.cpp index 5031a4d30..3a1956e95 100644 --- a/native/jni/inject/hook.cpp +++ b/native/jni/inject/hook.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -64,13 +65,17 @@ struct HookContext { void *raw_args; }; }; +struct vtable_t; static vector> *xhook_list; static vector *jni_hook_list; static hash_map>> *jni_method_map; -static JavaVM *g_jvm; static HookContext *current_ctx; +static JavaVM *g_jvm; +static vtable_t *gAppRuntimeVTable; +static const JNINativeInterface *old_functions; +static JNINativeInterface *new_functions; #define DCL_HOOK_FUNC(ret, func, ...) \ static ret (*old_##func)(__VA_ARGS__); \ @@ -103,10 +108,8 @@ if (methods[i].name == #method##sv) { \ continue; \ } -DCL_HOOK_FUNC(int, jniRegisterNativeMethods, +static unique_ptr hookAndSaveJNIMethods( JNIEnv *env, const char *className, const JNINativeMethod *methods, int numMethods) { - LOGD("hook: jniRegisterNativeMethods %s", className); - if (g_jvm == nullptr) { // Save for later unhooking env->GetJavaVM(&g_jvm); @@ -129,7 +132,40 @@ DCL_HOOK_FUNC(int, jniRegisterNativeMethods, HOOK_JNI(nativeForkSystemServer); } } + return newMethods; +} +static jclass gClassRef; +static jmethodID class_getName; +static string get_class_name(JNIEnv *env, jclass clazz) { + if (!gClassRef) { + jclass cls = env->FindClass("java/lang/Class"); + gClassRef = (jclass) env->NewGlobalRef(cls); + env->DeleteLocalRef(cls); + class_getName = env->GetMethodID(gClassRef, "getName", "()Ljava/lang/String;"); + } + auto nameRef = (jstring) env->CallObjectMethod(clazz, class_getName); + const char *name = env->GetStringUTFChars(nameRef, nullptr); + string className(name); + env->ReleaseStringUTFChars(nameRef, name); + std::replace(className.begin(), className.end(), '.', '/'); + return className; +} + +// ----------------------------------------------------------------- + +static jint new_env_RegisterNatives( + JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint numMethods) { + auto className = get_class_name(env, clazz); + LOGD("hook: JNIEnv->RegisterNatives %s\n", className.data()); + auto newMethods = hookAndSaveJNIMethods(env, className.data(), methods, numMethods); + return old_functions->RegisterNatives(env, clazz, newMethods.get() ?: methods, numMethods); +} + +DCL_HOOK_FUNC(int, jniRegisterNativeMethods, + JNIEnv *env, const char *className, const JNINativeMethod *methods, int numMethods) { + LOGD("hook: jniRegisterNativeMethods %s\n", className); + auto newMethods = hookAndSaveJNIMethods(env, className, methods, numMethods); return old_jniRegisterNativeMethods(env, className, newMethods.get() ?: methods, numMethods); } @@ -148,11 +184,56 @@ DCL_HOOK_FUNC(int, selinux_android_setcontext, return old_selinux_android_setcontext(uid, isSystemServer, seinfo, pkgname); } -static int sigmask(int how, int signum) { - sigset_t set; - sigemptyset(&set); - sigaddset(&set, signum); - return sigprocmask(how, &set, nullptr); +// ----------------------------------------------------------------- + +// android::AndroidRuntime vtable layout +struct vtable_t { + void *rtti; + void *dtor; + void (*onVmCreated)(void *self, JNIEnv* env); + void (*onStarted)(void *self); + void (*onZygoteInit)(void *self); + void (*onExit)(void *self, int code); +}; + +// This method is a trampoline for hooking JNIEnv->RegisterNatives +DCL_HOOK_FUNC(void, onVmCreated, void *self, JNIEnv *env) { + LOGD("hook: AppRuntime::onVmCreated\n"); + + // Restore the virtual table, we no longer need it + auto *new_table = *reinterpret_cast(self); + *reinterpret_cast(self) = gAppRuntimeVTable; + delete new_table; + + // Replace the function table in JNIEnv to hook RegisterNatives + old_functions = env->functions; + new_functions = new JNINativeInterface(); + memcpy(new_functions, env->functions, sizeof(*new_functions)); + new_functions->RegisterNatives = new_env_RegisterNatives; + env->functions = new_functions; + old_onVmCreated(self, env); +} + +// This method is a trampoline for swizzling android::AppRuntime vtable +static bool swizzled = false; +DCL_HOOK_FUNC(void, setArgv0, void *self, const char *argv0, bool setProcName) { + if (swizzled) { + old_setArgv0(self, argv0, setProcName); + return; + } + + LOGD("hook: AndroidRuntime::setArgv0\n"); + + // Swizzle C++ vtable to hook virtual function + gAppRuntimeVTable = *reinterpret_cast(self); + old_onVmCreated = gAppRuntimeVTable->onVmCreated; + auto *new_table = new vtable_t(); + memcpy(new_table, gAppRuntimeVTable, sizeof(vtable_t)); + new_table->onVmCreated = &new_onVmCreated; + *reinterpret_cast(self) = new_table; + swizzled = true; + + old_setArgv0(self, argv0, setProcName); } // ----------------------------------------------------------------- @@ -181,6 +262,13 @@ static void nativeSpecializeAppProcess_post(HookContext *ctx, JNIEnv *env, jclas // ----------------------------------------------------------------- +static int sigmask(int how, int signum) { + sigset_t set; + sigemptyset(&set); + sigaddset(&set, signum); + return sigprocmask(how, &set, nullptr); +} + // Do our own fork before loading any 3rd party code // First block SIGCHLD, unblock after original fork is done #define PRE_FORK() \ @@ -210,6 +298,16 @@ static void nativeForkAndSpecialize_post(HookContext *ctx, JNIEnv *env, jclass c // ----------------------------------------------------------------- static void nativeForkSystemServer_pre(HookContext *ctx, JNIEnv *env, jclass clazz) { + if (env->functions == new_functions) { + // Restore JNIEnv + env->functions = old_functions; + if (gClassRef) { + env->DeleteGlobalRef(gClassRef); + gClassRef = nullptr; + class_getName = nullptr; + } + } + PRE_FORK(); LOGD("hook: %s\n", __FUNCTION__); } @@ -245,22 +343,50 @@ static int hook_register(const char *path, const char *symbol, void *new_func, v return 0; } +template +static inline void default_new(T *&p) { p = new T(); } + +#define XHOOK_REGISTER_SYM(PATH_REGEX, SYM, NAME) \ + hook_register(PATH_REGEX, SYM, (void*) new_##NAME, (void **) &old_##NAME) + #define XHOOK_REGISTER(PATH_REGEX, NAME) \ - hook_register(PATH_REGEX, #NAME, (void*) new_##NAME, (void **) &old_##NAME) + XHOOK_REGISTER_SYM(PATH_REGEX, #NAME, NAME) + +#define ANDROID_RUNTIME ".*/libandroid_runtime.so$" +#define APP_PROCESS "^/system/bin/app_process.*" void hook_functions() { #ifdef MAGISK_DEBUG xhook_enable_debug(1); xhook_enable_sigsegv_protection(0); #endif - xhook_list = new remove_pointer_t(); - jni_hook_list = new remove_pointer_t(); - jni_method_map = new remove_pointer_t(); + default_new(xhook_list); + default_new(jni_hook_list); + default_new(jni_method_map); - XHOOK_REGISTER(".*\\libandroid_runtime.so$", jniRegisterNativeMethods); - XHOOK_REGISTER(".*\\libandroid_runtime.so$", fork); - XHOOK_REGISTER(".*\\libandroid_runtime.so$", selinux_android_setcontext); + XHOOK_REGISTER(ANDROID_RUNTIME, fork); + XHOOK_REGISTER(ANDROID_RUNTIME, selinux_android_setcontext); + XHOOK_REGISTER(ANDROID_RUNTIME, jniRegisterNativeMethods); hook_refresh(); + + // Remove unhooked methods + xhook_list->erase( + std::remove_if(xhook_list->begin(), xhook_list->end(), + [](auto &t) { return *std::get<2>(t) == nullptr;}), + xhook_list->end()); + + if (old_jniRegisterNativeMethods == nullptr) { + LOGD("hook: jniRegisterNativeMethods not used\n"); + + // android::AndroidRuntime::setArgv0(const char *, bool) + XHOOK_REGISTER_SYM(APP_PROCESS, "_ZN7android14AndroidRuntime8setArgv0EPKcb", setArgv0); + hook_refresh(); + + // We still need old_jniRegisterNativeMethods as other code uses it + // android::AndroidRuntime::registerNativeMethods(_JNIEnv*, const char *, const JNINativeMethod *, int) + constexpr char sig[] = "_ZN7android14AndroidRuntime21registerNativeMethodsEP7_JNIEnvPKcPK15JNINativeMethodi"; + *(void **) &old_jniRegisterNativeMethods = dlsym(RTLD_DEFAULT, sig); + } } bool unhook_functions() { diff --git a/native/jni/inject/inject.hpp b/native/jni/inject/inject.hpp index 8da418312..01663e648 100644 --- a/native/jni/inject/inject.hpp +++ b/native/jni/inject/inject.hpp @@ -11,11 +11,11 @@ // Unmap all pages matching the name void unmap_all(const char *name); -// Get library name and base address that contains the function -uintptr_t get_function_lib(uintptr_t addr, char *lib); +// Get library name + offset (from start of ELF), given function address +uintptr_t get_function_off(int pid, uintptr_t addr, char *lib); -// Get library base address with name -uintptr_t get_remote_lib(int pid, const char *lib); +// Get function address, given library name + offset +uintptr_t get_function_addr(int pid, const char *lib, uintptr_t off); void self_unload(); void hook_functions(); diff --git a/native/jni/inject/memory.cpp b/native/jni/inject/memory.cpp index 7b77c996c..10b051ba8 100644 --- a/native/jni/inject/memory.cpp +++ b/native/jni/inject/memory.cpp @@ -5,7 +5,8 @@ namespace jni_hook { // We know our minimum alignment is WORD size (size of pointer) static constexpr size_t ALIGN = sizeof(long); -static constexpr size_t CAPACITY = (1 << 24); +// 4MB is more than enough +static constexpr size_t CAPACITY = (1 << 22); // No need to be thread safe as the initial mmap always happens on the main thread static uint8_t *_area = nullptr; diff --git a/native/jni/inject/ptrace.cpp b/native/jni/inject/ptrace.cpp index 2d0b84bc7..69124ca60 100644 --- a/native/jni/inject/ptrace.cpp +++ b/native/jni/inject/ptrace.cpp @@ -226,16 +226,15 @@ uintptr_t remote_call_abi(int pid, uintptr_t func_addr, int nargs, va_list va) { uintptr_t remote_call_vararg(int pid, uintptr_t addr, int nargs, ...) { char lib_name[4096]; - auto local = get_function_lib(addr, lib_name); - if (local == 0) + auto off = get_function_off(getpid(), addr, lib_name); + if (off == 0) return 0; - auto remote = get_remote_lib(pid, lib_name); + auto remote = get_function_addr(pid, lib_name, off); if (remote == 0) return 0; - addr = addr - local + remote; va_list va; va_start(va, nargs); - auto result = remote_call_abi(pid, addr, nargs, va); + auto result = remote_call_abi(pid, remote, nargs, va); va_end(va); return result; } diff --git a/native/jni/inject/utils.cpp b/native/jni/inject/utils.cpp index c6522c7a1..c9898ad02 100644 --- a/native/jni/inject/utils.cpp +++ b/native/jni/inject/utils.cpp @@ -1,3 +1,4 @@ +#include #include #include "inject.hpp" @@ -9,10 +10,11 @@ namespace { struct map_info { uintptr_t start; uintptr_t end; + uintptr_t off; int perms; char *path; - map_info() : start(0), end(0), perms(0), path(nullptr) {} + map_info() : start(0), end(0), off(0), perms(0), path(nullptr) {} enum { EXEC = (1 << 0), @@ -30,27 +32,24 @@ static void parse_maps(int pid, Func fn) { // format: start-end perms offset dev inode path sprintf(file, "/proc/%d/maps", pid); file_readline(true, file, [=](string_view l) -> bool { - char *pos = (char *) l.data(); + char *line = (char *) l.data(); map_info info; + char perm[5]; + int path_off; - // Parse address hex strings - info.start = strtoul(pos, &pos, 16); - info.end = strtoul(++pos, &pos, 16); + if (sscanf(line, "%" PRIxPTR "-%" PRIxPTR " %4s %" PRIxPTR " %*x:%*x %*d %n%*s", + &info.start, &info.end, perm, &info.off, &path_off) != 4) + return true; // Parse permissions - if (*(++pos) != '-') + if (perm[0] != '-') info.perms |= map_info::READ; - if (*(++pos) != '-') + if (perm[1] != '-') info.perms |= map_info::WRITE; - if (*(++pos) != '-') + if (perm[2] != '-') info.perms |= map_info::EXEC; - pos += 3; - // Skip everything except path - int path_off; - sscanf(pos, "%*s %*s %*s %n%*s", &path_off); - pos += path_off; - info.path = pos; + info.path = line + path_off; return fn(info); }); @@ -74,28 +73,28 @@ void unmap_all(const char *name) { } } -uintptr_t get_function_lib(uintptr_t addr, char *lib) { - uintptr_t base = 0; - parse_maps(getpid(), [=, &base](map_info &info) -> bool { +uintptr_t get_function_off(int pid, uintptr_t addr, char *lib) { + uintptr_t off = 0; + parse_maps(pid, [=, &off](map_info &info) -> bool { if (addr >= info.start && addr < info.end) { if (lib) strcpy(lib, info.path); - base = info.start; + off = addr - info.start + info.off; return false; } return true; }); - return base; + return off; } -uintptr_t get_remote_lib(int pid, const char *lib) { - uintptr_t base = 0; - parse_maps(pid, [=, &base](map_info &info) -> bool { +uintptr_t get_function_addr(int pid, const char *lib, uintptr_t off) { + uintptr_t addr = 0; + parse_maps(pid, [=, &addr](map_info &info) -> bool { if (strcmp(info.path, lib) == 0 && (info.perms & map_info::EXEC)) { - base = info.start; + addr = info.start - info.off + off; return false; } return true; }); - return base; + return addr; }