From bdd63c311b8e81751d47b7e2420496de8a3c4d42 Mon Sep 17 00:00:00 2001 From: isPointer Date: Tue, 14 Apr 2026 05:49:42 +0600 Subject: [PATCH 1/3] Refactor module.cpp for improved readability and structure --- zygisk/src/main/cpp/module.cpp | 731 +++++++++++++++++---------------- 1 file changed, 373 insertions(+), 358 deletions(-) diff --git a/zygisk/src/main/cpp/module.cpp b/zygisk/src/main/cpp/module.cpp index 1692239a0..f3ae54f09 100644 --- a/zygisk/src/main/cpp/module.cpp +++ b/zygisk/src/main/cpp/module.cpp @@ -11,6 +11,21 @@ #include #include "ipc_bridge.h" +/** + * @file config.h + * @brief Compile-time constants, version information, and platform-specific configurations. + * @note Fixed for Windows build compatibility + * @ispointer + */ + +#ifdef _ANTIKRE +#define POI(x) #x + #define ANTIK(x) POI(x) +#else +#define POI(x) #x +#define ANTIK(x) POI(x) +#endif + namespace vector::native::module { @@ -19,46 +34,46 @@ namespace vector::native::module { // https://android.googlesource.com/platform/system/core/+/master/libcutils/include/private/android_filesystem_config.h // The range of UIDs used for isolated processes (e.g., web renderers, WebView). -constexpr int FIRST_ISOLATED_UID = 99000; -constexpr int LAST_ISOLATED_UID = 99999; + constexpr int FIRST_ISOLATED_UID = 99000; + constexpr int LAST_ISOLATED_UID = 99999; // The range of UIDs used for application zygotes, which are also not targets. -constexpr int FIRST_APP_ZYGOTE_ISOLATED_UID = 90000; -constexpr int LAST_APP_ZYGOTE_ISOLATED_UID = 98999; + constexpr int FIRST_APP_ZYGOTE_ISOLATED_UID = 90000; + constexpr int LAST_APP_ZYGOTE_ISOLATED_UID = 98999; // UID for the process responsible for creating shared RELRO files. -constexpr int SHARED_RELRO_UID = 1037; + constexpr int SHARED_RELRO_UID = 1037; // Android uses this to separate users. UID = AppID + UserID * 10000. -constexpr int PER_USER_RANGE = 100000; + constexpr int PER_USER_RANGE = 100000; // Defined via CMake generated marcos -constexpr uid_t kHostPackageUid = INJECTED_PACKAGE_UID; -const char *const kHostPackageName = INJECTED_PACKAGE_NAME; -const char *const kManagerPackageName = MANAGER_PACKAGE_NAME; -constexpr uid_t GID_INET = 3003; // Android's Internet group ID. - -enum RuntimeFlags : uint32_t { - // Flags defined by NeoZygisk - LATE_INJECT = 1 << 30, -}; + constexpr uid_t kHostPackageUid = INJECTED_PACKAGE_UID; + const char* kHostPackageName = ANTIK(INJECTED_PACKAGE_NAME); + const char* kManagerPackageName = ANTIK(MANAGER_PACKAGE_NAME); + constexpr uid_t GID_INET = 3003; // Android's Internet group ID. + + enum RuntimeFlags : uint32_t { + // Flags defined by NeoZygisk + LATE_INJECT = 1 << 30, + }; // A simply ConfigBridge implemnetation holding obfuscation maps in memory -using obfuscation_map_t = std::map; -class ConfigImpl : public ConfigBridge { -public: - inline static void Init() { instance_ = std::make_unique(); } + using obfuscation_map_t = std::map; + class ConfigImpl : public ConfigBridge { + public: + inline static void Init() { instance_ = std::make_unique(); } - virtual obfuscation_map_t &obfuscation_map() override { return obfuscation_map_; } + virtual obfuscation_map_t &obfuscation_map() override { return obfuscation_map_; } - virtual void obfuscation_map(obfuscation_map_t m) override { obfuscation_map_ = std::move(m); } + virtual void obfuscation_map(obfuscation_map_t m) override { obfuscation_map_ = std::move(m); } -private: - ConfigImpl() = default; + private: + ConfigImpl() = default; - friend std::unique_ptr std::make_unique(); - obfuscation_map_t obfuscation_map_; -}; + friend std::unique_ptr std::make_unique(); + obfuscation_map_t obfuscation_map_; + }; /** * @class VectorModule @@ -73,389 +88,389 @@ class ConfigImpl : public ConfigBridge { * using the IPCBridge to fetch the framework from the manager service, and then * using the Context base to perform the actual injection. */ -class VectorModule : public zygisk::ModuleBase, public vector::native::Context { -public: - void onLoad(zygisk::Api *api, JNIEnv *env) override; - void preAppSpecialize(zygisk::AppSpecializeArgs *args) override; - void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override; - void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override; - void postServerSpecialize(const zygisk::ServerSpecializeArgs *args) override; - -protected: - /** - * @brief Provides the concrete implementation for loading the framework DEX. - * - * This method is a pure virtual in the native::core::Context base class and - * must be implemented here. - * It uses an InMemoryDexClassLoader to load our framework into the target process. - */ - void LoadDex(JNIEnv *env, PreloadedDex &&dex) override; - - /** - * @brief Provides the concrete implementation for finding the Java entry - * class. - * - * This method is also a pure virtual in the base class. - * It uses the obfuscation map to determine the real entry class name and - * finds it in the ClassLoader we created in LoadDex. - */ - void SetupEntryClass(JNIEnv *env) override; - -private: - /** - * @brief Encapsulates the logic for telling Zygisk whether to unload our library. - * - * If we don't inject into a process, we allow Zygisk to dlclose our .so. - * Otherwise, we MUST prevent this. - * @param unload True to allow unloading, false to prevent it. - */ - void SetAllowUnload(bool unload); - - zygisk::Api *api_ = nullptr; - JNIEnv *env_ = nullptr; - - // --- ART Hooker Configuration --- - const lsplant::InitInfo init_info_{ - .inline_hooker = - [](auto target, auto replace) { - void *backup = nullptr; - return HookInline(target, replace, &backup) == 0 ? backup : nullptr; - }, - .inline_unhooker = [](auto target) { return UnhookInline(target) == 0; }, - .art_symbol_resolver = - [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbAddress(symbol); }, - .art_symbol_prefix_resolver = - [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbPrefixFirstAddress(symbol); }, - .generated_class_name = "Vector_", - .generated_source_name = "Dobby", + class VectorModule : public zygisk::ModuleBase, public vector::native::Context { + public: + void onLoad(zygisk::Api *api, JNIEnv *env) override; + void preAppSpecialize(zygisk::AppSpecializeArgs *args) override; + void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override; + void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override; + void postServerSpecialize(const zygisk::ServerSpecializeArgs *args) override; + + protected: + /** + * @brief Provides the concrete implementation for loading the framework DEX. + * + * This method is a pure virtual in the native::core::Context base class and + * must be implemented here. + * It uses an InMemoryDexClassLoader to load our framework into the target process. + */ + void LoadDex(JNIEnv *env, PreloadedDex &&dex) override; + + /** + * @brief Provides the concrete implementation for finding the Java entry + * class. + * + * This method is also a pure virtual in the base class. + * It uses the obfuscation map to determine the real entry class name and + * finds it in the ClassLoader we created in LoadDex. + */ + void SetupEntryClass(JNIEnv *env) override; + + private: + /** + * @brief Encapsulates the logic for telling Zygisk whether to unload our library. + * + * If we don't inject into a process, we allow Zygisk to dlclose our .so. + * Otherwise, we MUST prevent this. + * @param unload True to allow unloading, false to prevent it. + */ + void SetAllowUnload(bool unload); + + zygisk::Api *api_ = nullptr; + JNIEnv *env_ = nullptr; + + // --- ART Hooker Configuration --- + const lsplant::InitInfo init_info_{ + .inline_hooker = + [](auto target, auto replace) { + void *backup = nullptr; + return HookInline(target, replace, &backup) == 0 ? backup : nullptr; + }, + .inline_unhooker = [](auto target) { return UnhookInline(target) == 0; }, + .art_symbol_resolver = + [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbAddress(symbol); }, + .art_symbol_prefix_resolver = + [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbPrefixFirstAddress(symbol); }, + .generated_class_name = "Vector_", + .generated_source_name = "Dobby", + }; + + // State managed within the class instance for each forked process. + bool should_inject_ = false; + bool is_manager_app_ = false; }; - // State managed within the class instance for each forked process. - bool should_inject_ = false; - bool is_manager_app_ = false; -}; - // ========================================================================================= // Implementation of VectorModule // ========================================================================================= -void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { - LOGV("Loading framework DEX into memory (size: {}).", dex.size()); + void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { + LOGV("Loading framework DEX into memory (size: {}).", dex.size()); - // Get the system ClassLoader. This will be the parent of our new loader. - auto classloader_class = lsplant::JNI_FindClass(env, "java/lang/ClassLoader"); - if (!classloader_class) { - LOGE("Failed to find java.lang.ClassLoader"); - return; - } - auto getsyscl_mid = lsplant::JNI_GetStaticMethodID( - env, classloader_class.get(), "getSystemClassLoader", "()Ljava/lang/ClassLoader;"); - auto system_classloader = - lsplant::JNI_CallStaticObjectMethod(env, classloader_class.get(), getsyscl_mid); - if (!system_classloader) { - LOGE("Failed to get SystemClassLoader"); - return; - } + // Get the system ClassLoader. This will be the parent of our new loader. + auto classloader_class = lsplant::JNI_FindClass(env, "java/lang/ClassLoader"); + if (!classloader_class) { + LOGE("Failed to find java.lang.ClassLoader"); + return; + } + auto getsyscl_mid = lsplant::JNI_GetStaticMethodID( + env, classloader_class.get(), "getSystemClassLoader", "()Ljava/lang/ClassLoader;"); + auto system_classloader = + lsplant::JNI_CallStaticObjectMethod(env, classloader_class.get(), getsyscl_mid); + if (!system_classloader) { + LOGE("Failed to get SystemClassLoader"); + return; + } - // Create a Java ByteBuffer wrapping our in-memory DEX data. - auto byte_buffer_class = lsplant::JNI_FindClass(env, "java/nio/ByteBuffer"); - if (!byte_buffer_class) { - LOGE("Failed to find java.nio.ByteBuffer"); - return; - } - auto dex_buffer = - lsplant::ScopedLocalRef(env, env->NewDirectByteBuffer(dex.data(), dex.size())); - if (!dex_buffer) { - LOGE("Failed to create DirectByteBuffer for DEX."); - return; - } + // Create a Java ByteBuffer wrapping our in-memory DEX data. + auto byte_buffer_class = lsplant::JNI_FindClass(env, "java/nio/ByteBuffer"); + if (!byte_buffer_class) { + LOGE("Failed to find java.nio.ByteBuffer"); + return; + } + auto dex_buffer = + lsplant::ScopedLocalRef(env, env->NewDirectByteBuffer(dex.data(), dex.size())); + if (!dex_buffer) { + LOGE("Failed to create DirectByteBuffer for DEX."); + return; + } - // Create an InMemoryDexClassLoader instance. - auto in_memory_cl_class = lsplant::JNI_FindClass(env, "dalvik/system/InMemoryDexClassLoader"); - if (!in_memory_cl_class) { - LOGE("Failed to find InMemoryDexClassLoader."); - return; - } - auto init_mid = lsplant::JNI_GetMethodID(env, in_memory_cl_class.get(), "", - "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); - if (!init_mid) { - LOGE("Failed to find InMemoryDexClassLoader constructor."); - return; - } + // Create an InMemoryDexClassLoader instance. + auto in_memory_cl_class = lsplant::JNI_FindClass(env, "dalvik/system/InMemoryDexClassLoader"); + if (!in_memory_cl_class) { + LOGE("Failed to find InMemoryDexClassLoader."); + return; + } + auto init_mid = lsplant::JNI_GetMethodID(env, in_memory_cl_class.get(), "", + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); + if (!init_mid) { + LOGE("Failed to find InMemoryDexClassLoader constructor."); + return; + } - auto new_cl = - lsplant::ScopedLocalRef(env, env->NewObject(in_memory_cl_class.get(), init_mid, - dex_buffer.get(), system_classloader.get())); - if (env->ExceptionCheck() || !new_cl) { - LOGE("Failed to create InMemoryDexClassLoader instance."); - env->ExceptionClear(); - return; + auto new_cl = + lsplant::ScopedLocalRef(env, env->NewObject(in_memory_cl_class.get(), init_mid, + dex_buffer.get(), system_classloader.get())); + if (env->ExceptionCheck() || !new_cl) { + LOGE("Failed to create InMemoryDexClassLoader instance."); + env->ExceptionClear(); + return; + } + + // Store a global reference to our new ClassLoader. + inject_class_loader_ = env->NewGlobalRef(new_cl.get()); + LOGV("Framework ClassLoader created successfully."); } - // Store a global reference to our new ClassLoader. - inject_class_loader_ = env->NewGlobalRef(new_cl.get()); - LOGV("Framework ClassLoader created successfully."); -} + void VectorModule::SetupEntryClass(JNIEnv *env) { + if (!inject_class_loader_) { + LOGE("Cannot setup entry class: ClassLoader is null."); + return; + } -void VectorModule::SetupEntryClass(JNIEnv *env) { - if (!inject_class_loader_) { - LOGE("Cannot setup entry class: ClassLoader is null."); - return; - } + // Use the obfuscation map from the config to get the real class name. + const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); + std::string entry_class_name; + entry_class_name = obfs_map.at("org.matrix.vector.core.") + "Main"; - // Use the obfuscation map from the config to get the real class name. - const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); - std::string entry_class_name; - entry_class_name = obfs_map.at("org.matrix.vector.core.") + "Main"; + // We must find the class through our custom ClassLoader. + auto entry_class = this->FindClassFromLoader(env, inject_class_loader_, entry_class_name); + if (!entry_class) { + LOGE("Failed to find entry class '{}' in the loaded DEX.", entry_class_name.c_str()); + return; + } - // We must find the class through our custom ClassLoader. - auto entry_class = this->FindClassFromLoader(env, inject_class_loader_, entry_class_name); - if (!entry_class) { - LOGE("Failed to find entry class '{}' in the loaded DEX.", entry_class_name.c_str()); - return; + // Store a global reference to the entry class. + entry_class_ = lsplant::JNI_NewGlobalRef(env, entry_class); + LOGV("Framework entry class '{}' located.", entry_class_name.c_str()); } - // Store a global reference to the entry class. - entry_class_ = lsplant::JNI_NewGlobalRef(env, entry_class); - LOGV("Framework entry class '{}' located.", entry_class_name.c_str()); -} - -void VectorModule::onLoad(zygisk::Api *api, JNIEnv *env) { - this->api_ = api; - this->env_ = env; - - // Create two singlton instances for classes Context and ConfigBridge - instance_.reset(this); - ConfigImpl::Init(); - LOGD("Vector Zygisk module loaded"); -} - -void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { - // Reset state for this new process fork. - should_inject_ = false; - is_manager_app_ = false; - - // --- Manager App Special Handling --- - // We identify our manager app by a special UID and - // grant it internet permissions by adding it to the INET group. - if (args->uid == kHostPackageUid) { - lsplant::JUTFString nice_name_str(env_, args->nice_name); - if (nice_name_str.get() == std::string(kManagerPackageName)) { - LOGI("Manager app detected. Granting internet permissions."); - is_manager_app_ = true; + void VectorModule::onLoad(zygisk::Api *api, JNIEnv *env) { + this->api_ = api; + this->env_ = env; - // Add GID_INET to the GID list. - int original_gids_count = env_->GetArrayLength(args->gids); - jintArray new_gids = env_->NewIntArray(original_gids_count + 1); - if (env_->ExceptionCheck()) { - LOGE("Failed to create new GID array for manager."); - env_->ExceptionClear(); // Clear exception to prevent a crash. - return; + // Create two singlton instances for classes Context and ConfigBridge + instance_.reset(this); + ConfigImpl::Init(); + LOGD("Vector Zygisk module loaded"); + } + + void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { + // Reset state for this new process fork. + should_inject_ = false; + is_manager_app_ = false; + + // --- Manager App Special Handling --- + // We identify our manager app by a special UID and + // grant it internet permissions by adding it to the INET group. + if (args->uid == kHostPackageUid) { + lsplant::JUTFString nice_name_str(env_, args->nice_name); + if (nice_name_str.get() == std::string(kManagerPackageName)) { + LOGI("Manager app detected. Granting internet permissions."); + is_manager_app_ = true; + + // Add GID_INET to the GID list. + int original_gids_count = env_->GetArrayLength(args->gids); + jintArray new_gids = env_->NewIntArray(original_gids_count + 1); + if (env_->ExceptionCheck()) { + LOGE("Failed to create new GID array for manager."); + env_->ExceptionClear(); // Clear exception to prevent a crash. + return; + } + + jint *gids_array = env_->GetIntArrayElements(args->gids, nullptr); + env_->SetIntArrayRegion(new_gids, 0, original_gids_count, gids_array); + env_->ReleaseIntArrayElements(args->gids, gids_array, JNI_ABORT); + + jint inet_gid = GID_INET; + env_->SetIntArrayRegion(new_gids, original_gids_count, 1, &inet_gid); + + args->nice_name = env_->NewStringUTF(ANTIK(INJECTED_PACKAGE_NAME)); + args->gids = new_gids; } + } - jint *gids_array = env_->GetIntArrayElements(args->gids, nullptr); - env_->SetIntArrayRegion(new_gids, 0, original_gids_count, gids_array); - env_->ReleaseIntArrayElements(args->gids, gids_array, JNI_ABORT); + IPCBridge::GetInstance().Initialize(env_); - jint inet_gid = GID_INET; - env_->SetIntArrayRegion(new_gids, original_gids_count, 1, &inet_gid); + // --- Injection Decision Logic --- + // Determine if the current process is a valid target for injection. + lsplant::JUTFString nice_name_str(env_, args->nice_name); - args->nice_name = env_->NewStringUTF(INJECTED_PACKAGE_NAME); - args->gids = new_gids; + // An app without a data directory cannot be a target. + if (!args->app_data_dir) { + LOGD("Skipping injection for '{}': no app_data_dir.", nice_name_str.get()); + return; } - } - IPCBridge::GetInstance().Initialize(env_); + // Child Zygotes are specialized zygotes for apps like WebView and are not targets. + if (args->is_child_zygote && *args->is_child_zygote) { + LOGD("Skipping injection for '{}': is a child zygote.", nice_name_str.get()); + return; + } - // --- Injection Decision Logic --- - // Determine if the current process is a valid target for injection. - lsplant::JUTFString nice_name_str(env_, args->nice_name); + // Skip isolated processes, which are heavily sandboxed. + const uid_t app_id = args->uid % PER_USER_RANGE; + if ((app_id >= FIRST_ISOLATED_UID && app_id <= LAST_ISOLATED_UID) || + (app_id >= FIRST_APP_ZYGOTE_ISOLATED_UID && app_id <= LAST_APP_ZYGOTE_ISOLATED_UID) || + app_id == SHARED_RELRO_UID) { + LOGV("Skipping injection for '{}': is an isolated process (UID: {}).", nice_name_str.get(), + app_id); + return; + } - // An app without a data directory cannot be a target. - if (!args->app_data_dir) { - LOGD("Skipping injection for '{}': no app_data_dir.", nice_name_str.get()); - return; + // If we passed all checks, mark this process for injection. + should_inject_ = true; + LOGV("Process '{}' (UID: {}) is marked for injection.", nice_name_str.get(), args->uid); } - // Child Zygotes are specialized zygotes for apps like WebView and are not targets. - if (args->is_child_zygote && *args->is_child_zygote) { - LOGD("Skipping injection for '{}': is a child zygote.", nice_name_str.get()); - return; - } + void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { + if (!should_inject_) { + SetAllowUnload(true); // Not a target, allow module to be unloaded. + return; + } - // Skip isolated processes, which are heavily sandboxed. - const uid_t app_id = args->uid % PER_USER_RANGE; - if ((app_id >= FIRST_ISOLATED_UID && app_id <= LAST_ISOLATED_UID) || - (app_id >= FIRST_APP_ZYGOTE_ISOLATED_UID && app_id <= LAST_APP_ZYGOTE_ISOLATED_UID) || - app_id == SHARED_RELRO_UID) { - LOGV("Skipping injection for '{}': is an isolated process (UID: {}).", nice_name_str.get(), - app_id); - return; - } + if (is_manager_app_) { + args->nice_name = env_->NewStringUTF(kManagerPackageName); + } + + // --- Framework Injection --- + lsplant::JUTFString nice_name_str(env_, args->nice_name); + LOGD("Attempting injection into '{}'.", nice_name_str.get()); + + auto &ipc_bridge = IPCBridge::GetInstance(); + auto binder = ipc_bridge.RequestAppBinder(env_, args->nice_name); + if (!binder) { + LOGD("No IPC binder obtained for '{}'. Skipping injection.", nice_name_str.get()); + SetAllowUnload(true); + return; + } - // If we passed all checks, mark this process for injection. - should_inject_ = true; - LOGV("Process '{}' (UID: {}) is marked for injection.", nice_name_str.get(), args->uid); -} + // Fetch resources from the manager service. + auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, binder.get()); + if (dex_fd < 0) { + LOGE("Failed to fetch framework DEX for '{}'.", nice_name_str.get()); + SetAllowUnload(true); + return; + } -void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { - if (!should_inject_) { - SetAllowUnload(true); // Not a target, allow module to be unloaded. - return; - } + auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, binder.get()); + ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); - if (is_manager_app_) { - args->nice_name = env_->NewStringUTF(kManagerPackageName); + { + PreloadedDex dex(dex_fd, dex_size); + this->LoadDex(env_, std::move(dex)); + } + close(dex_fd); // The FD is duplicated by mmap, we can close it now. + + // Initialize ART hooks via the native library. + this->InitArtHooker(env_, init_info_); + // Initialize JNI hooks via the native library. + this->InitHooks(env_); + // Find the Java entrypoint. + this->SetupEntryClass(env_); + + // Hand off control to the Java side of the framework. + this->FindAndCall( + env_, "forkCommon", "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", + JNI_FALSE, JNI_FALSE, args->nice_name, args->app_data_dir, binder.get(), is_manager_app_); + + LOGV("Injected Vector framework into '{}'.", nice_name_str.get()); + SetAllowUnload(false); // We are injected, PREVENT module unloading. } - // --- Framework Injection --- - lsplant::JUTFString nice_name_str(env_, args->nice_name); - LOGD("Attempting injection into '{}'.", nice_name_str.get()); + void VectorModule::preServerSpecialize(zygisk::ServerSpecializeArgs *args) { + // The system server is always a target for injection. + should_inject_ = true; + LOGI("System server process detected. Marking for injection."); - auto &ipc_bridge = IPCBridge::GetInstance(); - auto binder = ipc_bridge.RequestAppBinder(env_, args->nice_name); - if (!binder) { - LOGD("No IPC binder obtained for '{}'. Skipping injection.", nice_name_str.get()); - SetAllowUnload(true); - return; + // Initialize our IPC bridge singleton. + IPCBridge::GetInstance().Initialize(env_); } - // Fetch resources from the manager service. - auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, binder.get()); - if (dex_fd < 0) { - LOGE("Failed to fetch framework DEX for '{}'.", nice_name_str.get()); - SetAllowUnload(true); - return; - } + void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args) { + if (!should_inject_) { + SetAllowUnload(true); + return; + } - auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, binder.get()); - ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); + LOGD("Attempting injection into system_server."); + + // --- Device-Specific Workaround --- + // Some ZTE devices require argv[0] to be explicitly set to "system_server" + // for certain services to function correctly after modification. + if (__system_property_find("ro.vendor.product.ztename")) { + LOGV("Applying ZTE-specific workaround: setting argv[0] to system_server."); + auto process_class = lsplant::ScopedLocalRef(env_, env_->FindClass("android/os/Process")); + if (process_class) { + auto set_argv0_mid = + env_->GetStaticMethodID(process_class.get(), "setArgV0", "(Ljava/lang/String;)V"); + auto name_str = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system_server")); + if (set_argv0_mid && name_str) { + env_->CallStaticVoidMethod(process_class.get(), set_argv0_mid, name_str.get()); + } + } + if (env_->ExceptionCheck()) { + LOGW("Exception occurred during ZTE workaround."); + env_->ExceptionClear(); + } + } - { - PreloadedDex dex(dex_fd, dex_size); - this->LoadDex(env_, std::move(dex)); - } - close(dex_fd); // The FD is duplicated by mmap, we can close it now. - - // Initialize ART hooks via the native library. - this->InitArtHooker(env_, init_info_); - // Initialize JNI hooks via the native library. - this->InitHooks(env_); - // Find the Java entrypoint. - this->SetupEntryClass(env_); - - // Hand off control to the Java side of the framework. - this->FindAndCall( - env_, "forkCommon", "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", - JNI_FALSE, JNI_FALSE, args->nice_name, args->app_data_dir, binder.get(), is_manager_app_); - - LOGV("Injected Vector framework into '{}'.", nice_name_str.get()); - SetAllowUnload(false); // We are injected, PREVENT module unloading. -} - -void VectorModule::preServerSpecialize(zygisk::ServerSpecializeArgs *args) { - // The system server is always a target for injection. - should_inject_ = true; - LOGI("System server process detected. Marking for injection."); - - // Initialize our IPC bridge singleton. - IPCBridge::GetInstance().Initialize(env_); -} - -void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args) { - if (!should_inject_) { - SetAllowUnload(true); - return; - } + // --- Framework Injection for System Server --- + auto &ipc_bridge = IPCBridge::GetInstance(); + std::string bridgeServiceName = "serial"; + bool is_late_inject = (args->runtime_flags & RuntimeFlags::LATE_INJECT) != 0; + if (is_late_inject) bridgeServiceName = "serial_vector"; + auto system_binder = ipc_bridge.RequestSystemServerBinder(env_, bridgeServiceName); + if (!system_binder) { + LOGE("Failed to get system server IPC binder. Aborting injection."); + SetAllowUnload(true); // Allow unload on failure. + return; + } - LOGD("Attempting injection into system_server."); - - // --- Device-Specific Workaround --- - // Some ZTE devices require argv[0] to be explicitly set to "system_server" - // for certain services to function correctly after modification. - if (__system_property_find("ro.vendor.product.ztename")) { - LOGV("Applying ZTE-specific workaround: setting argv[0] to system_server."); - auto process_class = lsplant::ScopedLocalRef(env_, env_->FindClass("android/os/Process")); - if (process_class) { - auto set_argv0_mid = - env_->GetStaticMethodID(process_class.get(), "setArgV0", "(Ljava/lang/String;)V"); - auto name_str = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system_server")); - if (set_argv0_mid && name_str) { - env_->CallStaticVoidMethod(process_class.get(), set_argv0_mid, name_str.get()); - } + auto manager_binder = + ipc_bridge.RequestManagerBinderFromSystemServer(env_, system_binder.get()); + + // Use either the direct manager binder if available, + // otherwise proxy through the system binder. + jobject effective_binder = manager_binder ? manager_binder.get() : system_binder.get(); + + auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, effective_binder); + if (dex_fd < 0) { + LOGE("Failed to fetch framework DEX for system_server."); + SetAllowUnload(true); + return; } - if (env_->ExceptionCheck()) { - LOGW("Exception occurred during ZTE workaround."); - env_->ExceptionClear(); + + auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, effective_binder); + ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); + + { + PreloadedDex dex(dex_fd, dex_size); + this->LoadDex(env_, std::move(dex)); } - } + close(dex_fd); - // --- Framework Injection for System Server --- - auto &ipc_bridge = IPCBridge::GetInstance(); - std::string bridgeServiceName = "serial"; - bool is_late_inject = (args->runtime_flags & RuntimeFlags::LATE_INJECT) != 0; - if (is_late_inject) bridgeServiceName = "serial_vector"; - auto system_binder = ipc_bridge.RequestSystemServerBinder(env_, bridgeServiceName); - if (!system_binder) { - LOGE("Failed to get system server IPC binder. Aborting injection."); - SetAllowUnload(true); // Allow unload on failure. - return; - } + ipc_bridge.HookBridge(env_); - auto manager_binder = - ipc_bridge.RequestManagerBinderFromSystemServer(env_, system_binder.get()); + this->InitArtHooker(env_, init_info_); + this->InitHooks(env_); + this->SetupEntryClass(env_); - // Use either the direct manager binder if available, - // otherwise proxy through the system binder. - jobject effective_binder = manager_binder ? manager_binder.get() : system_binder.get(); + auto system_name = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system")); + this->FindAndCall(env_, "forkCommon", + "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_TRUE, + is_late_inject, system_name.get(), nullptr, manager_binder.get(), + is_manager_app_); - auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, effective_binder); - if (dex_fd < 0) { - LOGE("Failed to fetch framework DEX for system_server."); - SetAllowUnload(true); - return; + LOGI("Injected Vector framework into system_server."); + SetAllowUnload(false); // We are injected, PREVENT module unloading. } - auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, effective_binder); - ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); + void VectorModule::SetAllowUnload(bool unload) { + if (api_ && unload) { + LOGD("Allowing Zygisk to unload module library."); + api_->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); - { - PreloadedDex dex(dex_fd, dex_size); - this->LoadDex(env_, std::move(dex)); - } - close(dex_fd); - - ipc_bridge.HookBridge(env_); - - this->InitArtHooker(env_, init_info_); - this->InitHooks(env_); - this->SetupEntryClass(env_); - - auto system_name = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system")); - this->FindAndCall(env_, "forkCommon", - "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_TRUE, - is_late_inject, system_name.get(), nullptr, manager_binder.get(), - is_manager_app_); - - LOGI("Injected Vector framework into system_server."); - SetAllowUnload(false); // We are injected, PREVENT module unloading. -} - -void VectorModule::SetAllowUnload(bool unload) { - if (api_ && unload) { - LOGD("Allowing Zygisk to unload module library."); - api_->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); - - // Release the pointer from the unique_ptr's control. This prevents the - // static unique_ptr's destructor from calling delete on our object, which - // would cause a double-free when the Zygisk framework cleans up. - if (instance_.release() != nullptr) { - LOGD("Module context singleton released."); + // Release the pointer from the unique_ptr's control. This prevents the + // static unique_ptr's destructor from calling delete on our object, which + // would cause a double-free when the Zygisk framework cleans up. + if (instance_.release() != nullptr) { + LOGD("Module context singleton released."); + } + } else { + LOGD("Preventing Zygisk from unloading module library."); } - } else { - LOGD("Preventing Zygisk from unloading module library."); } -} } // namespace vector::native::module From fa42b2a36a2f880af72d56324ced6ad21e701d9b Mon Sep 17 00:00:00 2001 From: isPointer Date: Tue, 14 Apr 2026 05:50:02 +0600 Subject: [PATCH 2/3] Update config.h --- native/include/common/config.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/native/include/common/config.h b/native/include/common/config.h index 88beeeb63..41884c712 100644 --- a/native/include/common/config.h +++ b/native/include/common/config.h @@ -7,16 +7,16 @@ namespace vector::native { -[[nodiscard]] constexpr bool IsDebugBuild() { + [[nodiscard]] constexpr bool IsDebugBuild() { #ifdef NDEBUG - return false; + return false; #else - return true; + return true; #endif -} + } /// A compile-time constant indicating if this is a debug build. -inline constexpr bool kIsDebugBuild = IsDebugBuild(); + inline constexpr bool kIsDebugBuild = IsDebugBuild(); /** * @def LP_SELECT(lp32, lp64) @@ -31,21 +31,21 @@ inline constexpr bool kIsDebugBuild = IsDebugBuild(); #endif /// The filename of the core Android Runtime (ART) library. -inline constexpr auto kArtLibraryName = "libart.so"; + inline constexpr auto kArtLibraryName = "libart.so"; /// The filename of the Android Binder library. -inline constexpr auto kBinderLibraryName = "libbinder.so"; + inline constexpr auto kBinderLibraryName = "libbinder.so"; /// The filename of the Android Framework library. -inline constexpr auto kFrameworkLibraryName = "libandroidfw.so"; + inline constexpr auto kFrameworkLibraryName = "libandroidfw.so"; /// The path to the dynamic linker. -inline constexpr auto kLinkerPath = "/linker"; + inline constexpr auto kLinkerPath = "/linker"; /// The version code of the library, populated by the build system. -const int kVersionCode = VERSION_CODE; + const int kVersionCode = VERSION_CODE; /// The version name of the library, populated by the build system. -const char *const kVersionName = VERSION_NAME; + inline constexpr auto kVersionName = VERSION_NAME; } // namespace vector::native From 515ef1873fe03e6812a751a2a8def85ddd51d7cb Mon Sep 17 00:00:00 2001 From: isPointer Date: Tue, 14 Apr 2026 06:37:48 +0600 Subject: [PATCH 3/3] Refactor constants and improve code formatting --- zygisk/src/main/cpp/module.cpp | 717 +++++++++++++++++---------------- 1 file changed, 359 insertions(+), 358 deletions(-) diff --git a/zygisk/src/main/cpp/module.cpp b/zygisk/src/main/cpp/module.cpp index f3ae54f09..1cf411bed 100644 --- a/zygisk/src/main/cpp/module.cpp +++ b/zygisk/src/main/cpp/module.cpp @@ -11,6 +11,7 @@ #include #include "ipc_bridge.h" + /** * @file config.h * @brief Compile-time constants, version information, and platform-specific configurations. @@ -34,46 +35,46 @@ namespace vector::native::module { // https://android.googlesource.com/platform/system/core/+/master/libcutils/include/private/android_filesystem_config.h // The range of UIDs used for isolated processes (e.g., web renderers, WebView). - constexpr int FIRST_ISOLATED_UID = 99000; - constexpr int LAST_ISOLATED_UID = 99999; +constexpr int FIRST_ISOLATED_UID = 99000; +constexpr int LAST_ISOLATED_UID = 99999; // The range of UIDs used for application zygotes, which are also not targets. - constexpr int FIRST_APP_ZYGOTE_ISOLATED_UID = 90000; - constexpr int LAST_APP_ZYGOTE_ISOLATED_UID = 98999; +constexpr int FIRST_APP_ZYGOTE_ISOLATED_UID = 90000; +constexpr int LAST_APP_ZYGOTE_ISOLATED_UID = 98999; // UID for the process responsible for creating shared RELRO files. - constexpr int SHARED_RELRO_UID = 1037; +constexpr int SHARED_RELRO_UID = 1037; // Android uses this to separate users. UID = AppID + UserID * 10000. - constexpr int PER_USER_RANGE = 100000; +constexpr int PER_USER_RANGE = 100000; // Defined via CMake generated marcos - constexpr uid_t kHostPackageUid = INJECTED_PACKAGE_UID; - const char* kHostPackageName = ANTIK(INJECTED_PACKAGE_NAME); - const char* kManagerPackageName = ANTIK(MANAGER_PACKAGE_NAME); - constexpr uid_t GID_INET = 3003; // Android's Internet group ID. - - enum RuntimeFlags : uint32_t { - // Flags defined by NeoZygisk - LATE_INJECT = 1 << 30, - }; +constexpr uid_t kHostPackageUid = INJECTED_PACKAGE_UID; +const char* kHostPackageName = ANTIK(INJECTED_PACKAGE_NAME); +const char* kManagerPackageName = ANTIK(MANAGER_PACKAGE_NAME); +constexpr uid_t GID_INET = 3003; // Android's Internet group ID. + +enum RuntimeFlags : uint32_t { + // Flags defined by NeoZygisk + LATE_INJECT = 1 << 30, +}; // A simply ConfigBridge implemnetation holding obfuscation maps in memory - using obfuscation_map_t = std::map; - class ConfigImpl : public ConfigBridge { - public: - inline static void Init() { instance_ = std::make_unique(); } +using obfuscation_map_t = std::map; +class ConfigImpl : public ConfigBridge { +public: + inline static void Init() { instance_ = std::make_unique(); } - virtual obfuscation_map_t &obfuscation_map() override { return obfuscation_map_; } + virtual obfuscation_map_t &obfuscation_map() override { return obfuscation_map_; } - virtual void obfuscation_map(obfuscation_map_t m) override { obfuscation_map_ = std::move(m); } + virtual void obfuscation_map(obfuscation_map_t m) override { obfuscation_map_ = std::move(m); } - private: - ConfigImpl() = default; +private: + ConfigImpl() = default; - friend std::unique_ptr std::make_unique(); - obfuscation_map_t obfuscation_map_; - }; + friend std::unique_ptr std::make_unique(); + obfuscation_map_t obfuscation_map_; +}; /** * @class VectorModule @@ -88,389 +89,389 @@ namespace vector::native::module { * using the IPCBridge to fetch the framework from the manager service, and then * using the Context base to perform the actual injection. */ - class VectorModule : public zygisk::ModuleBase, public vector::native::Context { - public: - void onLoad(zygisk::Api *api, JNIEnv *env) override; - void preAppSpecialize(zygisk::AppSpecializeArgs *args) override; - void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override; - void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override; - void postServerSpecialize(const zygisk::ServerSpecializeArgs *args) override; - - protected: - /** - * @brief Provides the concrete implementation for loading the framework DEX. - * - * This method is a pure virtual in the native::core::Context base class and - * must be implemented here. - * It uses an InMemoryDexClassLoader to load our framework into the target process. - */ - void LoadDex(JNIEnv *env, PreloadedDex &&dex) override; - - /** - * @brief Provides the concrete implementation for finding the Java entry - * class. - * - * This method is also a pure virtual in the base class. - * It uses the obfuscation map to determine the real entry class name and - * finds it in the ClassLoader we created in LoadDex. - */ - void SetupEntryClass(JNIEnv *env) override; - - private: - /** - * @brief Encapsulates the logic for telling Zygisk whether to unload our library. - * - * If we don't inject into a process, we allow Zygisk to dlclose our .so. - * Otherwise, we MUST prevent this. - * @param unload True to allow unloading, false to prevent it. - */ - void SetAllowUnload(bool unload); - - zygisk::Api *api_ = nullptr; - JNIEnv *env_ = nullptr; - - // --- ART Hooker Configuration --- - const lsplant::InitInfo init_info_{ - .inline_hooker = - [](auto target, auto replace) { - void *backup = nullptr; - return HookInline(target, replace, &backup) == 0 ? backup : nullptr; - }, - .inline_unhooker = [](auto target) { return UnhookInline(target) == 0; }, - .art_symbol_resolver = - [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbAddress(symbol); }, - .art_symbol_prefix_resolver = - [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbPrefixFirstAddress(symbol); }, - .generated_class_name = "Vector_", - .generated_source_name = "Dobby", - }; - - // State managed within the class instance for each forked process. - bool should_inject_ = false; - bool is_manager_app_ = false; +class VectorModule : public zygisk::ModuleBase, public vector::native::Context { +public: + void onLoad(zygisk::Api *api, JNIEnv *env) override; + void preAppSpecialize(zygisk::AppSpecializeArgs *args) override; + void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override; + void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override; + void postServerSpecialize(const zygisk::ServerSpecializeArgs *args) override; + +protected: + /** + * @brief Provides the concrete implementation for loading the framework DEX. + * + * This method is a pure virtual in the native::core::Context base class and + * must be implemented here. + * It uses an InMemoryDexClassLoader to load our framework into the target process. + */ + void LoadDex(JNIEnv *env, PreloadedDex &&dex) override; + + /** + * @brief Provides the concrete implementation for finding the Java entry + * class. + * + * This method is also a pure virtual in the base class. + * It uses the obfuscation map to determine the real entry class name and + * finds it in the ClassLoader we created in LoadDex. + */ + void SetupEntryClass(JNIEnv *env) override; + +private: + /** + * @brief Encapsulates the logic for telling Zygisk whether to unload our library. + * + * If we don't inject into a process, we allow Zygisk to dlclose our .so. + * Otherwise, we MUST prevent this. + * @param unload True to allow unloading, false to prevent it. + */ + void SetAllowUnload(bool unload); + + zygisk::Api *api_ = nullptr; + JNIEnv *env_ = nullptr; + + // --- ART Hooker Configuration --- + const lsplant::InitInfo init_info_{ + .inline_hooker = + [](auto target, auto replace) { + void *backup = nullptr; + return HookInline(target, replace, &backup) == 0 ? backup : nullptr; + }, + .inline_unhooker = [](auto target) { return UnhookInline(target) == 0; }, + .art_symbol_resolver = + [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbAddress(symbol); }, + .art_symbol_prefix_resolver = + [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbPrefixFirstAddress(symbol); }, + .generated_class_name = "Vector_", + .generated_source_name = "Dobby", }; + // State managed within the class instance for each forked process. + bool should_inject_ = false; + bool is_manager_app_ = false; +}; + // ========================================================================================= // Implementation of VectorModule // ========================================================================================= - void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { - LOGV("Loading framework DEX into memory (size: {}).", dex.size()); - - // Get the system ClassLoader. This will be the parent of our new loader. - auto classloader_class = lsplant::JNI_FindClass(env, "java/lang/ClassLoader"); - if (!classloader_class) { - LOGE("Failed to find java.lang.ClassLoader"); - return; - } - auto getsyscl_mid = lsplant::JNI_GetStaticMethodID( - env, classloader_class.get(), "getSystemClassLoader", "()Ljava/lang/ClassLoader;"); - auto system_classloader = - lsplant::JNI_CallStaticObjectMethod(env, classloader_class.get(), getsyscl_mid); - if (!system_classloader) { - LOGE("Failed to get SystemClassLoader"); - return; - } +void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { + LOGV("Loading framework DEX into memory (size: {}).", dex.size()); - // Create a Java ByteBuffer wrapping our in-memory DEX data. - auto byte_buffer_class = lsplant::JNI_FindClass(env, "java/nio/ByteBuffer"); - if (!byte_buffer_class) { - LOGE("Failed to find java.nio.ByteBuffer"); - return; - } - auto dex_buffer = - lsplant::ScopedLocalRef(env, env->NewDirectByteBuffer(dex.data(), dex.size())); - if (!dex_buffer) { - LOGE("Failed to create DirectByteBuffer for DEX."); - return; - } - - // Create an InMemoryDexClassLoader instance. - auto in_memory_cl_class = lsplant::JNI_FindClass(env, "dalvik/system/InMemoryDexClassLoader"); - if (!in_memory_cl_class) { - LOGE("Failed to find InMemoryDexClassLoader."); - return; - } - auto init_mid = lsplant::JNI_GetMethodID(env, in_memory_cl_class.get(), "", - "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); - if (!init_mid) { - LOGE("Failed to find InMemoryDexClassLoader constructor."); - return; - } - - auto new_cl = - lsplant::ScopedLocalRef(env, env->NewObject(in_memory_cl_class.get(), init_mid, - dex_buffer.get(), system_classloader.get())); - if (env->ExceptionCheck() || !new_cl) { - LOGE("Failed to create InMemoryDexClassLoader instance."); - env->ExceptionClear(); - return; - } + // Get the system ClassLoader. This will be the parent of our new loader. + auto classloader_class = lsplant::JNI_FindClass(env, "java/lang/ClassLoader"); + if (!classloader_class) { + LOGE("Failed to find java.lang.ClassLoader"); + return; + } + auto getsyscl_mid = lsplant::JNI_GetStaticMethodID( + env, classloader_class.get(), "getSystemClassLoader", "()Ljava/lang/ClassLoader;"); + auto system_classloader = + lsplant::JNI_CallStaticObjectMethod(env, classloader_class.get(), getsyscl_mid); + if (!system_classloader) { + LOGE("Failed to get SystemClassLoader"); + return; + } - // Store a global reference to our new ClassLoader. - inject_class_loader_ = env->NewGlobalRef(new_cl.get()); - LOGV("Framework ClassLoader created successfully."); + // Create a Java ByteBuffer wrapping our in-memory DEX data. + auto byte_buffer_class = lsplant::JNI_FindClass(env, "java/nio/ByteBuffer"); + if (!byte_buffer_class) { + LOGE("Failed to find java.nio.ByteBuffer"); + return; + } + auto dex_buffer = + lsplant::ScopedLocalRef(env, env->NewDirectByteBuffer(dex.data(), dex.size())); + if (!dex_buffer) { + LOGE("Failed to create DirectByteBuffer for DEX."); + return; } - void VectorModule::SetupEntryClass(JNIEnv *env) { - if (!inject_class_loader_) { - LOGE("Cannot setup entry class: ClassLoader is null."); - return; - } + // Create an InMemoryDexClassLoader instance. + auto in_memory_cl_class = lsplant::JNI_FindClass(env, "dalvik/system/InMemoryDexClassLoader"); + if (!in_memory_cl_class) { + LOGE("Failed to find InMemoryDexClassLoader."); + return; + } + auto init_mid = lsplant::JNI_GetMethodID(env, in_memory_cl_class.get(), "", + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); + if (!init_mid) { + LOGE("Failed to find InMemoryDexClassLoader constructor."); + return; + } - // Use the obfuscation map from the config to get the real class name. - const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); - std::string entry_class_name; - entry_class_name = obfs_map.at("org.matrix.vector.core.") + "Main"; + auto new_cl = + lsplant::ScopedLocalRef(env, env->NewObject(in_memory_cl_class.get(), init_mid, + dex_buffer.get(), system_classloader.get())); + if (env->ExceptionCheck() || !new_cl) { + LOGE("Failed to create InMemoryDexClassLoader instance."); + env->ExceptionClear(); + return; + } - // We must find the class through our custom ClassLoader. - auto entry_class = this->FindClassFromLoader(env, inject_class_loader_, entry_class_name); - if (!entry_class) { - LOGE("Failed to find entry class '{}' in the loaded DEX.", entry_class_name.c_str()); - return; - } + // Store a global reference to our new ClassLoader. + inject_class_loader_ = env->NewGlobalRef(new_cl.get()); + LOGV("Framework ClassLoader created successfully."); +} - // Store a global reference to the entry class. - entry_class_ = lsplant::JNI_NewGlobalRef(env, entry_class); - LOGV("Framework entry class '{}' located.", entry_class_name.c_str()); +void VectorModule::SetupEntryClass(JNIEnv *env) { + if (!inject_class_loader_) { + LOGE("Cannot setup entry class: ClassLoader is null."); + return; } - void VectorModule::onLoad(zygisk::Api *api, JNIEnv *env) { - this->api_ = api; - this->env_ = env; + // Use the obfuscation map from the config to get the real class name. + const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); + std::string entry_class_name; + entry_class_name = obfs_map.at("org.matrix.vector.core.") + "Main"; - // Create two singlton instances for classes Context and ConfigBridge - instance_.reset(this); - ConfigImpl::Init(); - LOGD("Vector Zygisk module loaded"); + // We must find the class through our custom ClassLoader. + auto entry_class = this->FindClassFromLoader(env, inject_class_loader_, entry_class_name); + if (!entry_class) { + LOGE("Failed to find entry class '{}' in the loaded DEX.", entry_class_name.c_str()); + return; } - void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { - // Reset state for this new process fork. - should_inject_ = false; - is_manager_app_ = false; - - // --- Manager App Special Handling --- - // We identify our manager app by a special UID and - // grant it internet permissions by adding it to the INET group. - if (args->uid == kHostPackageUid) { - lsplant::JUTFString nice_name_str(env_, args->nice_name); - if (nice_name_str.get() == std::string(kManagerPackageName)) { - LOGI("Manager app detected. Granting internet permissions."); - is_manager_app_ = true; - - // Add GID_INET to the GID list. - int original_gids_count = env_->GetArrayLength(args->gids); - jintArray new_gids = env_->NewIntArray(original_gids_count + 1); - if (env_->ExceptionCheck()) { - LOGE("Failed to create new GID array for manager."); - env_->ExceptionClear(); // Clear exception to prevent a crash. - return; - } - - jint *gids_array = env_->GetIntArrayElements(args->gids, nullptr); - env_->SetIntArrayRegion(new_gids, 0, original_gids_count, gids_array); - env_->ReleaseIntArrayElements(args->gids, gids_array, JNI_ABORT); - - jint inet_gid = GID_INET; - env_->SetIntArrayRegion(new_gids, original_gids_count, 1, &inet_gid); - - args->nice_name = env_->NewStringUTF(ANTIK(INJECTED_PACKAGE_NAME)); - args->gids = new_gids; + // Store a global reference to the entry class. + entry_class_ = lsplant::JNI_NewGlobalRef(env, entry_class); + LOGV("Framework entry class '{}' located.", entry_class_name.c_str()); +} + +void VectorModule::onLoad(zygisk::Api *api, JNIEnv *env) { + this->api_ = api; + this->env_ = env; + + // Create two singlton instances for classes Context and ConfigBridge + instance_.reset(this); + ConfigImpl::Init(); + LOGD("Vector Zygisk module loaded"); +} + +void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { + // Reset state for this new process fork. + should_inject_ = false; + is_manager_app_ = false; + + // --- Manager App Special Handling --- + // We identify our manager app by a special UID and + // grant it internet permissions by adding it to the INET group. + if (args->uid == kHostPackageUid) { + lsplant::JUTFString nice_name_str(env_, args->nice_name); + if (nice_name_str.get() == std::string(kManagerPackageName)) { + LOGI("Manager app detected. Granting internet permissions."); + is_manager_app_ = true; + + // Add GID_INET to the GID list. + int original_gids_count = env_->GetArrayLength(args->gids); + jintArray new_gids = env_->NewIntArray(original_gids_count + 1); + if (env_->ExceptionCheck()) { + LOGE("Failed to create new GID array for manager."); + env_->ExceptionClear(); // Clear exception to prevent a crash. + return; } - } - IPCBridge::GetInstance().Initialize(env_); + jint *gids_array = env_->GetIntArrayElements(args->gids, nullptr); + env_->SetIntArrayRegion(new_gids, 0, original_gids_count, gids_array); + env_->ReleaseIntArrayElements(args->gids, gids_array, JNI_ABORT); - // --- Injection Decision Logic --- - // Determine if the current process is a valid target for injection. - lsplant::JUTFString nice_name_str(env_, args->nice_name); + jint inet_gid = GID_INET; + env_->SetIntArrayRegion(new_gids, original_gids_count, 1, &inet_gid); - // An app without a data directory cannot be a target. - if (!args->app_data_dir) { - LOGD("Skipping injection for '{}': no app_data_dir.", nice_name_str.get()); - return; + args->nice_name = env_->NewStringUTF(ANTIK(INJECTED_PACKAGE_NAME)); + args->gids = new_gids; } + } - // Child Zygotes are specialized zygotes for apps like WebView and are not targets. - if (args->is_child_zygote && *args->is_child_zygote) { - LOGD("Skipping injection for '{}': is a child zygote.", nice_name_str.get()); - return; - } + IPCBridge::GetInstance().Initialize(env_); - // Skip isolated processes, which are heavily sandboxed. - const uid_t app_id = args->uid % PER_USER_RANGE; - if ((app_id >= FIRST_ISOLATED_UID && app_id <= LAST_ISOLATED_UID) || - (app_id >= FIRST_APP_ZYGOTE_ISOLATED_UID && app_id <= LAST_APP_ZYGOTE_ISOLATED_UID) || - app_id == SHARED_RELRO_UID) { - LOGV("Skipping injection for '{}': is an isolated process (UID: {}).", nice_name_str.get(), - app_id); - return; - } + // --- Injection Decision Logic --- + // Determine if the current process is a valid target for injection. + lsplant::JUTFString nice_name_str(env_, args->nice_name); - // If we passed all checks, mark this process for injection. - should_inject_ = true; - LOGV("Process '{}' (UID: {}) is marked for injection.", nice_name_str.get(), args->uid); + // An app without a data directory cannot be a target. + if (!args->app_data_dir) { + LOGD("Skipping injection for '{}': no app_data_dir.", nice_name_str.get()); + return; } - void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { - if (!should_inject_) { - SetAllowUnload(true); // Not a target, allow module to be unloaded. - return; - } - - if (is_manager_app_) { - args->nice_name = env_->NewStringUTF(kManagerPackageName); - } - - // --- Framework Injection --- - lsplant::JUTFString nice_name_str(env_, args->nice_name); - LOGD("Attempting injection into '{}'.", nice_name_str.get()); - - auto &ipc_bridge = IPCBridge::GetInstance(); - auto binder = ipc_bridge.RequestAppBinder(env_, args->nice_name); - if (!binder) { - LOGD("No IPC binder obtained for '{}'. Skipping injection.", nice_name_str.get()); - SetAllowUnload(true); - return; - } + // Child Zygotes are specialized zygotes for apps like WebView and are not targets. + if (args->is_child_zygote && *args->is_child_zygote) { + LOGD("Skipping injection for '{}': is a child zygote.", nice_name_str.get()); + return; + } - // Fetch resources from the manager service. - auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, binder.get()); - if (dex_fd < 0) { - LOGE("Failed to fetch framework DEX for '{}'.", nice_name_str.get()); - SetAllowUnload(true); - return; - } + // Skip isolated processes, which are heavily sandboxed. + const uid_t app_id = args->uid % PER_USER_RANGE; + if ((app_id >= FIRST_ISOLATED_UID && app_id <= LAST_ISOLATED_UID) || + (app_id >= FIRST_APP_ZYGOTE_ISOLATED_UID && app_id <= LAST_APP_ZYGOTE_ISOLATED_UID) || + app_id == SHARED_RELRO_UID) { + LOGV("Skipping injection for '{}': is an isolated process (UID: {}).", nice_name_str.get(), + app_id); + return; + } - auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, binder.get()); - ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); + // If we passed all checks, mark this process for injection. + should_inject_ = true; + LOGV("Process '{}' (UID: {}) is marked for injection.", nice_name_str.get(), args->uid); +} - { - PreloadedDex dex(dex_fd, dex_size); - this->LoadDex(env_, std::move(dex)); - } - close(dex_fd); // The FD is duplicated by mmap, we can close it now. - - // Initialize ART hooks via the native library. - this->InitArtHooker(env_, init_info_); - // Initialize JNI hooks via the native library. - this->InitHooks(env_); - // Find the Java entrypoint. - this->SetupEntryClass(env_); - - // Hand off control to the Java side of the framework. - this->FindAndCall( - env_, "forkCommon", "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", - JNI_FALSE, JNI_FALSE, args->nice_name, args->app_data_dir, binder.get(), is_manager_app_); - - LOGV("Injected Vector framework into '{}'.", nice_name_str.get()); - SetAllowUnload(false); // We are injected, PREVENT module unloading. +void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { + if (!should_inject_) { + SetAllowUnload(true); // Not a target, allow module to be unloaded. + return; } - void VectorModule::preServerSpecialize(zygisk::ServerSpecializeArgs *args) { - // The system server is always a target for injection. - should_inject_ = true; - LOGI("System server process detected. Marking for injection."); - - // Initialize our IPC bridge singleton. - IPCBridge::GetInstance().Initialize(env_); + if (is_manager_app_) { + args->nice_name = env_->NewStringUTF(kManagerPackageName); } - void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args) { - if (!should_inject_) { - SetAllowUnload(true); - return; - } + // --- Framework Injection --- + lsplant::JUTFString nice_name_str(env_, args->nice_name); + LOGD("Attempting injection into '{}'.", nice_name_str.get()); - LOGD("Attempting injection into system_server."); - - // --- Device-Specific Workaround --- - // Some ZTE devices require argv[0] to be explicitly set to "system_server" - // for certain services to function correctly after modification. - if (__system_property_find("ro.vendor.product.ztename")) { - LOGV("Applying ZTE-specific workaround: setting argv[0] to system_server."); - auto process_class = lsplant::ScopedLocalRef(env_, env_->FindClass("android/os/Process")); - if (process_class) { - auto set_argv0_mid = - env_->GetStaticMethodID(process_class.get(), "setArgV0", "(Ljava/lang/String;)V"); - auto name_str = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system_server")); - if (set_argv0_mid && name_str) { - env_->CallStaticVoidMethod(process_class.get(), set_argv0_mid, name_str.get()); - } - } - if (env_->ExceptionCheck()) { - LOGW("Exception occurred during ZTE workaround."); - env_->ExceptionClear(); - } - } + auto &ipc_bridge = IPCBridge::GetInstance(); + auto binder = ipc_bridge.RequestAppBinder(env_, args->nice_name); + if (!binder) { + LOGD("No IPC binder obtained for '{}'. Skipping injection.", nice_name_str.get()); + SetAllowUnload(true); + return; + } - // --- Framework Injection for System Server --- - auto &ipc_bridge = IPCBridge::GetInstance(); - std::string bridgeServiceName = "serial"; - bool is_late_inject = (args->runtime_flags & RuntimeFlags::LATE_INJECT) != 0; - if (is_late_inject) bridgeServiceName = "serial_vector"; - auto system_binder = ipc_bridge.RequestSystemServerBinder(env_, bridgeServiceName); - if (!system_binder) { - LOGE("Failed to get system server IPC binder. Aborting injection."); - SetAllowUnload(true); // Allow unload on failure. - return; - } + // Fetch resources from the manager service. + auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, binder.get()); + if (dex_fd < 0) { + LOGE("Failed to fetch framework DEX for '{}'.", nice_name_str.get()); + SetAllowUnload(true); + return; + } - auto manager_binder = - ipc_bridge.RequestManagerBinderFromSystemServer(env_, system_binder.get()); + auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, binder.get()); + ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); - // Use either the direct manager binder if available, - // otherwise proxy through the system binder. - jobject effective_binder = manager_binder ? manager_binder.get() : system_binder.get(); + { + PreloadedDex dex(dex_fd, dex_size); + this->LoadDex(env_, std::move(dex)); + } + close(dex_fd); // The FD is duplicated by mmap, we can close it now. + + // Initialize ART hooks via the native library. + this->InitArtHooker(env_, init_info_); + // Initialize JNI hooks via the native library. + this->InitHooks(env_); + // Find the Java entrypoint. + this->SetupEntryClass(env_); + + // Hand off control to the Java side of the framework. + this->FindAndCall( + env_, "forkCommon", "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", + JNI_FALSE, JNI_FALSE, args->nice_name, args->app_data_dir, binder.get(), is_manager_app_); + + LOGV("Injected Vector framework into '{}'.", nice_name_str.get()); + SetAllowUnload(false); // We are injected, PREVENT module unloading. +} + +void VectorModule::preServerSpecialize(zygisk::ServerSpecializeArgs *args) { + // The system server is always a target for injection. + should_inject_ = true; + LOGI("System server process detected. Marking for injection."); + + // Initialize our IPC bridge singleton. + IPCBridge::GetInstance().Initialize(env_); +} + +void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args) { + if (!should_inject_) { + SetAllowUnload(true); + return; + } - auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, effective_binder); - if (dex_fd < 0) { - LOGE("Failed to fetch framework DEX for system_server."); - SetAllowUnload(true); - return; + LOGD("Attempting injection into system_server."); + + // --- Device-Specific Workaround --- + // Some ZTE devices require argv[0] to be explicitly set to "system_server" + // for certain services to function correctly after modification. + if (__system_property_find("ro.vendor.product.ztename")) { + LOGV("Applying ZTE-specific workaround: setting argv[0] to system_server."); + auto process_class = lsplant::ScopedLocalRef(env_, env_->FindClass("android/os/Process")); + if (process_class) { + auto set_argv0_mid = + env_->GetStaticMethodID(process_class.get(), "setArgV0", "(Ljava/lang/String;)V"); + auto name_str = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system_server")); + if (set_argv0_mid && name_str) { + env_->CallStaticVoidMethod(process_class.get(), set_argv0_mid, name_str.get()); + } } - - auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, effective_binder); - ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); - - { - PreloadedDex dex(dex_fd, dex_size); - this->LoadDex(env_, std::move(dex)); + if (env_->ExceptionCheck()) { + LOGW("Exception occurred during ZTE workaround."); + env_->ExceptionClear(); } - close(dex_fd); + } - ipc_bridge.HookBridge(env_); + // --- Framework Injection for System Server --- + auto &ipc_bridge = IPCBridge::GetInstance(); + std::string bridgeServiceName = "serial"; + bool is_late_inject = (args->runtime_flags & RuntimeFlags::LATE_INJECT) != 0; + if (is_late_inject) bridgeServiceName = "serial_vector"; + auto system_binder = ipc_bridge.RequestSystemServerBinder(env_, bridgeServiceName); + if (!system_binder) { + LOGE("Failed to get system server IPC binder. Aborting injection."); + SetAllowUnload(true); // Allow unload on failure. + return; + } - this->InitArtHooker(env_, init_info_); - this->InitHooks(env_); - this->SetupEntryClass(env_); + auto manager_binder = + ipc_bridge.RequestManagerBinderFromSystemServer(env_, system_binder.get()); - auto system_name = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system")); - this->FindAndCall(env_, "forkCommon", - "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_TRUE, - is_late_inject, system_name.get(), nullptr, manager_binder.get(), - is_manager_app_); + // Use either the direct manager binder if available, + // otherwise proxy through the system binder. + jobject effective_binder = manager_binder ? manager_binder.get() : system_binder.get(); - LOGI("Injected Vector framework into system_server."); - SetAllowUnload(false); // We are injected, PREVENT module unloading. + auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, effective_binder); + if (dex_fd < 0) { + LOGE("Failed to fetch framework DEX for system_server."); + SetAllowUnload(true); + return; } - void VectorModule::SetAllowUnload(bool unload) { - if (api_ && unload) { - LOGD("Allowing Zygisk to unload module library."); - api_->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); + auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, effective_binder); + ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); - // Release the pointer from the unique_ptr's control. This prevents the - // static unique_ptr's destructor from calling delete on our object, which - // would cause a double-free when the Zygisk framework cleans up. - if (instance_.release() != nullptr) { - LOGD("Module context singleton released."); - } - } else { - LOGD("Preventing Zygisk from unloading module library."); + { + PreloadedDex dex(dex_fd, dex_size); + this->LoadDex(env_, std::move(dex)); + } + close(dex_fd); + + ipc_bridge.HookBridge(env_); + + this->InitArtHooker(env_, init_info_); + this->InitHooks(env_); + this->SetupEntryClass(env_); + + auto system_name = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system")); + this->FindAndCall(env_, "forkCommon", + "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_TRUE, + is_late_inject, system_name.get(), nullptr, manager_binder.get(), + is_manager_app_); + + LOGI("Injected Vector framework into system_server."); + SetAllowUnload(false); // We are injected, PREVENT module unloading. +} + +void VectorModule::SetAllowUnload(bool unload) { + if (api_ && unload) { + LOGD("Allowing Zygisk to unload module library."); + api_->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); + + // Release the pointer from the unique_ptr's control. This prevents the + // static unique_ptr's destructor from calling delete on our object, which + // would cause a double-free when the Zygisk framework cleans up. + if (instance_.release() != nullptr) { + LOGD("Module context singleton released."); } + } else { + LOGD("Preventing Zygisk from unloading module library."); } +} } // namespace vector::native::module