From a80cadf5870968275e3b2126b67740e9a669c33f Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Tue, 7 Nov 2023 23:34:33 -0800 Subject: [PATCH] Refactor hookJniNativeMethods Utilize NativeBridgeRuntimeCallbacks we obtained from native bridge to directly fetch and modify registered native JNI methods. By doing so, we do not need to keep a copy of every single JNINativeMethod registered in order to provide JNI hooking functionality. Co-authored-by: LoveSy --- .gitmodules | 3 - native/src/Android.mk | 2 - native/src/external/Android.mk | 6 -- native/src/external/parallel-hashmap | 1 - native/src/zygisk/gen_jni_hooks.py | 46 +-------- native/src/zygisk/hook.cpp | 145 +++++++++++++-------------- native/src/zygisk/jni_hooks.hpp | 66 ++++-------- native/src/zygisk/memory.cpp | 32 ------ native/src/zygisk/memory.hpp | 44 -------- 9 files changed, 87 insertions(+), 258 deletions(-) delete mode 160000 native/src/external/parallel-hashmap delete mode 100644 native/src/zygisk/memory.cpp delete mode 100644 native/src/zygisk/memory.hpp diff --git a/.gitmodules b/.gitmodules index 191ffd656..4473536d4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,9 +22,6 @@ [submodule "zlib"] path = native/src/external/zlib url = https://android.googlesource.com/platform/external/zlib -[submodule "parallel-hashmap"] - path = native/src/external/parallel-hashmap - url = https://github.com/greg7mdp/parallel-hashmap.git [submodule "zopfli"] path = native/src/external/zopfli url = https://github.com/google/zopfli.git diff --git a/native/src/Android.mk b/native/src/Android.mk index bee42029e..4c58b2c02 100644 --- a/native/src/Android.mk +++ b/native/src/Android.mk @@ -11,7 +11,6 @@ LOCAL_MODULE := magisk LOCAL_STATIC_LIBRARIES := \ libbase \ libsystemproperties \ - libphmap \ liblsplt \ libmagisk-rs @@ -36,7 +35,6 @@ LOCAL_SRC_FILES := \ zygisk/entry.cpp \ zygisk/main.cpp \ zygisk/hook.cpp \ - zygisk/memory.cpp \ zygisk/native_bridge.cpp \ zygisk/deny/cli.cpp \ zygisk/deny/utils.cpp \ diff --git a/native/src/external/Android.mk b/native/src/external/Android.mk index 42cdb39ef..af88e758e 100644 --- a/native/src/external/Android.mk +++ b/native/src/external/Android.mk @@ -1,11 +1,5 @@ LOCAL_PATH := $(call my-dir) -# Header only library -include $(CLEAR_VARS) -LOCAL_MODULE:= libphmap -LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/parallel-hashmap -include $(BUILD_STATIC_LIBRARY) - # libxz.a include $(CLEAR_VARS) LOCAL_MODULE:= libxz diff --git a/native/src/external/parallel-hashmap b/native/src/external/parallel-hashmap deleted file mode 160000 index 7684faf18..000000000 --- a/native/src/external/parallel-hashmap +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7684faf186806e2c88554a78188c18185b21f127 diff --git a/native/src/zygisk/gen_jni_hooks.py b/native/src/zygisk/gen_jni_hooks.py index 7a759b4d5..97afec326 100755 --- a/native/src/zygisk/gen_jni_hooks.py +++ b/native/src/zygisk/gen_jni_hooks.py @@ -213,60 +213,21 @@ def gen_jni_def(clz, methods): decl += ind(1) + f'return {m.ret.value};' decl += ind(0) + '}' - decl += ind(0) + f'const JNINativeMethod {m.base_name()}_methods[] = {{' + decl += ind(0) + f'std::array {m.base_name()}_methods = {{' for m in methods: - decl += ind(1) + '{' + decl += ind(1) + 'JNINativeMethod {' decl += ind(2) + f'"{m.base_name()}",' decl += ind(2) + f'"{m.jni()}",' decl += ind(2) + f'(void *) &{m.name}' decl += ind(1) + '},' decl += ind(0) + '};' decl = ind(0) + f'void *{m.base_name()}_orig = nullptr;' + decl - decl += ind(0) + f'constexpr int {m.base_name()}_methods_num = std::size({m.base_name()}_methods);' decl += ind(0) hook_map[clz].append(m.base_name()) return decl -def gen_jni_hook(): - decl = '' - decl += ind(0) + 'static JNINativeMethod *hookAndSaveJNIMethods(const char *className, const JNINativeMethod *methods, int numMethods) {' - decl += ind(1) + 'JNINativeMethod *newMethods = nullptr;' - decl += ind(1) + 'int clz_id = -1;' - decl += ind(1) + 'int hook_cnt = 0;' - decl += ind(1) + 'do {' - - for index, (clz, methods) in enumerate(hook_map.items()): - decl += ind(2) + f'if (className == "{clz}"sv) {{' - decl += ind(3) + f'clz_id = {index};' - decl += ind(3) + f'hook_cnt = {len(methods)};' - decl += ind(3) + 'break;' - decl += ind(2) + '}' - - decl += ind(1) + '} while (false);' - - decl += ind(1) + 'if (hook_cnt) {' - decl += ind(2) + 'newMethods = new JNINativeMethod[numMethods];' - decl += ind(2) + 'memcpy(newMethods, methods, sizeof(JNINativeMethod) * numMethods);' - decl += ind(1) + '}' - - decl += ind(1) + 'auto &class_map = (*jni_method_map)[className];' - decl += ind(1) + 'for (int i = 0; i < numMethods; ++i) {' - - for index, methods in enumerate(hook_map.values()): - decl += ind(2) + f'if (hook_cnt && clz_id == {index}) {{' - for m in methods: - decl += ind(3) + f'HOOK_JNI({m})' - decl += ind(2) + '}' - - decl += ind(2) + 'class_map[methods[i].name][methods[i].signature] = methods[i].fnPtr;' - decl += ind(1) + '}' - - decl += ind(1) + 'return newMethods;' - decl += ind(0) + '}' - return decl - with open('jni_hooks.hpp', 'w') as f: f.write('// Generated by gen_jni_hooks.py\n') f.write('\nnamespace {\n') @@ -283,6 +244,3 @@ with open('jni_hooks.hpp', 'w') as f: f.write(gen_jni_def(zygote, methods)) f.write('\n} // namespace\n') - - f.write(gen_jni_hook()) - f.write('\n') diff --git a/native/src/zygisk/hook.cpp b/native/src/zygisk/hook.cpp index 5c559f066..5e9bfe90a 100644 --- a/native/src/zygisk/hook.cpp +++ b/native/src/zygisk/hook.cpp @@ -12,14 +12,10 @@ #include #include "zygisk.hpp" -#include "memory.hpp" #include "module.hpp" #include "deny/deny.hpp" using namespace std; -using jni_hook::hash_map; -using jni_hook::tree_map; -using xstring = jni_hook::string; // Extreme verbose logging #define ZLOGV(...) ZLOGD(__VA_ARGS__) @@ -28,7 +24,6 @@ using xstring = jni_hook::string; static void hook_unloader(); static void unhook_functions(); static void hook_jni_env(); -static void restore_jni_env(JNIEnv *env); static void reload_native_bridge(const string &nb); namespace { @@ -49,7 +44,6 @@ enum { // Global variables vector> *plt_hook_list; map, StringCmp> *jni_hook_list; -hash_map>> *jni_method_map; bool should_unmap_zygisk = false; // Current context @@ -97,14 +91,7 @@ struct HookContext { HookContext(JNIEnv *env, void *args) : env(env), args{args}, process(nullptr), pid(-1), info_flags(0), - hook_info_lock(PTHREAD_MUTEX_INITIALIZER) { - static bool restored_env = false; - if (!restored_env) { - restore_jni_env(env); - restored_env = true; - } - g_ctx = this; - } + hook_info_lock(PTHREAD_MUTEX_INITIALIZER) { g_ctx = this; } ~HookContext(); @@ -224,38 +211,41 @@ DCL_HOOK_FUNC(int, dlclose, void *handle) { // ----------------------------------------------------------------- void hookJniNativeMethods(JNIEnv *env, const char *clz, JNINativeMethod *methods, int numMethods) { - auto class_map = jni_method_map->find(clz); - if (class_map == jni_method_map->end()) { - for (int i = 0; i < numMethods; ++i) { + jclass clazz; + if (!runtime_callbacks || !env || !clz || !(clazz = env->FindClass(clz))) { + for (auto i = 0; i < numMethods; ++i) { methods[i].fnPtr = nullptr; } return; } - vector hooks; - for (int i = 0; i < numMethods; ++i) { - auto method_map = class_map->second.find(methods[i].name); - if (method_map != class_map->second.end()) { - auto it = method_map->second.find(methods[i].signature); - if (it != method_map->second.end()) { - // Copy the JNINativeMethod - hooks.push_back(methods[i]); - // Save the original function pointer - methods[i].fnPtr = it->second; - // Do not allow double hook, remove method from map - method_map->second.erase(it); - continue; + // Backup existing methods + auto total = runtime_callbacks->getNativeMethodCount(env, clazz); + vector old_methods(total); + runtime_callbacks->getNativeMethods(env, clazz, old_methods.data(), total); + + // Replace the method + for (auto i = 0; i < numMethods; ++i) { + auto &method = methods[i]; + auto res = env->RegisterNatives(clazz, &method, 1); + // It's normal that the method is not found + if (res == JNI_ERR || env->ExceptionCheck()) { + auto exception = env->ExceptionOccurred(); + if (exception) env->DeleteLocalRef(exception); + env->ExceptionClear(); + method.fnPtr = nullptr; + } else { + // Find the old function pointer and return to caller + for (const auto &old_method : old_methods) { + if (strcmp(method.name, old_method.name) == 0 && + strcmp(method.signature, old_method.signature) == 0) { + ZLOGD("replace %s#%s%s %p -> %p\n", clz, + method.name, method.signature, old_method.fnPtr, method.fnPtr); + method.fnPtr = old_method.fnPtr; + } } } - // No matching method found, set fnPtr to null - methods[i].fnPtr = nullptr; } - - if (hooks.empty()) - return; - - old_functions->RegisterNatives( - env, env->FindClass(clz), hooks.data(), static_cast(hooks.size())); } ZygiskModule::ZygiskModule(int id, void *handle, void *entry) @@ -643,12 +633,6 @@ HookContext::~HookContext() { delete jni_hook_list; jni_hook_list = nullptr; - // Do NOT directly call delete - operator delete(jni_method_map); - // Directly unmap the whole memory block - jni_hook::memory_block::release(); - jni_method_map = nullptr; - // Strip out all API function pointers for (auto &m : modules) { m.clearApi(); @@ -844,7 +828,6 @@ static void hook_commit() { void hook_functions() { default_new(plt_hook_list); default_new(jni_hook_list); - default_new(jni_method_map); ino_t android_runtime_inode = 0; dev_t android_runtime_dev = 0; @@ -910,7 +893,8 @@ static void unhook_functions() { // ----------------------------------------------------------------- -static JNINativeMethod *hookAndSaveJNIMethods(const char *, const JNINativeMethod *, int); +// JNI method hook definitions, auto generated +#include "jni_hooks.hpp" static string get_class_name(JNIEnv *env, jclass clazz) { static auto class_getName = env->GetMethodID( @@ -923,13 +907,46 @@ static string get_class_name(JNIEnv *env, jclass clazz) { return className; } +static void replace_jni_methods( + vector &methods, + JNINativeMethod *hook_methods, size_t hook_methods_size, + void **orig_function) { + for (auto &method : methods) { + if (strcmp(method.name, hook_methods[0].name) == 0) { + for (auto i = 0; i < hook_methods_size; ++i) { + const auto &hook = hook_methods[i]; + if (strcmp(method.signature, hook.signature) == 0) { + *orig_function = method.fnPtr; + method.fnPtr = hook.fnPtr; + ZLOGI("replace %s\n", method.name); + return; + } + } + ZLOGE("unknown signature of %s%s\n", method.name, method.signature); + } + } +} + +#define HOOK_JNI(method) \ +replace_jni_methods(newMethods, method##_methods.data(), method##_methods.size(), &method##_orig) + static jint env_RegisterNatives( JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint numMethods) { auto className = get_class_name(env, clazz); - ZLOGV("JNIEnv->RegisterNatives [%s]\n", className.data()); - auto newMethods = unique_ptr( - hookAndSaveJNIMethods(className.data(), methods, numMethods)); - return old_functions->RegisterNatives(env, clazz, newMethods.get() ?: methods, numMethods); + if (className == "com/android/internal/os/Zygote") { + // Restore JNIEnv as we no longer need to replace anything + env->functions = old_functions; + delete new_functions; + new_functions = nullptr; + + vector newMethods(methods, methods + numMethods); + HOOK_JNI(nativeForkAndSpecialize); + HOOK_JNI(nativeSpecializeAppProcess); + HOOK_JNI(nativeForkSystemServer); + return old_functions->RegisterNatives(env, clazz, newMethods.data(), numMethods); + } else { + return old_functions->RegisterNatives(env, clazz, methods, numMethods); + } } static void hook_jni_env() { @@ -975,31 +992,3 @@ static void hook_jni_env() { old_functions = env->functions; env->functions = new_functions; } - -static void restore_jni_env(JNIEnv *env) { - env->functions = old_functions; - delete new_functions; - new_functions = nullptr; -} - -#define HOOK_JNI(method) \ -if (methods[i].name == #method##sv) { \ - int j = 0; \ - for (; j < method##_methods_num; ++j) { \ - if (strcmp(methods[i].signature, method##_methods[j].signature) == 0) { \ - jni_hook_list->try_emplace(className).first->second.push_back(methods[i]); \ - method##_orig = methods[i].fnPtr; \ - newMethods[i] = method##_methods[j]; \ - ZLOGI("replaced %s#" #method "\n", className); \ - --hook_cnt; \ - break; \ - } \ - } \ - if (j == method##_methods_num) { \ - ZLOGE("unknown signature of %s#" #method ": %s\n", className, methods[i].signature); \ - } \ - continue; \ -} - -// JNI method hook definitions, auto generated -#include "jni_hooks.hpp" diff --git a/native/src/zygisk/jni_hooks.hpp b/native/src/zygisk/jni_hooks.hpp index 9b6208398..eeeba4f69 100644 --- a/native/src/zygisk/jni_hooks.hpp +++ b/native/src/zygisk/jni_hooks.hpp @@ -109,54 +109,53 @@ void *nativeForkAndSpecialize_orig = nullptr; ctx.nativeForkAndSpecialize_post(); return ctx.pid; } -const JNINativeMethod nativeForkAndSpecialize_methods[] = { - { +std::array nativeForkAndSpecialize_methods = { + JNINativeMethod { "nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;Ljava/lang/String;[ILjava/lang/String;Ljava/lang/String;)I", (void *) &nativeForkAndSpecialize_l }, - { + JNINativeMethod { "nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[ILjava/lang/String;Ljava/lang/String;)I", (void *) &nativeForkAndSpecialize_o }, - { + JNINativeMethod { "nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/String;)I", (void *) &nativeForkAndSpecialize_p }, - { + JNINativeMethod { "nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/String;Z)I", (void *) &nativeForkAndSpecialize_q_alt }, - { + JNINativeMethod { "nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/String;Z[Ljava/lang/String;[Ljava/lang/String;ZZ)I", (void *) &nativeForkAndSpecialize_r }, - { + JNINativeMethod { "nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;IILjava/lang/String;[ILjava/lang/String;Ljava/lang/String;)I", (void *) &nativeForkAndSpecialize_samsung_m }, - { + JNINativeMethod { "nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;IILjava/lang/String;[ILjava/lang/String;Ljava/lang/String;I)I", (void *) &nativeForkAndSpecialize_samsung_n }, - { + JNINativeMethod { "nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;IILjava/lang/String;[I[ILjava/lang/String;Ljava/lang/String;)I", (void *) &nativeForkAndSpecialize_samsung_o }, - { + JNINativeMethod { "nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;IILjava/lang/String;[I[IZLjava/lang/String;Ljava/lang/String;)I", (void *) &nativeForkAndSpecialize_samsung_p }, }; -constexpr int nativeForkAndSpecialize_methods_num = std::size(nativeForkAndSpecialize_methods); void *nativeSpecializeAppProcess_orig = nullptr; [[clang::no_stack_protector]] void nativeSpecializeAppProcess_q(JNIEnv *env, jclass clazz, jint uid, jint gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name, jboolean is_child_zygote, jstring instruction_set, jstring app_data_dir) { @@ -205,29 +204,28 @@ void *nativeSpecializeAppProcess_orig = nullptr; ); ctx.nativeSpecializeAppProcess_post(); } -const JNINativeMethod nativeSpecializeAppProcess_methods[] = { - { +std::array nativeSpecializeAppProcess_methods = { + JNINativeMethod { "nativeSpecializeAppProcess", "(II[II[[IILjava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;)V", (void *) &nativeSpecializeAppProcess_q }, - { + JNINativeMethod { "nativeSpecializeAppProcess", "(II[II[[IILjava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Z)V", (void *) &nativeSpecializeAppProcess_q_alt }, - { + JNINativeMethod { "nativeSpecializeAppProcess", "(II[II[[IILjava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Z[Ljava/lang/String;[Ljava/lang/String;ZZ)V", (void *) &nativeSpecializeAppProcess_r }, - { + JNINativeMethod { "nativeSpecializeAppProcess", "(II[II[[IILjava/lang/String;IILjava/lang/String;ZLjava/lang/String;Ljava/lang/String;)V", (void *) &nativeSpecializeAppProcess_samsung_q }, }; -constexpr int nativeSpecializeAppProcess_methods_num = std::size(nativeSpecializeAppProcess_methods); void *nativeForkSystemServer_orig = nullptr; [[clang::no_stack_protector]] jint nativeForkSystemServer_l(JNIEnv *env, jclass clazz, jint uid, jint gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jlong permitted_capabilities, jlong effective_capabilities) { @@ -250,45 +248,17 @@ void *nativeForkSystemServer_orig = nullptr; ctx.nativeForkSystemServer_post(); return ctx.pid; } -const JNINativeMethod nativeForkSystemServer_methods[] = { - { +std::array nativeForkSystemServer_methods = { + JNINativeMethod { "nativeForkSystemServer", "(II[II[[IJJ)I", (void *) &nativeForkSystemServer_l }, - { + JNINativeMethod { "nativeForkSystemServer", "(II[IIII[[IJJ)I", (void *) &nativeForkSystemServer_samsung_q }, }; -constexpr int nativeForkSystemServer_methods_num = std::size(nativeForkSystemServer_methods); } // namespace - -static JNINativeMethod *hookAndSaveJNIMethods(const char *className, const JNINativeMethod *methods, int numMethods) { - JNINativeMethod *newMethods = nullptr; - int clz_id = -1; - int hook_cnt = 0; - do { - if (className == "com/android/internal/os/Zygote"sv) { - clz_id = 0; - hook_cnt = 3; - break; - } - } while (false); - if (hook_cnt) { - newMethods = new JNINativeMethod[numMethods]; - memcpy(newMethods, methods, sizeof(JNINativeMethod) * numMethods); - } - auto &class_map = (*jni_method_map)[className]; - for (int i = 0; i < numMethods; ++i) { - if (hook_cnt && clz_id == 0) { - HOOK_JNI(nativeForkAndSpecialize) - HOOK_JNI(nativeSpecializeAppProcess) - HOOK_JNI(nativeForkSystemServer) - } - class_map[methods[i].name][methods[i].signature] = methods[i].fnPtr; - } - return newMethods; -} diff --git a/native/src/zygisk/memory.cpp b/native/src/zygisk/memory.cpp deleted file mode 100644 index 0b4158fcf..000000000 --- a/native/src/zygisk/memory.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include -#include "memory.hpp" - -namespace jni_hook { - -// We know our minimum alignment is WORD size (size of pointer) -static constexpr size_t ALIGN = sizeof(long); - -// 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; - -static std::atomic _curr = nullptr; - -void *memory_block::allocate(size_t sz) { - if (!_area) { - // Memory will not actually be allocated because physical pages are mapped in on-demand - _area = static_cast(xmmap( - nullptr, CAPACITY, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); - _curr = _area; - } - return _curr.fetch_add(align_to(sz, ALIGN)); -} - -void memory_block::release() { - if (_area) - munmap(_area, CAPACITY); -} - -} // namespace jni_hook diff --git a/native/src/zygisk/memory.hpp b/native/src/zygisk/memory.hpp deleted file mode 100644 index 3c2215148..000000000 --- a/native/src/zygisk/memory.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-builtins" -#include -#pragma clang diagnostic pop - -#include - -namespace jni_hook { - -struct memory_block { - static void *allocate(size_t sz); - static void deallocate(void *, size_t) { /* Monotonic increase */ } - static void release(); -}; - -template -using allocator = stateless_allocator; - -using string = std::basic_string, allocator>; - -// Use node_hash_map since it will use less memory because we are using a monotonic allocator -template -using hash_map = phmap::node_hash_map, - phmap::priv::hash_default_eq, - allocator> ->; - -template -using tree_map = std::map, - allocator> ->; - -} // namespace jni_hook - -// Provide heterogeneous lookup for jni_hook::string -namespace phmap::priv { -template <> struct HashEq : StringHashEqT {}; -} // namespace phmap::priv