From e769acf8620906b7662e5ceeb099cd22ef052a73 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 25 Jan 2026 18:55:36 +0100 Subject: [PATCH 01/24] Add native module of Vector --- native/CMakeLists.txt | 48 ++ native/README.md | 44 ++ native/include/common/config.h | 51 ++ native/include/common/logging.h | 107 +++ native/include/common/utils.h | 47 ++ native/include/core/config_bridge.h | 48 ++ native/include/core/context.h | 196 +++++ native/include/core/native_api.h | 147 ++++ native/include/elf/elf_image.h | 193 +++++ native/include/elf/symbol_cache.h | 63 ++ native/include/framework/android_types.h | 325 ++++++++ native/include/jni/jni_bridge.h | 106 +++ native/include/jni/jni_hooks.h | 24 + native/src/core/context.cpp | 140 ++++ native/src/core/native_api.cpp | 197 +++++ native/src/elf/elf_image.cpp | 377 +++++++++ native/src/elf/symbol_cache.cpp | 120 +++ native/src/jni/dex_parser_bridge.cpp | 960 +++++++++++++++++++++++ native/src/jni/hook_bridge.cpp | 575 ++++++++++++++ native/src/jni/native_api_bridge.cpp | 16 + native/src/jni/resources_hook.cpp | 307 ++++++++ 21 files changed, 4091 insertions(+) create mode 100644 native/CMakeLists.txt create mode 100644 native/README.md create mode 100644 native/include/common/config.h create mode 100644 native/include/common/logging.h create mode 100644 native/include/common/utils.h create mode 100644 native/include/core/config_bridge.h create mode 100644 native/include/core/context.h create mode 100644 native/include/core/native_api.h create mode 100644 native/include/elf/elf_image.h create mode 100644 native/include/elf/symbol_cache.h create mode 100644 native/include/framework/android_types.h create mode 100644 native/include/jni/jni_bridge.h create mode 100644 native/include/jni/jni_hooks.h create mode 100644 native/src/core/context.cpp create mode 100644 native/src/core/native_api.cpp create mode 100644 native/src/elf/elf_image.cpp create mode 100644 native/src/elf/symbol_cache.cpp create mode 100644 native/src/jni/dex_parser_bridge.cpp create mode 100644 native/src/jni/hook_bridge.cpp create mode 100644 native/src/jni/native_api_bridge.cpp create mode 100644 native/src/jni/resources_hook.cpp diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt new file mode 100644 index 000000000..c8ab37084 --- /dev/null +++ b/native/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.10) + +project(native) + +set(CMAKE_CXX_STANDARD 23) + +add_subdirectory(${VECTOR_ROOT}/external external) + +file(GLOB_RECURSE NATIVE_SOURCES "src/*.cpp") +add_library(${PROJECT_NAME} STATIC ${NATIVE_SOURCES}) + +set(IGNORED_WARNINGS + -Wno-c99-extensions + -Wno-extra-semi + -Wno-gnu-zero-variadic-macro-arguments + -Wno-gnu-flexible-array-initializer + -Wno-variadic-macros + -Wno-zero-length-array +) + +# --- Include Directories --- +# Add the 'include' directory as a PUBLIC include path. This means any +# target that links against vector_native will also inherit this include path. +target_include_directories(${PROJECT_NAME} PUBLIC include) + +# Add the 'src' directory and external XZ library includes as PRIVATE. +# These are only needed to build vector_native itself. +target_include_directories(${PROJECT_NAME} PRIVATE src ${VECTOR_ROOT}/external/xz-embedded/linux/include) + +# --- Compiler Options --- +# Apply the standard pedantic warnings and the ignored warnings list. +target_compile_options(${PROJECT_NAME} PRIVATE -Wpedantic ${IGNORED_WARNINGS}) + +# --- Library Linking --- +# Link against required external and system libraries. +# PUBLIC libraries are propagated to targets that link against this one. +# PRIVATE libraries are only used for the compilation of this target. +target_link_libraries(${PROJECT_NAME} PUBLIC + dobby_static + lsplant_static + xz_static + log # Android logging library + fmt-header-only +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + dex_builder_static +) diff --git a/native/README.md b/native/README.md new file mode 100644 index 000000000..3afe933ec --- /dev/null +++ b/native/README.md @@ -0,0 +1,44 @@ +# Vector Native Library (`native`) + +## Purpose and Design Philosophy + +This library provides low-level hooking and modification capabilities for the Android OS. +It is not a standalone application but a collection of components designed to be integrated into a larger loading mechanism, such as a Zygisk module. + +## Architectural Breakdown + +The library is organized into distinct modules, each with a clear responsibility. + +### `core` - The Abstract Engine + +This module defines the central abstractions and manages the runtime state. It's the conceptual heart of the library. + +- **`Context`**: An abstract base class that defines the injection lifecycle. It contains pure virtual methods like `LoadDex` and `SetupEntryClass`. The consumer of this library (e.g., the Zygisk module) must inherit from `Context` and provide the concrete implementations for these steps. +- **`ConfigBridge`**: A simple, native-side singleton that acts as a cache for configuration data (specifically, the obfuscation map) that is fetched and provided by the consumer. +- **`native_api`**: Implements the native module support system. It works by hooking the system's `do_dlopen` function. When it detects a registered module library being loaded, it calls that library's `native_init` entry point, providing it with a set of [API](include/core/native_api.h)s for creating its own native hooks. + +### `elf` - Symbol Resolution + +This module is responsible for runtime symbol lookups in shared libraries, a critical function for native hooking. + +- **`ElfImage`**: A parser for ELF files mapped into the current process's memory. It can resolve symbols in stripped binaries by locating, decompressing (using `xz-embedded`), and parsing the `.gnu_debugdata` section. It applies a cascading lookup strategy: GNU hash -> ELF hash -> linear scan of the symbol table. +- **`ElfSymbolCache`**: A thread-safe, lazy-initialized cache for `ElfImage` instances, providing a safe way to access common libraries like `libart.so` and the `linker`. + +### `jni` - The Business Logic Interface + +This is the most significant module and represents the library's primary service layer. It contains a set of JNI bridges that expose the core features to the injected Java framework code. The functionality here is the main product of the native library. + +- **`jni_bridge.h`**: Provides a set of helper macros (`VECTOR_NATIVE_METHOD`, `REGISTER_VECTOR_NATIVE_METHODS`, etc.) to standardize and simplify the tedious process of writing JNI boilerplate. +- **`HookBridge`**: The engine for ART method hooking. It maintains a thread-safe map of all active hooks. It also includes some stability controls, such as using atomic operations to set the backup method trampoline and throwing a Java exception instead of causing a native crash if a user tries to invoke the original method of a failed hook. +- **`ResourcesHook`**: Provides the functionality to intercept and rewrite Android's binary XML resources on the fly. It relies on non-public structures from `libandroidfw.so` and uses the `elf` module to find the necessary function symbols at runtime. +- **`DexParserBridge`**: Exposes a native DEX parser to the Java layer using a visitor pattern. This allows for analysis of an app's bytecode without the overhead of instantiating the entire DEX structure as Java objects. +- **`NativeApiBridge`**: The JNI counterpart to the `core/native_api`. It exposes a method for the Java framework to register the filenames of third-party native modules. + +### `common` & `framework` + +- **`common`**: A collection of basic utilities, including a `fmt`-based logging system, global constants, and helper functions. +- **`framework`**: Contains minimal C++ structure definitions that mirror those inside Android's internal `libandroidfw.so`. These are necessary to correctly interpret resource data pointers. + +## 3. Build System + +The library is configured with CMake to be built as a **static library (`libnative.a`)**. All external dependencies are also linked statically for maximum portability. diff --git a/native/include/common/config.h b/native/include/common/config.h new file mode 100644 index 000000000..88beeeb63 --- /dev/null +++ b/native/include/common/config.h @@ -0,0 +1,51 @@ +#pragma once + +/** + * @file config.h + * @brief Compile-time constants, version information, and platform-specific configurations. + */ + +namespace vector::native { + +[[nodiscard]] constexpr bool IsDebugBuild() { +#ifdef NDEBUG + return false; +#else + return true; +#endif +} + +/// A compile-time constant indicating if this is a debug build. +inline constexpr bool kIsDebugBuild = IsDebugBuild(); + +/** + * @def LP_SELECT(lp32, lp64) + * @brief A preprocessor macro to select a value based on the architecture. + * @param lp32 The value to use on 32-bit platforms. + * @param lp64 The value to use on 64-bit platforms. + */ +#if defined(__LP64__) +#define LP_SELECT(lp32, lp64) lp64 +#else +#define LP_SELECT(lp32, lp64) lp32 +#endif + +/// The filename of the core Android Runtime (ART) library. +inline constexpr auto kArtLibraryName = "libart.so"; + +/// The filename of the Android Binder library. +inline constexpr auto kBinderLibraryName = "libbinder.so"; + +/// The filename of the Android Framework library. +inline constexpr auto kFrameworkLibraryName = "libandroidfw.so"; + +/// The path to the dynamic linker. +inline constexpr auto kLinkerPath = "/linker"; + +/// The version code of the library, populated by the build system. +const int kVersionCode = VERSION_CODE; + +/// The version name of the library, populated by the build system. +const char *const kVersionName = VERSION_NAME; + +} // namespace vector::native diff --git a/native/include/common/logging.h b/native/include/common/logging.h new file mode 100644 index 000000000..25533e4fb --- /dev/null +++ b/native/include/common/logging.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include + +#include + +/** + * @file logging.h + * @brief Provides a lightweight logging framework using fmt. + * + */ + +/// The tag used for all log messages from this library. +#ifndef LOG_TAG +#define LOG_TAG "VectorNative" +#endif + +/** + * @def LOGV(fmt, ...) + * @brief Logs a verbose message. Compiled out in release builds. + * Includes file, line, and function information. + */ + +/** + * @def LOGD(fmt, ...) + * @brief Logs a debug message. Compiled out in release builds. + * Includes file, line, and function information. + */ + +/** + * @def LOGI(fmt, ...) + * @brief Logs an informational message. + */ + +/** + * @def LOGW(fmt, ...) + * @brief Logs a warning message. + */ + +/** + * @def LOGE(fmt, ...) + * @brief Logs an error message. + */ + +/** + * @def LOGF(fmt, ...) + * @brief Logs a fatal error message. + */ + +/** + * @def PLOGE(fmt, ...) + * @brief Logs an error message and appends the string representation of the + * current `errno` value. + */ + +#ifdef LOG_DISABLED +#define LOGV(...) ((void)0) +#define LOGD(...) ((void)0) +#define LOGI(...) ((void)0) +#define LOGW(...) ((void)0) +#define LOGE(...) ((void)0) +#define LOGF(...) ((void)0) +#define PLOGE(...) ((void)0) +#else + +namespace vector::native::detail { +template +inline void LogToAndroid(int prio, const char *tag, fmt::format_string fmt, T &&...args) { + // Using a stack-allocated buffer for performance. + std::array buf{}; + // format_to_n is safe against buffer overflows. + auto result = fmt::format_to_n(buf.data(), buf.size() - 1, fmt, std::forward(args)...); + buf[result.size] = '\0'; + __android_log_write(prio, tag, buf.data()); +} +} // namespace vector::native::detail + +#ifndef NDEBUG +#define LOGV(fmt, ...) \ + ::vector::native::detail::LogToAndroid(ANDROID_LOG_VERBOSE, LOG_TAG, "{}:{} ({}): " fmt, \ + __FILE_NAME__, __LINE__, \ + __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) +#define LOGD(fmt, ...) \ + ::vector::native::detail::LogToAndroid(ANDROID_LOG_DEBUG, LOG_TAG, "{}:{} ({}): " fmt, \ + __FILE_NAME__, __LINE__, \ + __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) +#else +#define LOGV(...) ((void)0) +#define LOGD(...) ((void)0) +#endif + +#define LOGI(fmt, ...) \ + ::vector::native::detail::LogToAndroid(ANDROID_LOG_INFO, LOG_TAG, \ + fmt __VA_OPT__(, ) __VA_ARGS__) +#define LOGW(fmt, ...) \ + ::vector::native::detail::LogToAndroid(ANDROID_LOG_WARN, LOG_TAG, \ + fmt __VA_OPT__(, ) __VA_ARGS__) +#define LOGE(fmt, ...) \ + ::vector::native::detail::LogToAndroid(ANDROID_LOG_ERROR, LOG_TAG, \ + fmt __VA_OPT__(, ) __VA_ARGS__) +#define LOGF(fmt, ...) \ + ::vector::native::detail::LogToAndroid(ANDROID_LOG_FATAL, LOG_TAG, \ + fmt __VA_OPT__(, ) __VA_ARGS__) +#define PLOGE(fmt, ...) LOGE(fmt " failed with error {}: {}", ##__VA_ARGS__, errno, strerror(errno)) + +#endif // LOG_DISABLED diff --git a/native/include/common/utils.h b/native/include/common/utils.h new file mode 100644 index 000000000..0ab4cd85b --- /dev/null +++ b/native/include/common/utils.h @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include +#include + +/** + * @file utils.h + * @brief Miscellaneous utility functions and templates for the native library. + */ + +namespace vector::native { + +/** + * @brief Converts a Java class name (dot-separated) to a JNI signature format. + * + * Example: "java.lang.String" -> "Ljava/lang/String;" + * Note: This implementation only prepends 'L' and does not append ';'. + * + * @param className The dot-separated Java class name. + * @return The class name in JNI format (e.g., "Ljava/lang/Object"). + */ +[[nodiscard]] inline std::string JavaNameToSignature(std::string className) { + std::replace(className.begin(), className.end(), '.', '/'); + return "L" + className; +} + +/** + * @brief Returns the number of elements in a statically-allocated C-style array. + * + * This is a compile-time constant. + * Attempting to use this on a pointer will result in a compilation error, + * preventing common mistakes. + * + * @tparam T The type of the array elements. + * @tparam N The size of the array. + * @param arr A reference to the array. + * @return The number of elements in the array. + */ +template +[[nodiscard]] constexpr inline size_t ArraySize(T (&)[N]) { + return N; +} + +} // namespace vector::native diff --git a/native/include/core/config_bridge.h b/native/include/core/config_bridge.h new file mode 100644 index 000000000..f050eae15 --- /dev/null +++ b/native/include/core/config_bridge.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +/** + * @file config_bridge.h + * @brief A native-side cache for configuration data, currently only the obfuscation map. + */ + +namespace vector::native { + +/** + * @class ConfigBridge + * @brief A singleton that holds configuration data. + */ +class ConfigBridge { +public: + virtual ~ConfigBridge() = default; + + ConfigBridge(const ConfigBridge &) = delete; + ConfigBridge &operator=(const ConfigBridge &) = delete; + + /** + * @brief Gets the singleton instance of the ConfigBridge. + */ + static ConfigBridge *GetInstance() { return instance_.get(); } + + /** + * @brief Releases ownership of the singleton instance. + */ + static std::unique_ptr ReleaseInstance() { return std::move(instance_); } + + /// Gets a reference to the obfuscation map. + virtual std::map &obfuscation_map() = 0; + + /// Sets the obfuscation map. + virtual void obfuscation_map(std::map map) = 0; + +protected: + ConfigBridge() = default; + + /// The singleton instance, managed alongside the main Context. + static std::unique_ptr instance_; +}; + +} // namespace vector::native diff --git a/native/include/core/context.h b/native/include/core/context.h new file mode 100644 index 000000000..230a05460 --- /dev/null +++ b/native/include/core/context.h @@ -0,0 +1,196 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "common/logging.h" + +/** + * @file context.h + * @brief Defines the core runtime context for the Vector native environment. + * + * The Context class is a singleton that holds essential runtime information, such as + * the injected class loader, and provides core functionalities like class finding and DEX loading. + * It serves as the central hub for native operations. + */ + +namespace vector::native { + +// Forward declaration from another module. +class ConfigBridge; + +/** + * @class Context + * @brief Manages the global state and core operations of the native library. + * + * This singleton is responsible for initializing hooks, managing DEX files, + * and providing access to the application's class loader. + * It orchestrates the setup process when the library is loaded into the target application. + */ +class Context { +public: + Context(const Context &) = delete; + Context &operator=(const Context &) = delete; + + /** + * @brief Gets the singleton instance of the Context. + * @return A pointer to the global Context instance. + */ + static Context *GetInstance(); + + /** + * @brief Releases ownership of the singleton instance. + * + * This is typically used during shutdown to clean up resources. + * After this call, GetInstance() will return nullptr until a new instance is created. + * + * @return A unique_ptr owning the Context instance. + */ + static std::unique_ptr ReleaseInstance(); + + /** + * @brief Gets the class loader used for injecting framework classes. + * @return A global JNI reference to the class loader. + */ + [[nodiscard]] jobject GetCurrentClassLoader() const { return inject_class_loader_; } + + /** + * @brief Finds a class using the framework's injected class loader. + * + * This is the primary method for looking up classes that are part of the + * Vector framework's Java components. + * + * @param env The JNI environment. + * @param class_name The fully qualified name of the class to find (dot-separated). + * @return A ScopedLocalRef containing the jclass object, or nullptr if not found. + */ + [[nodiscard]] lsplant::ScopedLocalRef FindClassFromCurrentLoader( + JNIEnv *env, std::string_view class_name) const { + return FindClassFromLoader(env, GetCurrentClassLoader(), class_name); + } + + virtual ~Context() = default; + +protected: + /** + * @class PreloadedDex + * @brief Manages a memory-mapped DEX file. + * + * This helper class handles the mapping of a DEX file from a file descriptor + * into memory and ensures it is unmapped upon destruction. + */ + class PreloadedDex { + public: + PreloadedDex() : addr_(nullptr), size_(0) {} + PreloadedDex(const PreloadedDex &) = delete; + PreloadedDex &operator=(const PreloadedDex &) = delete; + + /** + * @brief Memory-maps a DEX file from a file descriptor. + * @param fd The file descriptor of the DEX file. + * @param size The size of the file. + */ + PreloadedDex(int fd, size_t size); + + PreloadedDex(PreloadedDex &&other) noexcept : addr_(other.addr_), size_(other.size_) { + other.addr_ = nullptr; + other.size_ = 0; + } + + PreloadedDex &operator=(PreloadedDex &&other) noexcept { + if (this != &other) { + if (addr_) { + munmap(addr_, size_); + } + addr_ = other.addr_; + size_ = other.size_; + other.addr_ = nullptr; + other.size_ = 0; + } + return *this; + } + + ~PreloadedDex(); + + /// Checks if the DEX file was successfully mapped. + explicit operator bool() const { return addr_ != nullptr && size_ > 0; } + /// Returns the size of the mapped DEX data. + [[nodiscard]] auto size() const { return size_; } + /// Returns a pointer to the beginning of the mapped DEX data. + [[nodiscard]] auto data() const { return addr_; } + + private: + void *addr_; + size_t size_; + }; + + Context() = default; + + /** + * @brief Finds a class from a specific class loader instance. + * @param env The JNI environment. + * @param class_loader The class loader to use for the lookup. + * @param class_name The name of the class to find. + * @return A ScopedLocalRef containing the jclass, or nullptr if not found. + */ + static lsplant::ScopedLocalRef FindClassFromLoader(JNIEnv *env, jobject class_loader, + std::string_view class_name); + + /** + * @brief Finds and calls a static void method on the framework's entry class. + * + * A utility for internal communication between the native and Java layers. + * + * @tparam Args Argument types for the method call. + * @param env The JNI environment. + * @param method_name The name of the static method. + * @param method_sig The JNI signature of the method. + * @param args The arguments to pass to the method. + */ + template + void FindAndCall(JNIEnv *env, std::string_view method_name, std::string_view method_sig, + Args &&...args) const { + if (!entry_class_) { + LOGE("Cannot call method '{}', entry class is null", method_name.data()); + return; + } + jmethodID mid = lsplant::JNI_GetStaticMethodID(env, entry_class_, method_name, method_sig); + if (mid) { + env->CallStaticVoidMethod(entry_class_, mid, + lsplant::UnwrapScope(std::forward(args))...); + } else { + LOGE("Static method '{}' with signature '{}' not found", method_name.data(), + method_sig.data()); + } + } + + // --- Virtual methods for platform-specific implementations --- + + /// Initializes the ART hooking framework (LSPlant). + virtual void InitArtHooker(JNIEnv *env, const lsplant::InitInfo &initInfo); + + /// Registers all necessary JNI bridges and native hooks. + virtual void InitHooks(JNIEnv *env); + + /// Loads a DEX file into the target application. + virtual void LoadDex(JNIEnv *env, PreloadedDex &&dex) = 0; + + /// Sets up the main entry class for native-to-Java calls. + virtual void SetupEntryClass(JNIEnv *env) = 0; + +protected: + /// The singleton instance of the Context. + static std::unique_ptr instance_; + + /// Global reference to the classloader used to load the framework. + jobject inject_class_loader_ = nullptr; + + /// Global reference to the primary entry point class in the Java framework. + jclass entry_class_ = nullptr; +}; + +} // namespace vector::native diff --git a/native/include/core/native_api.h b/native/include/core/native_api.h new file mode 100644 index 000000000..1a97e82de --- /dev/null +++ b/native/include/core/native_api.h @@ -0,0 +1,147 @@ +#pragma once + +#include +#include + +#include +#include + +#include "common/config.h" +#include "common/logging.h" + +/** + * @file native_api.h + * @brief Manages the native module ecosystem and provides a stable API for them. + * + * This component is responsible for hooking the dynamic library loader (`dlopen`) to + * detect when registered native modules are loaded. + * It then provides these modules with a set of function pointers for + * interacting with the Vector core, primarily for creating native hooks. + */ + +// NOTE: The following type definitions form a public ABI for native modules. +// Do not change them without careful consideration for backward compatibility. + +/* + * ========================================================================================= + * Vector Native API Interface + * ========================================================================================= + * + * This following function types and data structures allow a native library (your module) to + * interface with the Vector framework. + * The core idea is that Vector provides a set of powerful tools (like function hooking), + * and your module consumes these tools through a well-defined entry point. + * + * The interaction flow is as follows: + * + * 1. Vector intercepts the loading of your native library (e.g., libnative.so). + * 2. Vector looks for and calls the `native_init` function within your library. + * 3. Vector passes a `NativeAPIEntries` struct to your `native_init`, + * which contains function pointers to Vector's hooking + * and unhooking implementations (powered by Dobby). + * 4. Your `native_init` function saves these function pointers for later use + * and returns a callback function (`NativeOnModuleLoaded`). + * 5. Vector will then invoke your returned callback every time + * a new native library is loaded into the target process, + * allowing you to perform "late" hooks on specific libraries. + * + * + * Initialization Flow + * + * Vector Framework Your Native Module (e.g., libnative.so) + * ----------------- ------------------------------------- + * + * | | + * [ Intercepts dlopen("libnative.so") ] | + * | | + * |----------> [ Finds & Calls native_init() ] | + * | | + * [ Passes NativeAPIEntries* ] ---> [ Stores function pointers ] + * (Contains hook/unhook funcs) | + * | | + * | | + * | <-----------[ Returns `NativeOnModuleLoaded` callback ] + * | | + * | | + * [ Stores your callback ] | + * | | + * + */ + +// Function pointer type for a native hooking implementation. +using HookFunType = int (*)(void *func, void *replace, void **backup); + +// Function pointer type for a native unhooking implementation. +using UnhookFunType = int (*)(void *func); + +// Callback function pointer that modules receive, invoked when any library is loaded. +using NativeOnModuleLoaded = void (*)(const char *name, void *handle); + +/** + * @struct NativeAPIEntries + * @brief A struct containing function pointers exposed to native modules. + */ +struct NativeAPIEntries { + uint32_t version; // The version of this API struct. + HookFunType hookFunc; // Pointer to the function for inline hooking. + UnhookFunType unhookFunc; // Pointer to the function for unhooking. +}; + +// NOTE: Module developers should not include the following INTERNAL definitions. + +namespace vector::native { + +// The entry point function that native modules must export (`native_init`). +using NativeInit = NativeOnModuleLoaded (*)(const NativeAPIEntries *entries); + +/** + * @brief Installs the hooks required for the native API to function. + * @param handler The LSPlant hook handler. + * @return True on success, false on failure. + */ +bool InstallNativeAPI(const lsplant::HookHandler &handler); + +/** + * @brief Registers a native library by its filename for module initialization. + * + * When a library with a matching filename is loaded via `dlopen`, the runtime will attempt to + * initialize it as a native module by calling its `native_init` function. + * + * @param library_name The filename of the native module's .so file (e.g., "libmymodule.so"). + */ +void RegisterNativeLib(const std::string &library_name); + +/** + * @brief A wrapper around DobbyHook. + */ +inline int HookInline(void *original, void *replace, void **backup) { + if constexpr (kIsDebugBuild) { + Dl_info info; + if (dladdr(original, &info)) { + LOGD("Dobby hooking {} ({}) from {} ({})", + info.dli_sname ? info.dli_sname : "(unknown symbol)", + info.dli_saddr ? info.dli_saddr : original, + info.dli_fname ? info.dli_fname : "(unknown file)", info.dli_fbase); + } + } + return DobbyHook(original, reinterpret_cast(replace), + reinterpret_cast(backup)); +} + +/** + * @brief A wrapper around DobbyDestroy. + */ +inline int UnhookInline(void *original) { + if constexpr (kIsDebugBuild) { + Dl_info info; + if (dladdr(original, &info)) { + LOGD("Dobby unhooking {} ({}) from {} ({})", + info.dli_sname ? info.dli_sname : "(unknown symbol)", + info.dli_saddr ? info.dli_saddr : original, + info.dli_fname ? info.dli_fname : "(unknown file)", info.dli_fbase); + } + } + return DobbyDestroy(original); +} + +} // namespace vector::native diff --git a/native/include/elf/elf_image.h b/native/include/elf/elf_image.h new file mode 100644 index 000000000..9cf19bd39 --- /dev/null +++ b/native/include/elf/elf_image.h @@ -0,0 +1,193 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +/** + * @file elf_image.h + * @brief Defines the ElfImage class for parsing ELF files from memory. + * + * This utility can find the base address of a loaded shared library, parse its ELF headers, and + * look up symbol addresses using various methods (GNU hash, ELF hash, and linear search). + * + * It handles stripped ELF files by decompressing and parsing the `.gnu_debugdata` section. + */ + +namespace vector::native { + +/** + * @class ElfImage + * @brief Represents a loaded ELF shared library in the current process. + * + * An ElfImage instance is created with the filename of a library (e.g., "libart.so"). + * It automatically finds the library's base address in memory by parsing `/proc/self/maps` and + * then memory-maps the ELF file from disk to parse its headers. + */ +class ElfImage { +public: + /** + * @brief Constructs an ElfImage for a given shared library. + * @param lib_name The filename of the library (e.g., "libart.so", "/linker"). + */ + explicit ElfImage(std::string_view lib_name); + ~ElfImage(); + + // Disable copy and assignment to prevent accidental slicing and resource mismanagement. + ElfImage(const ElfImage &) = delete; + ElfImage &operator=(const ElfImage &) = delete; + + /** + * @brief Finds the memory address of a symbol by its name. + * + * This method attempts to resolve a symbol's address using, in order: + * 1. The GNU hash table (.gnu.hash) for fast lookups. + * 2. The standard ELF hash table (.hash) as a fallback. + * 3. A linear search through the full symbol table (.symtab). + * + * @tparam T The desired pointer type (e.g., `void*`, `int (*)(...)`). + * @param name The name of the symbol to find. + * @return The absolute memory address of the symbol, or nullptr if not found. + */ + template + requires(std::is_pointer_v) + const T getSymbAddress(std::string_view name) const { + // Pre-calculate hashes for efficiency. + auto gnu_hash = GnuHash(name); + auto elf_hash = ElfHash(name); + auto offset = getSymbOffset(name, gnu_hash, elf_hash); + if (offset > 0 && base_ != nullptr) { + // The final address is: base_address + symbol_offset - load_bias + return reinterpret_cast(reinterpret_cast(base_) + offset - bias_); + } + return nullptr; + } + + /** + * @brief Finds the first symbol whose name starts with the given prefix. + * + * This is useful for finding symbols when the exact name is unknown, such as mangled C++ + * symbols. This search is performed only on the full symbol table (.symtab) and may be slow. + * + * @tparam T The desired pointer type. + * @param prefix The prefix to search for. + * @return The address of the first matching symbol, or nullptr if none is found. + */ + template + requires(std::is_pointer_v) + const T getSymbPrefixFirstAddress(std::string_view prefix) const { + auto offset = prefixLookupFirst(prefix); + if (offset > 0 && base_ != nullptr) { + return reinterpret_cast(reinterpret_cast(base_) + offset - bias_); + } + return nullptr; + } + + /** + * @brief Checks if the ELF image was successfully loaded and parsed. + * @return True if the image is valid, false otherwise. + */ + [[nodiscard]] bool IsValid() const { return base_ != nullptr; } + + /** + * @brief Returns the canonical path of the loaded library, as found in /proc/self/maps. + */ + [[nodiscard]] const std::string &GetPath() const { return path_; } + +private: + // Finds the base address of the library in the current process's memory map. + bool findModuleBase(); + // Parses the main ELF headers from a given header pointer. + void parseHeaders(ElfW(Ehdr) * header); + // Decompresses the .gnu_debugdata section if it exists. + bool decompressGnuDebugData(); + + // Looks up a symbol offset using the ELF hash table. + ElfW(Addr) elfLookup(std::string_view name, uint32_t hash) const; + // Looks up a symbol offset using the GNU hash table. + ElfW(Addr) gnuLookup(std::string_view name, uint32_t hash) const; + // Looks up a symbol offset via a linear scan of the .symtab section. + ElfW(Addr) linearLookup(std::string_view name) const; + // Finds all symbol offsets with a given name via a linear scan. + std::vector linearRangeLookup(std::string_view name) const; + // Finds the first symbol offset whose name starts with the given prefix. + ElfW(Addr) prefixLookupFirst(std::string_view prefix) const; + + // Gets a symbol's offset from the start of the file. + ElfW(Addr) getSymbOffset(std::string_view name, uint32_t gnu_hash, uint32_t elf_hash) const; + + // Lazily initializes the map for linear symbol lookups from the .symtab section. + void ensureLinearMapInitialized() const; + + // Calculates the standard ELF hash for a symbol name. + [[nodiscard]] static constexpr uint32_t ElfHash(std::string_view name); + // Calculates the GNU hash for a symbol name. + [[nodiscard]] static constexpr uint32_t GnuHash(std::string_view name); + + // --- Member Variables --- + + std::string path_; + void *base_ = nullptr; + void *file_map_ = nullptr; + size_t file_size_ = 0; + ElfW(Addr) bias_ = 0; + + // Pointers into the mapped ELF file data. + ElfW(Ehdr) *header_ = nullptr; + ElfW(Shdr) *dynsym_ = nullptr; + ElfW(Sym) *dynsym_start_ = nullptr; + const char *strtab_start_ = nullptr; // Note: const char* is safer. + + // ELF hash section fields + uint32_t nbucket_ = 0; + uint32_t *bucket_ = nullptr; + uint32_t *chain_ = nullptr; + + // GNU hash section fields + uint32_t gnu_nbucket_ = 0; + uint32_t gnu_symndx_ = 0; + uint32_t gnu_bloom_size_ = 0; + uint32_t gnu_shift2_ = 0; + uintptr_t *gnu_bloom_filter_ = nullptr; + uint32_t *gnu_bucket_ = nullptr; + uint32_t *gnu_chain_ = nullptr; + + // For stripped binaries with .gnu_debugdata + std::string elf_debugdata_; + ElfW(Ehdr) *header_debugdata_ = nullptr; + ElfW(Sym) *symtab_start_ = nullptr; + ElfW(Off) symtab_count_ = 0; + const char *symtab_str_start_ = nullptr; + + // Lazily-initialized map for fast linear lookups. + // `mutable` allows init in const methods. + mutable std::map symtabs_; +}; + +// --- Inlined Hash Function Implementations --- + +constexpr uint32_t ElfImage::ElfHash(std::string_view name) { + uint32_t h = 0, g; + for (unsigned char p : name) { + h = (h << 4) + p; + if ((g = h & 0xf0000000) != 0) { + h ^= g >> 24; + } + h &= ~g; + } + return h; +} + +constexpr uint32_t ElfImage::GnuHash(std::string_view name) { + uint32_t h = 5381; + for (unsigned char p : name) { + h = (h << 5) + h + p; // h * 33 + p + } + return h; +} + +} // namespace vector::native diff --git a/native/include/elf/symbol_cache.h b/native/include/elf/symbol_cache.h new file mode 100644 index 000000000..8a1152ee2 --- /dev/null +++ b/native/include/elf/symbol_cache.h @@ -0,0 +1,63 @@ +#pragma once + +/** + * @file symbol_cache.h + * @brief Provides a thread-safe, lazy-initialized cache for commonly used ElfImage objects. + * + * This avoids the cost of repeatedly parsing the ELF files for libart, libbinder, + * and the linker during runtime. + */ + +namespace vector::native { + +// Forward declaration +class ElfImage; + +/** + * @class ElfSymbolCache + * @brief A singleton cache for frequently accessed system library ELF images. + * + * All methods are static and guarantee thread-safe, one-time initialization + * of the underlying ElfImage objects. + */ +class ElfSymbolCache { +public: + /** + * @brief Gets the cached ElfImage for the ART library (libart.so). + * @return A const pointer to the ElfImage, or nullptr if it could not be loaded. + */ + static const ElfImage *GetArt(); + + /** + * @brief Gets the cached ElfImage for the Binder library (libbinder.so). + * @return A const pointer to the ElfImage, or nullptr if it could not be loaded. + */ + static const ElfImage *GetLibBinder(); + + /** + * @brief Gets the cached ElfImage for the dynamic linker. + * @return A const pointer to the ElfImage, or nullptr if it could not be loaded. + */ + static const ElfImage *GetLinker(); + + /** + * @brief Clears the cache for a specific ElfImage object. + * + * If the provided pointer matches one of the cached images, that specific cache entry will be cleared, + * forcing a reload on the next `Get...()` call for that library. + * If the pointer does not match any cached image, this function does nothing. + * + * @param image_to_clear A pointer to the cached ElfImage to be removed. + */ + static bool ClearCache(const ElfImage *image_to_clear); + + /** + * @brief Clears the cache, releasing all ElfImage objects. + * + * This is primarily for testing or specific shutdown scenarios. + * After this call, the next call to a Get...() method will reload the library from scratch. + */ + static void ClearCache(); +}; + +} // namespace vector::native diff --git a/native/include/framework/android_types.h b/native/include/framework/android_types.h new file mode 100644 index 000000000..1e2df2e92 --- /dev/null +++ b/native/include/framework/android_types.h @@ -0,0 +1,325 @@ +#pragma once + +#include +#include + +#include "utils/hook_helper.hpp" + +using lsplant::operator""_sym; + +// References: +// https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h +namespace android { + +typedef int32_t status_t; + +template +struct unexpected { + E val_; +}; + +template +struct expected { + using value_type = T; + using error_type = E; + using unexpected_type = unexpected; + std::variant var_; + + constexpr bool has_value() const noexcept { return var_.index() == 0; } + + constexpr const T &value() const & { return std::get(var_); } + + constexpr T &value() & { return std::get(var_); } + + constexpr const T *operator->() const { return std::addressof(value()); } + + constexpr T *operator->() { return std::addressof(value()); } +}; + +enum class IOError { + // Used when reading a file residing on an IncFs file-system times out. + PAGES_MISSING = -1, +}; + +template +struct BasicStringPiece { + const TChar *data_; + size_t length_; +}; + +using NullOrIOError = std::variant; + +using StringPiece16 = BasicStringPiece; + +enum { + RES_NULL_TYPE = 0x0000, + RES_STRING_POOL_TYPE = 0x0001, + RES_TABLE_TYPE = 0x0002, + RES_XML_TYPE = 0x0003, + // Chunk types in RES_XML_TYPE + RES_XML_FIRST_CHUNK_TYPE = 0x0100, + RES_XML_START_NAMESPACE_TYPE = 0x0100, + RES_XML_END_NAMESPACE_TYPE = 0x0101, + RES_XML_START_ELEMENT_TYPE = 0x0102, + RES_XML_END_ELEMENT_TYPE = 0x0103, + RES_XML_CDATA_TYPE = 0x0104, + RES_XML_LAST_CHUNK_TYPE = 0x017f, + // This contains a uint32_t array mapping strings in the string + // pool back to resource identifiers. It is optional. + RES_XML_RESOURCE_MAP_TYPE = 0x0180, + // Chunk types in RES_TABLE_TYPE + RES_TABLE_PACKAGE_TYPE = 0x0200, + RES_TABLE_TYPE_TYPE = 0x0201, + RES_TABLE_TYPE_SPEC_TYPE = 0x0202, + RES_TABLE_LIBRARY_TYPE = 0x0203 +}; + +struct ResXMLTree_node { + void *header; + // Line number in original source file at which this element appeared. + uint32_t lineNumber; + // Optional XML comment that was associated with this element; -1 if none. + void *comment; +}; + +class ResXMLTree; + +class ResXMLParser { +public: + enum event_code_t { + BAD_DOCUMENT = -1, + START_DOCUMENT = 0, + END_DOCUMENT = 1, + + FIRST_CHUNK_CODE = RES_XML_FIRST_CHUNK_TYPE, + + START_NAMESPACE = RES_XML_START_NAMESPACE_TYPE, + END_NAMESPACE = RES_XML_END_NAMESPACE_TYPE, + START_TAG = RES_XML_START_ELEMENT_TYPE, + END_TAG = RES_XML_END_ELEMENT_TYPE, + TEXT = RES_XML_CDATA_TYPE + }; + + const ResXMLTree &mTree; + event_code_t mEventCode; + const ResXMLTree_node *mCurNode; + const void *mCurExt; +}; + +class ResStringPool { +public: + status_t mError; + void *mOwnedData; + const void *mHeader; + size_t mSize; + mutable pthread_mutex_t mDecodeLock; + const uint32_t *mEntries; + const uint32_t *mEntryStyles; + const void *mStrings; + char16_t mutable **mCache; + uint32_t mStringPoolSize; // number of uint16_t + const uint32_t *mStyles; + uint32_t mStylePoolSize; // number of uint32_t + + using stringAtRet = expected; + + inline static auto stringAtS_ = ("_ZNK7android13ResStringPool8stringAtEjPj"_sym | + "_ZNK7android13ResStringPool8stringAtEmPm"_sym) + .as; + + inline static auto stringAt_ = ("_ZNK7android13ResStringPool8stringAtEj"_sym | + "_ZNK7android13ResStringPool8stringAtEm"_sym) + .as; + + StringPiece16 stringAt(size_t idx) const { + if (stringAt_) { + size_t len; + const char16_t *str = stringAt_(const_cast(this), idx, &len); + return {str, len}; + } else if (stringAtS_) { + auto str = stringAtS_(const_cast(this), idx); + if (str.has_value()) { + return {str->data_, str->length_}; + } + } + return {nullptr, 0u}; + } + + static bool setup(const lsplant::HookHandler &handler) { + return handler(stringAt_) || handler(stringAtS_); + } +}; + +class ResXMLTree : public ResXMLParser { +public: + void *mDynamicRefTable; + status_t mError; + void *mOwnedData; + const void *mHeader; + size_t mSize; + const uint8_t *mDataEnd; + ResStringPool mStrings; + const uint32_t *mResIds; + size_t mNumResIds; + const ResXMLTree_node *mRootNode; + const void *mRootExt; + event_code_t mRootCode; +}; + +struct ResStringPool_ref { + // Index into the string pool table at which + // to find the location of the string data in the pool. + // (uint32_t-offset from the indices immediately after ResStringPool_header) + uint32_t index; +}; + +struct ResXMLTree_attrExt { + // String of the full namespace of this element. + struct ResStringPool_ref ns; + + // String name of this node if it is an ELEMENT; the raw character data if this is a CDATA node. + struct ResStringPool_ref name; + + // Byte offset from the start of this structure where the attributes start. + uint16_t attributeStart; + + // Size of the ResXMLTree_attribute structures that follow. + uint16_t attributeSize; + + // Number of attributes associated with an ELEMENT. + // These are available as an array of ResXMLTree_attribute structures + // immediately following this node. + uint16_t attributeCount; + + // Index (1-based) of the "id" attribute. 0 if none. + uint16_t idIndex; + + // Index (1-based) of the "class" attribute. 0 if none. + uint16_t classIndex; + + // Index (1-based) of the "style" attribute. 0 if none. + uint16_t styleIndex; +}; + +struct Res_value { + // Number of bytes in this structure. + uint16_t size; + // Always set to 0. + uint8_t res0; + + // Type of the data value. + enum : uint8_t { + // The 'data' is either 0 or 1, specifying this resource is + // either undefined or empty, respectively. + TYPE_NULL = 0x00, + // The 'data' holds a ResTable_ref, a reference to another resource table entry. + TYPE_REFERENCE = 0x01, + // The 'data' holds an attribute resource identifier. + TYPE_ATTRIBUTE = 0x02, + // The 'data' holds an index into the containing resource table's global value string pool. + TYPE_STRING = 0x03, + // The 'data' holds a single-precision floating point number. + TYPE_FLOAT = 0x04, + // The 'data' holds a complex number encoding a dimension value, such as "100in". + TYPE_DIMENSION = 0x05, + // The 'data' holds a complex number encoding a fraction of a container. + TYPE_FRACTION = 0x06, + // The 'data' holds a dynamic ResTable_ref, + // which needs to be resolved before it can be used like a TYPE_REFERENCE. + TYPE_DYNAMIC_REFERENCE = 0x07, + // The 'data' holds an attribute resource identifier, + // which needs to be resolved before it can be used like a TYPE_ATTRIBUTE. + TYPE_DYNAMIC_ATTRIBUTE = 0x08, + // Beginning of integer flavors... + TYPE_FIRST_INT = 0x10, + // The 'data' is a raw integer value of the form n..n. + TYPE_INT_DEC = 0x10, + // The 'data' is a raw integer value of the form 0xn..n. + TYPE_INT_HEX = 0x11, + // The 'data' is either 0 or 1, for input "false" or "true" respectively. + TYPE_INT_BOOLEAN = 0x12, + // Beginning of color integer flavors... + TYPE_FIRST_COLOR_INT = 0x1c, + // The 'data' is a raw integer value of the form #aarrggbb. + TYPE_INT_COLOR_ARGB8 = 0x1c, + // The 'data' is a raw integer value of the form #rrggbb. + TYPE_INT_COLOR_RGB8 = 0x1d, + // The 'data' is a raw integer value of the form #argb. + TYPE_INT_COLOR_ARGB4 = 0x1e, + // The 'data' is a raw integer value of the form #rgb. + TYPE_INT_COLOR_RGB4 = 0x1f, + // ...end of integer flavors. + TYPE_LAST_COLOR_INT = 0x1f, + // ...end of integer flavors. + TYPE_LAST_INT = 0x1f + }; + uint8_t dataType; + // Structure of complex data values (TYPE_UNIT and TYPE_FRACTION) + enum { + // Where the unit type information is. + // This gives us 16 possible types, as defined below. + COMPLEX_UNIT_SHIFT = 0, + COMPLEX_UNIT_MASK = 0xf, + // TYPE_DIMENSION: Value is raw pixels. + COMPLEX_UNIT_PX = 0, + // TYPE_DIMENSION: Value is Device Independent Pixels. + COMPLEX_UNIT_DIP = 1, + // TYPE_DIMENSION: Value is a Scaled device independent Pixels. + COMPLEX_UNIT_SP = 2, + // TYPE_DIMENSION: Value is in points. + COMPLEX_UNIT_PT = 3, + // TYPE_DIMENSION: Value is in inches. + COMPLEX_UNIT_IN = 4, + // TYPE_DIMENSION: Value is in millimeters. + COMPLEX_UNIT_MM = 5, + // TYPE_FRACTION: A basic fraction of the overall size. + COMPLEX_UNIT_FRACTION = 0, + // TYPE_FRACTION: A fraction of the parent size. + COMPLEX_UNIT_FRACTION_PARENT = 1, + + // Where the radix information is, telling where the decimal place appears in the mantissa. + // This give us 4 possible fixed point representations as defined below. + COMPLEX_RADIX_SHIFT = 4, + COMPLEX_RADIX_MASK = 0x3, + // The mantissa is an integral number -- i.e., 0xnnnnnn.0 + COMPLEX_RADIX_23p0 = 0, + // The mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn + COMPLEX_RADIX_16p7 = 1, + // The mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn + COMPLEX_RADIX_8p15 = 2, + // The mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn + COMPLEX_RADIX_0p23 = 3, + + // Where the actual value is. + // This gives us 23 bits of precision. + // The top bit is the sign. + COMPLEX_MANTISSA_SHIFT = 8, + COMPLEX_MANTISSA_MASK = 0xffffff + }; + // Possible data values for TYPE_NULL. + enum { + // The value is not defined. + DATA_NULL_UNDEFINED = 0, + // The value is explicitly defined as empty. + DATA_NULL_EMPTY = 1 + }; + // The data for this item, as interpreted according to dataType. + typedef uint32_t data_type; + data_type data; +}; + +struct ResXMLTree_attribute { + // Namespace of this attribute. + struct ResStringPool_ref ns; + + // Name of this attribute. + struct ResStringPool_ref name; + + // The original raw string value of this attribute. + struct ResStringPool_ref rawValue; + + // Processesd typed value of this attribute. + struct Res_value typedValue; +}; + +} // namespace android diff --git a/native/include/jni/jni_bridge.h b/native/include/jni/jni_bridge.h new file mode 100644 index 000000000..02bd2cade --- /dev/null +++ b/native/include/jni/jni_bridge.h @@ -0,0 +1,106 @@ +#pragma once + +#include + +#include "common/logging.h" +#include "common/utils.h" +#include "core/config_bridge.h" +#include "core/context.h" + +/** + * @file jni_bridge.h + * @brief Provides essential macros and helper functions for creating JNI bridges. + * + */ + +namespace vector::native::jni { + +/** + * @brief A helper function to get the obfuscated native bridge class signature prefix. + * + * It reads the obfuscation map to find the correct, potentially obfuscated, + * package name for the native bridge classes. + * + * @return The JNI signature prefix (e.g., "org/matrix/vector/nativebridge/"). + */ +inline std::string GetNativeBridgeSignature() { + auto *bridge = ConfigBridge::GetInstance(); + if (bridge) { + const auto &obfs_map = bridge->obfuscation_map(); + // The key must match what the Java build script places in the map. + auto it = obfs_map.find("org.matrix.vector.nativebridge."); + if (it != obfs_map.end()) { + return it->second; + } + } + // Fallback or default value if not found. + return "org/matrix/vector/nativebridge/"; +} + +/** + * @brief Internal implementation for registering native methods. + * + * Finds the target class using the framework's class loader and calls JNI's RegisterNatives. + */ +[[gnu::always_inline]] +inline bool RegisterNativeMethodsInternal(JNIEnv *env, std::string_view class_name, + const JNINativeMethod *methods, jint method_count) { + auto *context = Context::GetInstance(); + if (!context) { + LOGF("Cannot register natives for '{}', Context is null.", class_name.data()); + return false; + } + auto clazz = context->FindClassFromCurrentLoader(env, class_name); + if (clazz.get() == nullptr) { + LOGF("JNI class not found: {}", class_name.data()); + return false; + } + return env->RegisterNatives(clazz.get(), methods, method_count) == JNI_OK; +} + +// A helper cast for the native method function pointers. +#define VECTOR_JNI_CAST(to) reinterpret_cast + +/** + * @def VECTOR_NATIVE_METHOD(className, functionName, signature) + * @brief Defines a JNINativeMethod entry. + * + * This macro constructs a JNINativeMethod struct, automatically + * creating the mangled C-style function name that JNI expects. + * + * @param className The simple name of the Java class (e.g., "HookBridge"). + * @param functionName The name of the Java method (e.g., "hookMethod"). + * @param signature The JNI signature of the method (e.g., "(I)V"). + */ +#define VECTOR_NATIVE_METHOD(className, functionName, signature) \ + {#functionName, signature, \ + VECTOR_JNI_CAST(void *)(Java_org_matrix_vector_nativebridge_##className##_##functionName)} + +/** + * @def JNI_START + * @brief Defines the standard first two arguments for any JNI native method implementation. + */ +#define JNI_START [[maybe_unused]] JNIEnv *env, [[maybe_unused]] jclass clazz + +/** + * @def VECTOR_DEF_NATIVE_METHOD(ret, className, functionName, ...) + * @brief Defines the function signature for a JNI native method implementation. + * + * This macro creates the full C++ function definition with + * the correct JNI name-mangling convention. + */ +#define VECTOR_DEF_NATIVE_METHOD(ret, className, functionName, ...) \ + extern "C" JNIEXPORT ret JNICALL \ + Java_org_matrix_vector_nativebridge_##className##_##functionName(JNI_START, ##__VA_ARGS__) + +/** + * @def REGISTER_VECTOR_NATIVE_METHODS(class_name) + * @brief Registers all methods defined in the `gMethods` array for a given class. + * + * This is the final step in linking the C++ implementations to the Java native methods. + */ +#define REGISTER_VECTOR_NATIVE_METHODS(class_name) \ + RegisterNativeMethodsInternal(env, GetNativeBridgeSignature() + #class_name, gMethods, \ + ArraySize(gMethods)) + +} // namespace vector::native::jni diff --git a/native/include/jni/jni_hooks.h b/native/include/jni/jni_hooks.h new file mode 100644 index 000000000..703ae0114 --- /dev/null +++ b/native/include/jni/jni_hooks.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +/** + * @file jni_hooks.h + * @brief Declares the registration functions for all JNI bridge modules. + */ + +namespace vector::native::jni { + +/// Registers the JNI methods for the DexParserBridge. +void RegisterDexParserBridge(JNIEnv *env); + +/// Registers the JNI methods for the HookBridge. +void RegisterHookBridge(JNIEnv *env); + +/// Registers the JNI methods for the NativeApiBridge. +void RegisterNativeApiBridge(JNIEnv *env); + +/// Registers the JNI methods for the ResourcesHook bridge. +void RegisterResourcesHook(JNIEnv *env); + +} // namespace vector::native::jni diff --git a/native/src/core/context.cpp b/native/src/core/context.cpp new file mode 100644 index 000000000..2db2e1032 --- /dev/null +++ b/native/src/core/context.cpp @@ -0,0 +1,140 @@ +#include "core/context.h" + +#include "core/config_bridge.h" +#include "jni/jni_hooks.h" + +namespace vector::native { + +// Instantiate the singleton pointers for Context and ConfigBridge. +std::unique_ptr Context::instance_; +std::unique_ptr ConfigBridge::instance_; + +Context *Context::GetInstance() { return instance_.get(); } + +std::unique_ptr Context::ReleaseInstance() { return std::move(instance_); } + +Context::PreloadedDex::PreloadedDex(int fd, size_t size) { + LOGD("Mapping PreloadedDex: fd={}, size={}", fd, size); + void *addr = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0); + + if (addr != MAP_FAILED) { + addr_ = addr; + size_ = size; + } else { + addr_ = nullptr; + size_ = 0; + PLOGE("Failed to mmap dex file"); + } +} + +Context::PreloadedDex::~PreloadedDex() { + if (addr_ && size_ > 0) { + munmap(addr_, size_); + } +} + +void Context::InitArtHooker(JNIEnv *env, const lsplant::InitInfo &initInfo) { + if (!lsplant::Init(env, initInfo)) { + LOGE("Failed to initialize LSPlant hooking framework."); + } +} + +void Context::InitHooks(JNIEnv *env) { + // ------------------------------------------------------------------------- + // DEX Privilege Elevation + // ------------------------------------------------------------------------- + // We traverse the DexPathList of the injected ClassLoader to access the + // underlying 'mCookie' for every loaded DEX file. + // The cookie provides a handle to the native C++ DexFile object in ART memory. + + // Retrieve the DexPathList object (holds the array of DEX elements). + auto path_list = lsplant::JNI_GetObjectFieldOf(env, inject_class_loader_, "pathList", + "Ldalvik/system/DexPathList;"); + if (!path_list) { + LOGE("InitHooks: Failed to retrieve 'pathList' from the injected class loader."); + return; + } + + // Retrieve the 'dexElements' array, which contains the actual DEX files and resources. + auto elements = lsplant::JNI_Cast(lsplant::JNI_GetObjectFieldOf( + env, path_list, "dexElements", "[Ldalvik/system/DexPathList$Element;")); + if (!elements) { + LOGE("InitHooks: Failed to retrieve 'dexElements' from DexPathList."); + return; + } + + // Iterate over every element in the DexPathList to process individual DEX files. + for (auto &element : elements) { + if (element.get() == nullptr) continue; + + // extract the DexFile Java object from the element. + auto java_dex_file = + lsplant::JNI_GetObjectFieldOf(env, element, "dexFile", "Ldalvik/system/DexFile;"); + if (!java_dex_file) { + // Not all elements are guaranteed to have a valid DexFile + // (e.g., resource-only elements). + LOGW("InitHooks: Encountered a dexElement with no associated DexFile."); + continue; + } + + // Retrieve the 'mCookie'. In ART, this field stores the pointer (as a long or object) + // to the internal native DexFile structure. + auto cookie = + lsplant::JNI_GetObjectFieldOf(env, java_dex_file, "mCookie", "Ljava/lang/Object;"); + if (!cookie) { + LOGW("InitHooks: Could not retrieve 'mCookie' (native handle) from DexFile."); + continue; + } + + // Attempt to modify the internal ART flags for this DEX file. + // This effectively whitelists the DEX file, treating it as if it were part of + // the BootClassPath, thereby bypassing Hidden API enforcement policies. + if (lsplant::MakeDexFileTrusted(env, cookie.get())) { + LOGD("InitHooks: Successfully elevated trust privileges for DexFile."); + } else { + LOGW("InitHooks: Failed to elevate trust privileges for DexFile."); + } + } + + // ------------------------------------------------------------------------- + // JNI Bridge Registration + // ------------------------------------------------------------------------- + jni::RegisterResourcesHook(env); + jni::RegisterHookBridge(env); + jni::RegisterNativeApiBridge(env); + jni::RegisterDexParserBridge(env); +} + +lsplant::ScopedLocalRef Context::FindClassFromLoader(JNIEnv *env, jobject class_loader, + std::string_view class_name) { + if (class_loader == nullptr) { + return {env, nullptr}; + } + static const auto dex_class_loader_class = + lsplant::JNI_NewGlobalRef(env, lsplant::JNI_FindClass(env, "dalvik/system/DexClassLoader")); + static jmethodID load_class_mid = lsplant::JNI_GetMethodID( + env, dex_class_loader_class, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + if (!load_class_mid) { + load_class_mid = lsplant::JNI_GetMethodID(env, dex_class_loader_class, "findClass", + "(Ljava/lang/String;)Ljava/lang/Class;"); + } + + if (load_class_mid) { + auto name_str = lsplant::JNI_NewStringUTF(env, class_name.data()); + auto result = lsplant::JNI_CallObjectMethod(env, class_loader, load_class_mid, name_str); + if (result) { + return result; + } + } else { + LOGE("Could not find DexClassLoader.loadClass / .findClass method ID."); + } + + // Log clearly on failure. + if (env->ExceptionCheck()) { + env->ExceptionClear(); // Clear exception to prevent app crash + } + LOGE("Class '{}' not found using the provided class loader.", class_name.data()); + return {env, nullptr}; +} + +} // namespace vector::native diff --git a/native/src/core/native_api.cpp b/native/src/core/native_api.cpp new file mode 100644 index 000000000..14920e2e8 --- /dev/null +++ b/native/src/core/native_api.cpp @@ -0,0 +1,197 @@ +#include "core/native_api.h" + +#include + +#include +#include +#include +#include + +#include "common/logging.h" +#include "elf/elf_image.h" +#include "elf/symbol_cache.h" + +/** + * @file native_api.cpp + * @brief Implementation of the native module loading and API provisioning system. + */ + +using lsplant::operator""_sym; +/* + * =========================================================================================== + * LSPLANT HOOKING DSL (DOMAIN SPECIFIC LANGUAGE) DOCUMENTATION + * =========================================================================================== + * + * This source file utilizes the 'lsplant' library, which implements a C++20 Hooking DSL. + * Unlike traditional C-style hooking (which relies on void* casting, manual trampolines, + * and global function pointers), this DSL uses compile-time metaprogramming to ensure + * type safety and encapsulate hooking logic. + * + * ------------------------------------------------------------------------------------------- + * 1. SYNTAX ANATOMY + * ------------------------------------------------------------------------------------------- + * The hooking syntax follows this pattern: + * "SYMBOL_NAME"_sym .hook ->* [] (args...) { ...body... }; + * + * A. "SYMBOL_NAME"_sym + * - This is a C++ User-Defined Literal (UDL). It converts the string literal into a + * compile-time 'Symbol' type. + * - For C++ mangled names (common in Android system libs), you must provide the full + * mangled signature (e.g., "__dl__Z9do_dlopen..."). + * + * B. Multi-Architecture Support (| Operator) + * - Android often requires different symbol names for 32-bit (ARM) and 64-bit (ARM64). + * - The DSL supports the pipe operator '|' to select the correct symbol at compile time: + * ("Sym32"_sym | "Sym64"_sym) + * + * C. .hook ->* + * - '.hook' accesses the hook injection mechanism. + * - '->*' (Member Pointer Operator) is overloaded to bind the symbol to the lambda. + * + * D. The Template Lambda (The Replacement) + * - Syntax: [] (Type arg1, Type arg2...) { ... } + * - This is a C++20 Template Lambda. + * - 'backup': Represents the ORIGINAL function (trampoline). + * You call this to execute the original system logic. + * - 'args...': Must match the signature of the target function exactly. + * + * ------------------------------------------------------------------------------------------- + * 2. EXAMPLE USAGE + * ------------------------------------------------------------------------------------------- + * inline static auto my_hook = + * "__open"_sym.hook ->* [](const char* path, int flags) { + * // 1. Pre-processing (Before original) + * LOGD("Opening file: %s", path); + * + * // 2. Call Original (The "Backup") + * int result = backup(path, flags); + * + * // 3. Post-processing (After original) + * return result; + * }; + * + * ------------------------------------------------------------------------------------------- + * 3. REGISTRATION + * ------------------------------------------------------------------------------------------- + * Defining the hook variable does not apply it. + * You must pass the variable to the HookHandler to modify memory: handler(my_hook). + * =========================================================================================== + */ + +namespace vector::native { + +namespace { +// Mutex to protect access to the global module lists. +std::mutex g_module_registry_mutex; +// List of callback functions provided by loaded native modules. +std::list g_module_loaded_callbacks; +// List of native library filenames that are registered as modules. +std::list g_module_native_libs; + +// A smart pointer to a memory page that will hold the NativeAPIEntries struct. +std::unique_ptr> g_api_page( + mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0), [](void *ptr) { + if (ptr != MAP_FAILED) { + munmap(ptr, 4096); + } + }); +} // namespace + +// The read-only, statically available Native API entry points for modules. +const NativeAPIEntries *g_native_api_entries = nullptr; + +/** + * @brief Initializes the Native API entries struct and makes it read-only. + */ +void InitializeApiEntries() { + if (g_api_page.get() == MAP_FAILED) { + LOGF("Failed to allocate memory for native API entries."); + LOGD("Release the memory page pointer %p", g_api_page.release()); + return; + } + auto *entries = new (g_api_page.get()) NativeAPIEntries{ + .version = 2, + .hookFunc = &HookInline, + .unhookFunc = &UnhookInline, + }; + if (mprotect(g_api_page.get(), 4096, PROT_READ) != 0) { + PLOGE("Failed to mprotect API page to read-only"); + } + g_native_api_entries = entries; + LOGI("Native API entries initialized and protected."); +} + +void RegisterNativeLib(const std::string &library_name) { + static bool is_initialized = []() { + InitializeApiEntries(); + return InstallNativeAPI(lsplant::InitInfo{ + .inline_hooker = + [](void *target, void *replacement) { + void *backup = nullptr; + return HookInline(target, replacement, &backup) == 0 ? backup : nullptr; + }, + .art_symbol_resolver = + [](auto symbol) { return ElfSymbolCache::GetLinker()->getSymbAddress(symbol); }, + }); + }(); + + if (!is_initialized) { + LOGE("Cannot register module '{}' because native API failed to initialize.", + library_name.c_str()); + return; + } + + std::lock_guard lock(g_module_registry_mutex); + g_module_native_libs.push_back(library_name); + LOGD("Native module library '{}' has been registered.", library_name.c_str()); +} + +bool HasEnding(std::string_view fullString, std::string_view ending) { + if (fullString.length() >= ending.length()) { + return (fullString.compare(fullString.length() - ending.length(), std::string_view::npos, + ending) == 0); + } + return false; +} + +inline static auto do_dlopen_hook = + "__dl__Z9do_dlopenPKciPK17android_dlextinfoPKv"_sym.hook->* + [](const char *name, int flags, const void *extinfo, + const void *caller_addr) static -> void * { + void *handle = backup(name, flags, extinfo, caller_addr); + const std::string lib_name = (name != nullptr) ? name : "null"; + LOGV("do_dlopen hook triggered for library: '{}'", lib_name.c_str()); + + if (handle == nullptr) return nullptr; + + std::lock_guard lock(g_module_registry_mutex); + + for (std::string_view module_lib : g_module_native_libs) { + if (HasEnding(lib_name, module_lib)) { + LOGI("Detected registered native module being loaded: '{}'", lib_name.c_str()); + void *init_sym = dlsym(handle, "native_init"); + if (init_sym == nullptr) { + LOGW("Library '{}' matches a module name but does not export 'native_init'.", + lib_name.c_str()); + break; + } + auto native_init = reinterpret_cast(init_sym); + if (auto callback = native_init(g_native_api_entries)) { + g_module_loaded_callbacks.push_back(callback); + LOGI("Initialized native module '{}' and registered its callback.", + lib_name.c_str()); + } + break; + } + } + + for (const auto &callback : g_module_loaded_callbacks) { + callback(name, handle); + } + + return handle; +}; + +bool InstallNativeAPI(const lsplant::HookHandler &handler) { return handler(do_dlopen_hook); } + +} // namespace vector::native diff --git a/native/src/elf/elf_image.cpp b/native/src/elf/elf_image.cpp new file mode 100644 index 000000000..eb86fc0ff --- /dev/null +++ b/native/src/elf/elf_image.cpp @@ -0,0 +1,377 @@ +#include "elf/elf_image.h" + +#include +#include // For decompressing .gnu_debugdata +#include +#include +#include + +#include +#include // For std::move + +#include "common/logging.h" + +namespace vector::native { + +namespace { +// Helper to safely cast an offset from a base pointer. +template +inline T PtrOffset(void *base, ptrdiff_t offset) { + return reinterpret_cast(reinterpret_cast(base) + offset); +} +} // namespace + +ElfImage::ElfImage(std::string_view lib_name) : path_(lib_name) { + if (!findModuleBase()) { + base_ = nullptr; // Ensure base_ is null on failure. + return; + } + + int fd = open(path_.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { + PLOGE("Failed to open ELF file: {}", path_.c_str()); + return; + } + + struct stat file_info; + if (fstat(fd, &file_info) < 0) { + PLOGE("fstat failed for {}", path_.c_str()); + close(fd); + return; + } + file_size_ = file_info.st_size; + + file_map_ = mmap(nullptr, file_size_, PROT_READ, MAP_SHARED, fd, 0); + close(fd); + + if (file_map_ == MAP_FAILED) { + PLOGE("mmap failed for {}", path_.c_str()); + file_map_ = nullptr; + return; + } + + header_ = static_cast(file_map_); + parseHeaders(header_); + + // Check for and handle compressed debug symbols. + if (decompressGnuDebugData()) { + header_debugdata_ = PtrOffset(elf_debugdata_.data(), 0); + // Re-parse to find the .symtab and its .strtab from the debug data. + parseHeaders(header_debugdata_); + } +} + +ElfImage::~ElfImage() { + if (file_map_ != nullptr) { + munmap(file_map_, file_size_); + } +} + +void ElfImage::parseHeaders(ElfW(Ehdr) * header) { + if (!header) return; + + ElfW(Shdr) *section_headers = PtrOffset(header, header->e_shoff); + const char *section_str_table = + PtrOffset(header, section_headers[header->e_shstrndx].sh_offset); + + for (int i = 0; i < header->e_shnum; ++i) { + ElfW(Shdr) *section_h = §ion_headers[i]; + const char *sname = section_str_table + section_h->sh_name; + + switch (section_h->sh_type) { + case SHT_DYNSYM: + // We only care about the first .dynsym found in the original ELF file. + if (dynsym_ == nullptr) { + dynsym_ = section_h; + dynsym_start_ = PtrOffset(header, section_h->sh_offset); + } + break; + case SHT_SYMTAB: + if (strcmp(sname, ".symtab") == 0) { + symtab_start_ = PtrOffset(header, section_h->sh_offset); + symtab_count_ = section_h->sh_size / section_h->sh_entsize; + } + break; + case SHT_STRTAB: + // The string table for .dynsym is usually the first SHT_STRTAB after .dynsym. + // We identify it by checking if dynsym is found but its strtab is not. + if (dynsym_ != nullptr && strtab_start_ == nullptr) { + strtab_start_ = PtrOffset(header, section_h->sh_offset); + } + // The string table for .symtab is explicitly named ".strtab". + if (strcmp(sname, ".strtab") == 0) { + symtab_str_start_ = PtrOffset(header, section_h->sh_offset); + } + break; + case SHT_PROGBITS: + // The load bias is the difference between + // the virtual address of a loaded segment and its offset in the file. + // We calculate it once. + if (bias_ == 0 && section_h->sh_flags & SHF_ALLOC && section_h->sh_addr > 0) { + bias_ = section_h->sh_addr - section_h->sh_offset; + } + break; + case SHT_HASH: + // Standard ELF hash table. + if (nbucket_ == 0) { + uint32_t *hash_data = PtrOffset(header, section_h->sh_offset); + nbucket_ = hash_data[0]; + // nchain is hash_data[1] + bucket_ = &hash_data[2]; + chain_ = bucket_ + nbucket_; + } + break; + case SHT_GNU_HASH: + // GNU-style hash table. + if (gnu_nbucket_ == 0) { + uint32_t *hash_data = PtrOffset(header, section_h->sh_offset); + gnu_nbucket_ = hash_data[0]; + gnu_symndx_ = hash_data[1]; + gnu_bloom_size_ = hash_data[2]; + gnu_shift2_ = hash_data[3]; + gnu_bloom_filter_ = reinterpret_cast(&hash_data[4]); + gnu_bucket_ = reinterpret_cast(gnu_bloom_filter_ + gnu_bloom_size_); + gnu_chain_ = gnu_bucket_ + gnu_nbucket_; + } + break; + } + } +} + +bool ElfImage::decompressGnuDebugData() { + ElfW(Shdr) *section_headers = PtrOffset(header_, header_->e_shoff); + const char *section_str_table = + PtrOffset(header_, section_headers[header_->e_shstrndx].sh_offset); + ElfW(Off) debugdata_offset = 0; + ElfW(Off) debugdata_size = 0; + + for (int i = 0; i < header_->e_shnum; ++i) { + if (strcmp(section_str_table + section_headers[i].sh_name, ".gnu_debugdata") == 0) { + debugdata_offset = section_headers[i].sh_offset; + debugdata_size = section_headers[i].sh_size; + break; + } + } + + if (debugdata_offset == 0 || debugdata_size == 0) { + return false; // Section not found. + } + LOGD("Found .gnu_debugdata section in {} ({} bytes). Decompressing...", path_.c_str(), + debugdata_size); + + xz_crc32_init(); + struct xz_dec *dec = xz_dec_init(XZ_DYNALLOC, 1 << 26); + if (!dec) return false; + + struct xz_buf buf; + buf.in = PtrOffset(header_, debugdata_offset); + buf.in_pos = 0; + buf.in_size = debugdata_size; + + elf_debugdata_.resize(debugdata_size * 8); // Initial guess + buf.out = reinterpret_cast(elf_debugdata_.data()); + buf.out_pos = 0; + buf.out_size = elf_debugdata_.size(); + + while (true) { + enum xz_ret ret = xz_dec_run(dec, &buf); + if (ret == XZ_STREAM_END) { + elf_debugdata_.resize(buf.out_pos); + xz_dec_end(dec); + LOGD("Successfully decompressed .gnu_debugdata ({} bytes)", elf_debugdata_.size()); + return true; + } + if (ret != XZ_OK) { + LOGE("XZ decompression failed with code {}", (int)ret); + xz_dec_end(dec); + return false; + } + if (buf.out_pos == buf.out_size) { + elf_debugdata_.resize(elf_debugdata_.size() * 2); + // Reset pointer to the potentially new base address + buf.out = reinterpret_cast(elf_debugdata_.data()); + // Update the total capacity + buf.out_size = elf_debugdata_.size(); + } + } +} + +ElfW(Addr) ElfImage::getSymbOffset(std::string_view name, uint32_t gnu_hash, + uint32_t elf_hash) const { + if (auto offset = gnuLookup(name, gnu_hash); offset > 0) { + return offset; + } else if (offset = elfLookup(name, elf_hash); offset > 0) { + return offset; + } else if (offset = linearLookup(name); offset > 0) { + return offset; + } else { + return 0; + } +} + +ElfW(Addr) ElfImage::gnuLookup(std::string_view name, uint32_t hash) const { + if (gnu_nbucket_ == 0) return 0; + + constexpr auto bloom_mask_bits = sizeof(ElfW(Addr)) * 8; + auto bloom_word = gnu_bloom_filter_[(hash / bloom_mask_bits) % gnu_bloom_size_]; + uintptr_t mask = + (1ULL << (hash % bloom_mask_bits)) | (1ULL << ((hash >> gnu_shift2_) % bloom_mask_bits)); + + if ((bloom_word & mask) != mask) { + return 0; // Not in bloom filter, definitely not here. + } + + uint32_t sym_idx = gnu_bucket_[hash % gnu_nbucket_]; + if (sym_idx < gnu_symndx_) return 0; + + do { + ElfW(Sym) *sym = dynsym_start_ + sym_idx; + if (((gnu_chain_[sym_idx - gnu_symndx_] ^ hash) >> 1) == 0) { + if (std::string_view(strtab_start_ + sym->st_name) == name) { + return sym->st_value; + } + } + } while ((gnu_chain_[sym_idx++ - gnu_symndx_] & 1) == 0); + + return 0; +} + +ElfW(Addr) ElfImage::elfLookup(std::string_view name, uint32_t hash) const { + if (nbucket_ == 0) return 0; + + for (uint32_t n = bucket_[hash % nbucket_]; n != 0; n = chain_[n]) { + ElfW(Sym) *sym = dynsym_start_ + n; + if (std::string_view(strtab_start_ + sym->st_name) == name) { + return sym->st_value; + } + } + return 0; +} + +void ElfImage::ensureLinearMapInitialized() const { + // Lazily parse the .symtab section and build a map for faster lookups. + if (!symtabs_.empty() || !symtab_start_ || !symtab_str_start_) { + return; + } + + for (ElfW(Off) i = 0; i < symtab_count_; ++i) { + auto *sym = &symtab_start_[i]; + unsigned int st_type = ELF_ST_TYPE(sym->st_info); + // We only care about function or object symbols that have a size. + if ((st_type == STT_FUNC || st_type == STT_OBJECT) && sym->st_size > 0) { + const char *st_name = symtab_str_start_ + sym->st_name; + symtabs_.emplace(st_name, sym); + } + } +} + +ElfW(Addr) ElfImage::linearLookup(std::string_view name) const { + ensureLinearMapInitialized(); + auto it = symtabs_.find(name); + if (it != symtabs_.end()) { + return it->second->st_value; + } + return 0; +} + +std::vector ElfImage::linearRangeLookup(std::string_view name) const { + ensureLinearMapInitialized(); + std::vector res; + for (auto [it, end] = symtabs_.equal_range(name); it != end; ++it) { + res.emplace_back(it->second->st_value); + } + return res; +} + +ElfW(Addr) ElfImage::prefixLookupFirst(std::string_view prefix) const { + ensureLinearMapInitialized(); + // lower_bound finds the first element not less than the prefix. + auto it = symtabs_.lower_bound(prefix); + if (it != symtabs_.end() && it->first.starts_with(prefix)) { + return it->second->st_value; + } + return 0; +} + +bool ElfImage::findModuleBase() { + // A helper struct to hold parsed map entry data. + struct MapEntry { + uintptr_t start_addr; + char perms[5] = {0}; + std::string pathname; + }; + + FILE *maps = fopen("/proc/self/maps", "r"); + if (!maps) { + PLOGE("Failed to open /proc/self/maps"); + return false; + } + + char line_buffer[512]; + std::vector filtered_list; + + // Filter all entries containing the library name. + while (fgets(line_buffer, sizeof(line_buffer), maps)) { + if (strstr(line_buffer, path_.c_str())) { + unsigned long long temp_start; + char path_buffer[256] = {0}; + char p[5] = {0}; + int items_parsed = + sscanf(line_buffer, "%llx-%*x %4s %*x %*s %*d %255s", &temp_start, p, path_buffer); + + if (items_parsed >= 2) { + MapEntry entry; + entry.start_addr = static_cast(temp_start); + strncpy(entry.perms, p, 4); + if (items_parsed == 3) entry.pathname = path_buffer; + filtered_list.push_back(std::move(entry)); + LOGD("Found module entry: {}", line_buffer); + } + } + } + fclose(maps); + + if (filtered_list.empty()) { + LOGE("Could not find any mappings for {}", path_.c_str()); + return false; + } + + const MapEntry *found_block = nullptr; + + // Search for the first `r--p` whose next entry is `r-xp`. + // This is the most reliable pattern for `libart.so`. + for (size_t i = 0; i + 1 < filtered_list.size(); ++i) { + if (strcmp(filtered_list[i].perms, "r--p") == 0 && + strcmp(filtered_list[i + 1].perms, "r-xp") == 0) { + found_block = &filtered_list[i]; + break; + } + } + + // If the pattern was not found, find the first `r-xp` entry. + if (!found_block) { + for (const auto &entry : filtered_list) { + if (strcmp(entry.perms, "r-xp") == 0) { + found_block = &entry; + break; + } + } + } + + // If still no match, take the very first entry found. + if (!found_block) { + found_block = &filtered_list[0]; + } + + // Use the starting address of the found block as the base address. + base_ = reinterpret_cast(found_block->start_addr); + // Update path to the canonical one from the maps file. + if (!found_block->pathname.empty()) { + path_ = found_block->pathname; + } + + LOGD("Found base for {} at {:#x}", path_.c_str(), found_block->start_addr); + return true; +} + +} // namespace vector::native diff --git a/native/src/elf/symbol_cache.cpp b/native/src/elf/symbol_cache.cpp new file mode 100644 index 000000000..80a4412c8 --- /dev/null +++ b/native/src/elf/symbol_cache.cpp @@ -0,0 +1,120 @@ +#include "elf/symbol_cache.h" + +#include + +#include "common/config.h" +#include "elf/elf_image.h" + +namespace vector::native { + +namespace { +// Each cached ElfImage gets its own unique_ptr and a mutex to guard its +// initialization. +std::unique_ptr g_art_image = nullptr; +std::mutex g_art_mutex; + +std::unique_ptr g_binder_image = nullptr; +std::mutex g_binder_mutex; + +std::unique_ptr g_linker_image = nullptr; +std::mutex g_linker_mutex; +} // namespace + +const ElfImage *ElfSymbolCache::GetArt() { + // Double-checked locking pattern for performance. + // The first check is lock-free. + if (g_art_image) { + return g_art_image.get(); + } + + // If it's null, acquire the lock to perform the initialization safely. + std::lock_guard lock(g_art_mutex); + + // Check again inside the lock in case another thread initialized it + // while we were waiting for the lock. + if (!g_art_image) { + g_art_image = std::make_unique(kArtLibraryName); + if (!g_art_image->IsValid()) { + g_art_image.reset(); // Release if invalid. + } + } + return g_art_image.get(); +} + +const ElfImage *ElfSymbolCache::GetLibBinder() { + if (g_binder_image) { + return g_binder_image.get(); + } + std::lock_guard lock(g_binder_mutex); + if (!g_binder_image) { + g_binder_image = std::make_unique(kBinderLibraryName); + if (!g_binder_image->IsValid()) { + g_binder_image.reset(); + } + } + return g_binder_image.get(); +} + +const ElfImage *ElfSymbolCache::GetLinker() { + if (g_linker_image) { + return g_linker_image.get(); + } + std::lock_guard lock(g_linker_mutex); + if (!g_linker_image) { + g_linker_image = std::make_unique(kLinkerPath); + if (!g_linker_image->IsValid()) { + g_linker_image.reset(); + } + } + return g_linker_image.get(); +} + +bool ElfSymbolCache::ClearCache(const ElfImage *image_to_clear) { + if (!image_to_clear) { + return false; + } + + // This "lock, check, then reset" pattern must be atomic for each cache entry. + // We check each cache one by one. + + // Check ART cache + { + std::lock_guard lock(g_art_mutex); + if (image_to_clear == g_art_image.get()) { + g_art_image.reset(); + return true; // Found and cleared, no need to check others. + } + } + + // Check Binder cache + { + std::lock_guard lock(g_binder_mutex); + if (image_to_clear == g_binder_image.get()) { + g_binder_image.reset(); + return true; + } + } + + // Check Linker cache + { + std::lock_guard lock(g_linker_mutex); + if (image_to_clear == g_linker_image.get()) { + g_linker_image.reset(); + return true; + } + } + + return false; +} + +void ElfSymbolCache::ClearCache() { + // Acquire all locks to ensure no other thread is currently initializing. + std::lock_guard art_lock(g_art_mutex); + std::lock_guard binder_lock(g_binder_mutex); + std::lock_guard linker_lock(g_linker_mutex); + g_art_image.reset(); + g_binder_image.reset(); + g_linker_image.reset(); +} + +} // namespace vector::native diff --git a/native/src/jni/dex_parser_bridge.cpp b/native/src/jni/dex_parser_bridge.cpp new file mode 100644 index 000000000..41549a7e0 --- /dev/null +++ b/native/src/jni/dex_parser_bridge.cpp @@ -0,0 +1,960 @@ +#include +#include + +#include + +#include "jni/jni_bridge.h" +#include "jni/jni_hooks.h" + +/** + * @file dex_parser_bridge.cpp + * @brief Implements a JNI bridge to a native DEX file parser. + * + * This bridge provides a memory-efficient way for Java code to parse Android DEX files. + * It avoids creating a complete object representation of the DEX file in memory, + * which can be very large. + * + * It employs a visitor pattern: + * 1. The `openDex` method performs an initial parse of the DEX file's main sections + * (strings, types, fields, methods, classes) and returns them + * to the Java caller as primitive arrays. + * It stores the detailed parsed data in a native `DexParser` object. + * 2. The `visitClass` method then iterates through the parsed classes and + * invokes callback methods on a Java "visitor" object for + * each class, field, and method. + * + * This approach minimizes JNI overhead and memory consumption by processing data + * in a streaming fashion and only creating Java objects as needed for the visitor callbacks. + */ + +namespace { +// Type aliases for representing DEX encoded values and annotations. +// These structures temporarily hold parsed annotation data before it's converted to Java objects. + +// A DEX encoded value, represented as a tuple of its type and raw byte data. +using Value = std::tuple /*data*/>; +// A DEX encoded array, which is a vector of encoded values. +using Array = std::vector; +// A list of encoded arrays. A list is used because its elements won't be +// reallocated, which is important when indices are stored. +using ArrayList = std::list; +// An element of an annotation, consisting of a name (index into string table) and a value. +using Element = std::tuple; +// A list of annotation elements. +using ElementList = std::vector; +// A DEX annotation, containing its visibility, type, and a list of its elements. +using Annotation = std::tuple; +// A list of annotations. +using AnnotationList = std::vector; + +/** + * @class DexParser + * @brief Extends slicer's dex::Reader to hold parsed class, method, and annotation data. + * + * This class serves as the main native handle for a parsed DEX file. + * It stores structured data that has been read from the DEX file, + * making it readily available for the `visitClass` function. + */ +class DexParser : public dex::Reader { +public: + DexParser(const dex::u1 *data, size_t size) : dex::Reader(data, size, nullptr, 0) {} + + /** + * @struct ClassData + * @brief Holds all relevant information for a single class definition. + * + * This structure is populated during the `openDex` phase and contains indices + * pointing to the DEX file's various data pools (types, fields, methods). + */ + struct ClassData { + std::vector interfaces; + std::vector static_fields; + std::vector static_fields_access_flags; + std::vector instance_fields; + std::vector instance_fields_access_flags; + std::vector direct_methods; + std::vector direct_methods_access_flags; + std::vector direct_methods_code; // Pointers to method bytecode + std::vector virtual_methods; + std::vector virtual_methods_access_flags; + std::vector virtual_methods_code; // Pointers to method bytecode + std::vector annotations; + }; + + /** + * @struct MethodBody + * @brief Lazily-parsed information from a method's bytecode. + * + * This data is only computed when a method is visited in `visitClass`, + * saving significant processing time if the caller is not interested in method body details. + */ + struct MethodBody { + bool loaded = false; // Flag to indicate if this body has been parsed yet. + std::vector referred_strings; + std::vector accessed_fields; // Fields read from (iget/sget) + std::vector assigned_fields; // Fields written to (iput/sput) + std::vector invoked_methods; + std::vector opcodes; + }; + + // Parsed data storage + std::vector class_data; // One entry per ClassDef in the DEX file. + // Mappings from an item's index to a list of annotation indices. + // Using phmap::flat_hash_map for fast lookups. + phmap::flat_hash_map> field_annotations; + phmap::flat_hash_map> method_annotations; + phmap::flat_hash_map> parameter_annotations; + + // Lazily populated map of method index to its parsed body. + phmap::flat_hash_map method_bodies; +}; + +/** + * @brief Parses a variable-length integer from the DEX byte stream. + * @tparam T The integral type to parse (e.g., int8_t, int32_t). + * @param pptr Pointer to the current position in the byte stream. + * @param size The number of bytes to read (1 to sizeof(T)). + * @return A vector of bytes containing the parsed value. + */ +template +static std::vector ParseIntValue(const dex::u1 **pptr, size_t size) { + static_assert(std::is_integral::value, "must be an integral type"); + std::vector ret(sizeof(T)); + // Use reinterpret_cast to type-pun the byte vector's data into the target integer type. + T &value = *reinterpret_cast(ret.data()); + value = 0; // Ensure starting from a clean state. + for (size_t i = 0; i < size; ++i) { + value |= T(*(*pptr)++) << (i * 8); + } + + // If the type is signed and we read fewer bytes than its full size, + // we need to manually sign-extend the value. + if constexpr (std::is_signed_v) { + size_t shift = (sizeof(T) - size) * 8; + if (shift > 0) { + value = T(value << shift) >> shift; + } + } + return ret; +} + +/** + * @brief Parses a variable-length float from the DEX byte stream. + * @tparam T The floating-point type to parse (float or double). + * @param pptr Pointer to the current position in the byte stream. + * @param size The number of bytes to read. + * @return A vector of bytes containing the parsed value. + */ +template +static std::vector ParseFloatValue(const dex::u1 **pptr, size_t size) { + std::vector ret(sizeof(T), 0); + T &value = *reinterpret_cast(ret.data()); + // The value is right-padded with zero bytes, so we copy into the higher-order bytes. + int start_byte = sizeof(T) - size; + for (dex::u1 *p = reinterpret_cast(&value) + start_byte; size > 0; --size) { + *p++ = *(*pptr)++; + } + return ret; +} + +// Forward declarations for recursive parsing functions. +Annotation ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, + ArrayList &array_list); + +Array ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list); + +/** + * @brief Parses a single `encoded_value` from the byte stream. + * This is the core of the annotation parsing logic and + * handles all possible value types recursively. + */ +Value ParseValue(const dex::u1 **value, AnnotationList &annotation_list, ArrayList &array_list) { + Value res; + auto &[type, value_content] = res; + auto header = *(*value)++; + type = header & dex::kEncodedValueTypeMask; + dex::u1 arg = header >> dex::kEncodedValueArgShift; + switch (type) { + // For numeric types, `arg` is `size - 1`. + case dex::kEncodedByte: + value_content = ParseIntValue(value, arg + 1); + break; + case dex::kEncodedShort: + value_content = ParseIntValue(value, arg + 1); + break; + case dex::kEncodedChar: + value_content = ParseIntValue(value, arg + 1); + break; + case dex::kEncodedInt: + value_content = ParseIntValue(value, arg + 1); + break; + case dex::kEncodedLong: + value_content = ParseIntValue(value, arg + 1); + break; + case dex::kEncodedFloat: + value_content = ParseFloatValue(value, arg + 1); + break; + case dex::kEncodedDouble: + value_content = ParseFloatValue(value, arg + 1); + break; + // For index types, the value is the index itself. + case dex::kEncodedMethodType: + case dex::kEncodedMethodHandle: + case dex::kEncodedString: + case dex::kEncodedType: + case dex::kEncodedField: + case dex::kEncodedMethod: + case dex::kEncodedEnum: + value_content = ParseIntValue(value, arg + 1); + break; + // For complex types, we parse them recursively and store an index to the + // parsed object. + case dex::kEncodedArray: + value_content.resize(sizeof(jint)); + *reinterpret_cast(value_content.data()) = static_cast(array_list.size()); + array_list.emplace_back(ParseArray(value, annotation_list, array_list)); + break; + case dex::kEncodedAnnotation: + value_content.resize(sizeof(jint)); + *reinterpret_cast(value_content.data()) = static_cast(annotation_list.size()); + annotation_list.emplace_back(ParseAnnotation(value, annotation_list, array_list)); + break; + case dex::kEncodedNull: + // No value content needed. + break; + case dex::kEncodedBoolean: + // The boolean value is stored in the `arg` part of the header. + value_content = {static_cast(arg == 1)}; + break; + default: + // This should never be reached for a valid DEX file. + __builtin_unreachable(); + } + return res; +} + +/** + * @brief Parses an `encoded_annotation` structure. + */ +Annotation ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, + ArrayList &array_list) { + Annotation ret = {dex::kVisibilityEncoded, dex::ReadULeb128(annotation), ElementList{}}; + auto &[vis, type, element_list] = ret; + auto size = dex::ReadULeb128(annotation); + element_list.resize(size); + for (size_t j = 0; j < size; ++j) { + auto &[name, value] = element_list[j]; + name = static_cast(dex::ReadULeb128(annotation)); + value = ParseValue(annotation, annotation_list, array_list); + } + return ret; +} + +/** + * @brief Parses an `encoded_array` structure. + */ +Array ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list) { + auto size = dex::ReadULeb128(array); + Array ret; + ret.reserve(size); + for (size_t i = 0; i < size; ++i) { + ret.emplace_back(ParseValue(array, annotation_list, array_list)); + } + return ret; +} + +/** + * @brief Parses an `AnnotationSetItem`, which is a collection of annotations. + */ +void ParseAnnotationSet(dex::Reader &dex, AnnotationList &annotation_list, ArrayList &array_list, + std::vector &indices, const dex::AnnotationSetItem *annotation_set) { + if (annotation_set == nullptr) { + return; + } + for (size_t i = 0; i < annotation_set->size; ++i) { + auto *item = dex.dataPtr(annotation_set->entries[i]); + auto *annotation_data = item->annotation; + // Store the index of the new annotation in the output list. + indices.emplace_back(annotation_list.size()); + // Parse the annotation and add it to the global list. + auto &[visibility, type, element_list] = annotation_list.emplace_back( + ParseAnnotation(&annotation_data, annotation_list, array_list)); + // The visibility is stored in the item, not the encoded annotation itself. + visibility = item->visibility; + } +} +} // namespace + +namespace vector::native::jni { +/** + * @brief JNI method to open a DEX file and perform initial parsing. + * @param data A direct java.nio.ByteBuffer containing the DEX file. + * @param args A jlongArray used for passing arguments. + * args[0] is an output parameter to store the native DexParser pointer (cookie). + * args[1] is an input flag to control whether to parse annotations. + * @return A java.lang.Object[] array containing the top-level DEX structures. + */ +VECTOR_DEF_NATIVE_METHOD(jobject, DexParserBridge, openDex, jobject data, jlongArray args) { + auto dex_size = env->GetDirectBufferCapacity(data); + if (dex_size == -1) { + env->ThrowNew(env->FindClass("java/io/IOException"), + "DEX data must be in a direct ByteBuffer"); + return nullptr; + } + auto *dex_data = env->GetDirectBufferAddress(data); + if (dex_data == nullptr) { + env->ThrowNew(env->FindClass("java/io/IOException"), "Failed to get direct buffer address"); + return nullptr; + } + + // Create the native parser object. + // This will be the handle for subsequent calls. + auto *dex_reader = new DexParser(reinterpret_cast(dex_data), dex_size); + auto *args_ptr = env->GetLongArrayElements(args, nullptr); + auto include_annotations = args_ptr[1]; + env->ReleaseLongArrayElements(args, args_ptr, JNI_ABORT); + // Store the pointer to the native object in the first element of the args array. + // This "cookie" will be passed back to other native methods. + env->SetLongArrayRegion(args, 0, 1, reinterpret_cast(&dex_reader)); + auto &dex = *dex_reader; + if (dex.IsCompact()) { + env->ThrowNew(env->FindClass("java/io/IOException"), "Compact dex is not supported"); + delete dex_reader; // Clean up before returning. + return nullptr; + } + + // Find classes needed for creating Java objects. + auto object_class = env->FindClass("java/lang/Object"); + auto string_class = env->FindClass("java/lang/String"); + auto int_array_class = env->FindClass("[I"); + // This is the main output array that will be returned to Java. + auto out = env->NewObjectArray(8, object_class, nullptr); + + // 1. Parse String IDs + auto out0 = + env->NewObjectArray(static_cast(dex.StringIds().size()), string_class, nullptr); + auto strings = dex.StringIds(); + for (size_t i = 0; i < strings.size(); ++i) { + const auto *ptr = dex.dataPtr(strings[i].string_data_off); + // The string data is MUTF-8 encoded. We skip the length prefix. + [[maybe_unused]] size_t len = dex::ReadULeb128(&ptr); + auto str = env->NewStringUTF(reinterpret_cast(ptr)); + env->SetObjectArrayElement(out0, static_cast(i), str); + env->DeleteLocalRef(str); + } + env->SetObjectArrayElement(out, 0, out0); + env->DeleteLocalRef(out0); + + // 2. Parse Type IDs + auto types = dex.TypeIds(); + auto out1 = env->NewIntArray(static_cast(types.size())); + auto *out1_ptr = env->GetIntArrayElements(out1, nullptr); + for (size_t i = 0; i < types.size(); ++i) { + out1_ptr[i] = static_cast(types[i].descriptor_idx); // Index into String table + } + env->ReleaseIntArrayElements(out1, out1_ptr, 0); + env->SetObjectArrayElement(out, 1, out1); + env->DeleteLocalRef(out1); + + // 3. Parse Proto IDs (Method Prototypes) + auto protos = dex.ProtoIds(); + auto out2 = env->NewObjectArray(static_cast(protos.size()), int_array_class, nullptr); + auto empty_type_list = dex::TypeList{.size = 0, .list = {}}; + for (size_t i = 0; i < protos.size(); ++i) { + auto &proto = protos[i]; + const auto ¶ms = proto.parameters_off + ? *dex.dataPtr(proto.parameters_off) + : empty_type_list; + + auto out2i = env->NewIntArray(static_cast(2 + params.size)); + auto *out2i_ptr = env->GetIntArrayElements(out2i, nullptr); + out2i_ptr[0] = static_cast(proto.shorty_idx); + out2i_ptr[1] = static_cast(proto.return_type_idx); + for (size_t j = 0; j < params.size; ++j) { + out2i_ptr[2 + j] = static_cast(params.list[j].type_idx); + } + env->ReleaseIntArrayElements(out2i, out2i_ptr, 0); + env->SetObjectArrayElement(out2, static_cast(i), out2i); + env->DeleteLocalRef(out2i); + } + env->SetObjectArrayElement(out, 2, out2); + env->DeleteLocalRef(out2); + + // 4. Parse Field IDs + auto fields = dex.FieldIds(); + auto out3 = env->NewIntArray(static_cast(3 * fields.size())); + auto *out3_ptr = env->GetIntArrayElements(out3, nullptr); + for (size_t i = 0; i < fields.size(); ++i) { + auto &field = fields[i]; + out3_ptr[3 * i] = static_cast(field.class_idx); // Defining class type index + out3_ptr[3 * i + 1] = static_cast(field.type_idx); // Field type index + out3_ptr[3 * i + 2] = static_cast(field.name_idx); // Field name string index + } + env->ReleaseIntArrayElements(out3, out3_ptr, 0); + env->SetObjectArrayElement(out, 3, out3); + env->DeleteLocalRef(out3); + + // 5. Parse Method IDs + auto methods = dex.MethodIds(); + auto out4 = env->NewIntArray(static_cast(3 * methods.size())); + auto *out4_ptr = env->GetIntArrayElements(out4, nullptr); + for (size_t i = 0; i < methods.size(); ++i) { + out4_ptr[3 * i] = static_cast(methods[i].class_idx); // Defining class type index + out4_ptr[3 * i + 1] = static_cast(methods[i].proto_idx); // Method prototype index + out4_ptr[3 * i + 2] = static_cast(methods[i].name_idx); // Method name string index + } + env->ReleaseIntArrayElements(out4, out4_ptr, 0); + env->SetObjectArrayElement(out, 4, out4); + env->DeleteLocalRef(out4); + + // 6. Parse Class Definitions and their data + auto classes = dex.ClassDefs(); + dex.class_data.resize(classes.size()); + + // These lists will store all annotations found in the DEX file. + AnnotationList annotation_list; + ArrayList array_list; + + for (size_t i = 0; i < classes.size(); ++i) { + auto &class_def = classes[i]; + + // Pointers to various parts of the class data. Initialize to safe defaults. + dex::u4 static_fields_count = 0; + dex::u4 instance_fields_count = 0; + dex::u4 direct_methods_count = 0; + dex::u4 virtual_methods_count = 0; + const dex::u1 *class_data_ptr = nullptr; + + const dex::AnnotationsDirectoryItem *annotations = nullptr; + const dex::AnnotationSetItem *class_annotation = nullptr; + dex::u4 field_annotations_count = 0; + dex::u4 method_annotations_count = 0; + dex::u4 parameter_annotations_count = 0; + + auto &class_data = dex.class_data[i]; + + // Parse implemented interfaces. + if (class_def.interfaces_off) { + auto defined_interfaces = dex.dataPtr(class_def.interfaces_off); + class_data.interfaces.resize(defined_interfaces->size); + for (size_t k = 0; k < class_data.interfaces.size(); ++k) { + class_data.interfaces[k] = defined_interfaces->list[k].type_idx; + } + } + + // Locate the annotations directory for this class, if it exists. + if (class_def.annotations_off != 0) { + annotations = dex.dataPtr(class_def.annotations_off); + if (annotations->class_annotations_off != 0) { + class_annotation = + dex.dataPtr(annotations->class_annotations_off); + } + field_annotations_count = annotations->fields_size; + method_annotations_count = annotations->methods_size; + parameter_annotations_count = annotations->parameters_size; + } + + // Read the core class data: fields and methods. + if (class_def.class_data_off != 0) { + class_data_ptr = dex.dataPtr(class_def.class_data_off); + static_fields_count = dex::ReadULeb128(&class_data_ptr); + instance_fields_count = dex::ReadULeb128(&class_data_ptr); + direct_methods_count = dex::ReadULeb128(&class_data_ptr); + virtual_methods_count = dex::ReadULeb128(&class_data_ptr); + // Pre-allocate vectors to improve performance. + class_data.static_fields.resize(static_fields_count); + class_data.static_fields_access_flags.resize(static_fields_count); + class_data.instance_fields.resize(instance_fields_count); + class_data.instance_fields_access_flags.resize(instance_fields_count); + class_data.direct_methods.resize(direct_methods_count); + class_data.direct_methods_access_flags.resize(direct_methods_count); + class_data.direct_methods_code.resize(direct_methods_count); + class_data.virtual_methods.resize(virtual_methods_count); + class_data.virtual_methods_access_flags.resize(virtual_methods_count); + class_data.virtual_methods_code.resize(virtual_methods_count); + } + + // Now, decode the field and method lists. + if (class_data_ptr) { + // Static fields + for (size_t k = 0, field_idx = 0; k < static_fields_count; ++k) { + field_idx += + dex::ReadULeb128(&class_data_ptr); // field_idx is a diff from previous + class_data.static_fields[k] = static_cast(field_idx); + class_data.static_fields_access_flags[k] = + static_cast(dex::ReadULeb128(&class_data_ptr)); + } + + // Instance fields + for (size_t k = 0, field_idx = 0; k < instance_fields_count; ++k) { + field_idx += dex::ReadULeb128(&class_data_ptr); + class_data.instance_fields[k] = static_cast(field_idx); + class_data.instance_fields_access_flags[k] = + static_cast(dex::ReadULeb128(&class_data_ptr)); + } + + // Direct methods (static, private, constructors) + for (size_t k = 0, method_idx = 0; k < direct_methods_count; ++k) { + method_idx += dex::ReadULeb128(&class_data_ptr); + class_data.direct_methods[k] = static_cast(method_idx); + class_data.direct_methods_access_flags[k] = + static_cast(dex::ReadULeb128(&class_data_ptr)); + auto code_off = dex::ReadULeb128(&class_data_ptr); + class_data.direct_methods_code[k] = + code_off ? dex.dataPtr(code_off) : nullptr; + } + + // Virtual methods + for (size_t k = 0, method_idx = 0; k < virtual_methods_count; ++k) { + method_idx += dex::ReadULeb128(&class_data_ptr); + class_data.virtual_methods[k] = static_cast(method_idx); + class_data.virtual_methods_access_flags[k] = + static_cast(dex::ReadULeb128(&class_data_ptr)); + auto code_off = dex::ReadULeb128(&class_data_ptr); + class_data.virtual_methods_code[k] = + code_off ? dex.dataPtr(code_off) : nullptr; + } + } + + // Optionally skip the expensive annotation parsing. + if (!include_annotations) continue; + + // Parse annotations for the class, its fields, methods, and parameters. + ParseAnnotationSet(dex, annotation_list, array_list, class_data.annotations, + class_annotation); + + auto *field_annotations = + annotations ? reinterpret_cast(annotations + 1) + : nullptr; + for (size_t k = 0; k < field_annotations_count; ++k) { + auto *field_annotation = + dex.dataPtr(field_annotations[k].annotations_off); + ParseAnnotationSet( + dex, annotation_list, array_list, + dex.field_annotations[static_cast(field_annotations[k].field_idx)], + field_annotation); + } + + auto *method_annotations = field_annotations + ? reinterpret_cast( + field_annotations + field_annotations_count) + : nullptr; + for (size_t k = 0; k < method_annotations_count; ++k) { + auto *method_annotation = + dex.dataPtr(method_annotations[k].annotations_off); + ParseAnnotationSet( + dex, annotation_list, array_list, + dex.method_annotations[static_cast(method_annotations[k].method_idx)], + method_annotation); + } + + auto *parameter_annotations = method_annotations + ? reinterpret_cast( + method_annotations + method_annotations_count) + : nullptr; + for (size_t k = 0; k < parameter_annotations_count; ++k) { + auto *parameter_annotation = + dex.dataPtr(parameter_annotations[k].annotations_off); + auto &indices = + dex.parameter_annotations[static_cast(parameter_annotations[k].method_idx)]; + for (size_t l = 0; l < parameter_annotation->size; ++l) { + if (parameter_annotation->list[l].annotations_off != 0) { + auto *parameter_annotation_item = dex.dataPtr( + parameter_annotation->list[l].annotations_off); + ParseAnnotationSet(dex, annotation_list, array_list, indices, + parameter_annotation_item); + } + // A kNoIndex entry serves as a separator between parameter annotation sets. + indices.emplace_back(dex::kNoIndex); + } + } + } + + // If annotations were skipped, we are done. + if (!include_annotations) return out; + + // 7. Convert parsed C++ annotation structures to Java objects. + auto out5 = env->NewIntArray(static_cast(2 * annotation_list.size())); + auto out6 = + env->NewObjectArray(static_cast(2 * annotation_list.size()), object_class, nullptr); + auto out5_ptr = env->GetIntArrayElements(out5, nullptr); + size_t i = 0; + for (auto &[visibility, type, items] : annotation_list) { + auto out6i0 = env->NewIntArray(static_cast(2 * items.size())); + auto out6i0_ptr = env->GetIntArrayElements(out6i0, nullptr); + auto out6i1 = env->NewObjectArray(static_cast(items.size()), object_class, nullptr); + size_t j = 0; + for (auto &[name, value] : items) { + auto &[value_type, value_data] = value; + // The raw value data is passed in a direct ByteBuffer. + auto java_value = value_data.empty() + ? nullptr + : env->NewDirectByteBuffer(value_data.data(), value_data.size()); + env->SetObjectArrayElement(out6i1, static_cast(j), java_value); + out6i0_ptr[2 * j] = name; + out6i0_ptr[2 * j + 1] = value_type; + env->DeleteLocalRef(java_value); + ++j; + } + env->ReleaseIntArrayElements(out6i0, out6i0_ptr, 0); + env->SetObjectArrayElement(out6, static_cast(2 * i), out6i0); + env->SetObjectArrayElement(out6, static_cast(2 * i + 1), out6i1); + out5_ptr[2 * i] = visibility; + out5_ptr[2 * i + 1] = type; + env->DeleteLocalRef(out6i0); + env->DeleteLocalRef(out6i1); + ++i; + } + env->ReleaseIntArrayElements(out5, out5_ptr, 0); + env->SetObjectArrayElement(out, 5, out5); + env->SetObjectArrayElement(out, 6, out6); + env->DeleteLocalRef(out5); + env->DeleteLocalRef(out6); + + // 8. Convert parsed C++ array values to Java objects. + auto out7 = + env->NewObjectArray(static_cast(2 * array_list.size()), object_class, nullptr); + i = 0; + for (auto &array : array_list) { + auto out7i0 = env->NewIntArray(static_cast(array.size())); + auto out7i0_ptr = env->GetIntArrayElements(out7i0, nullptr); + auto out7i1 = env->NewObjectArray(static_cast(array.size()), object_class, nullptr); + size_t j = 0; + for (auto &value : array) { + auto &[value_type, value_data] = value; + auto java_value = value_data.empty() + ? nullptr + : env->NewDirectByteBuffer(value_data.data(), value_data.size()); + out7i0_ptr[j] = value_type; + env->SetObjectArrayElement(out7i1, static_cast(j), java_value); + env->DeleteLocalRef(java_value); + ++j; + } + env->ReleaseIntArrayElements(out7i0, out7i0_ptr, 0); + env->SetObjectArrayElement(out7, static_cast(2 * i), out7i0); + env->SetObjectArrayElement(out7, static_cast(2 * i + 1), out7i1); + env->DeleteLocalRef(out7i0); + env->DeleteLocalRef(out7i1); + ++i; + } + env->SetObjectArrayElement(out, 7, out7); + env->DeleteLocalRef(out7); + + return out; +} + +/** + * @brief JNI method to release the native DexParser object. + * @param cookie The pointer to the DexParser object created by `openDex`. + */ +VECTOR_DEF_NATIVE_METHOD(void, DexParserBridge, closeDex, jlong cookie) { + if (cookie != 0) delete reinterpret_cast(cookie); +} + +/** + * @brief Iterates through classes, fields, and methods, calling back to a Java + * visitor. + * @param cookie The pointer to the DexParser object. + * @param visitor The main Java visitor object. + * @param ...visitor_class/.._method Java reflection objects used to + * get method IDs and perform type checks. + */ +VECTOR_DEF_NATIVE_METHOD(void, DexParserBridge, visitClass, jlong cookie, jobject visitor, + jclass field_visitor_class, jclass method_visitor_class, + jobject class_visit_method, jobject field_visit_method, + jobject method_visit_method, jobject method_body_visit_method, + jobject stop_method) { + // Constants for DEX opcodes used in method body parsing. + static constexpr dex::u1 kOpcodeMask = 0xff; + static constexpr dex::u1 kOpcodeNoOp = 0x00; + static constexpr dex::u1 kOpcodeConstString = 0x1a; + static constexpr dex::u1 kOpcodeConstStringJumbo = 0x1b; + static constexpr dex::u1 kOpcodeIGetStart = 0x52; + static constexpr dex::u1 kOpcodeIGetEnd = 0x58; + static constexpr dex::u1 kOpcodeSGetStart = 0x60; + static constexpr dex::u1 kOpcodeSGetEnd = 0x66; + static constexpr dex::u1 kOpcodeIPutStart = 0x59; + static constexpr dex::u1 kOpcodeIPutEnd = 0x5f; + static constexpr dex::u1 kOpcodeSPutStart = 0x67; + static constexpr dex::u1 kOpcodeSPutEnd = 0x6d; + static constexpr dex::u1 kOpcodeInvokeStart = 0x6e; + static constexpr dex::u1 kOpcodeInvokeEnd = 0x72; + static constexpr dex::u1 kOpcodeInvokeRangeStart = 0x74; + static constexpr dex::u1 kOpcodeInvokeRangeEnd = 0x78; + // Constants for special "payload" opcodes that follow a NOP instruction. + static constexpr dex::u2 kInstPackedSwitchPlayLoad = 0x0100; + static constexpr dex::u2 kInstSparseSwitchPlayLoad = 0x0200; + static constexpr dex::u2 kInstFillArrayDataPlayLoad = 0x0300; + + if (cookie == 0) { + return; + } + auto &dex = *reinterpret_cast(cookie); + // Get jmethodIDs from the reflected java.lang.reflect.Method objects. + auto *visit_class = env->FromReflectedMethod(class_visit_method); + auto *visit_field = env->FromReflectedMethod(field_visit_method); + auto *visit_method = env->FromReflectedMethod(method_visit_method); + auto *visit_method_body = env->FromReflectedMethod(method_body_visit_method); + auto *stop = env->FromReflectedMethod(stop_method); + + auto classes = dex.ClassDefs(); + + for (size_t i = 0; i < classes.size(); ++i) { + auto &class_def = classes[i]; + auto &class_data = dex.class_data[i]; + + // --- Prepare arguments for the visit_class callback --- + // This involves converting C++ vectors of integers into Java int arrays. + auto interfaces = env->NewIntArray(static_cast(class_data.interfaces.size())); + env->SetIntArrayRegion(interfaces, 0, static_cast(class_data.interfaces.size()), + class_data.interfaces.data()); + auto static_fields = env->NewIntArray(static_cast(class_data.static_fields.size())); + env->SetIntArrayRegion(static_fields, 0, static_cast(class_data.static_fields.size()), + class_data.static_fields.data()); + auto static_fields_access_flags = + env->NewIntArray(static_cast(class_data.static_fields_access_flags.size())); + env->SetIntArrayRegion(static_fields_access_flags, 0, + static_cast(class_data.static_fields_access_flags.size()), + class_data.static_fields_access_flags.data()); + auto instance_fields = + env->NewIntArray(static_cast(class_data.instance_fields.size())); + env->SetIntArrayRegion(instance_fields, 0, + static_cast(class_data.instance_fields.size()), + class_data.instance_fields.data()); + auto instance_fields_access_flags = + env->NewIntArray(static_cast(class_data.instance_fields_access_flags.size())); + env->SetIntArrayRegion(instance_fields_access_flags, 0, + static_cast(class_data.instance_fields_access_flags.size()), + class_data.instance_fields_access_flags.data()); + auto direct_methods = env->NewIntArray(static_cast(class_data.direct_methods.size())); + env->SetIntArrayRegion(direct_methods, 0, + static_cast(class_data.direct_methods.size()), + class_data.direct_methods.data()); + auto direct_methods_access_flags = + env->NewIntArray(static_cast(class_data.direct_methods_access_flags.size())); + env->SetIntArrayRegion(direct_methods_access_flags, 0, + static_cast(class_data.direct_methods_access_flags.size()), + class_data.direct_methods_access_flags.data()); + auto virtual_methods = + env->NewIntArray(static_cast(class_data.virtual_methods.size())); + env->SetIntArrayRegion(virtual_methods, 0, + static_cast(class_data.virtual_methods.size()), + class_data.virtual_methods.data()); + auto virtual_methods_access_flags = + env->NewIntArray(static_cast(class_data.virtual_methods_access_flags.size())); + env->SetIntArrayRegion(virtual_methods_access_flags, 0, + static_cast(class_data.virtual_methods_access_flags.size()), + class_data.virtual_methods_access_flags.data()); + auto class_annotations = env->NewIntArray(static_cast(class_data.annotations.size())); + env->SetIntArrayRegion(class_annotations, 0, + static_cast(class_data.annotations.size()), + class_data.annotations.data()); + + // --- Call back to the Java visitor for the class --- + jobject member_visitor = env->CallObjectMethod( + visitor, visit_class, static_cast(class_def.class_idx), + static_cast(class_def.access_flags), static_cast(class_def.superclass_idx), + interfaces, static_cast(class_def.source_file_idx), static_fields, + static_fields_access_flags, instance_fields, instance_fields_access_flags, + direct_methods, direct_methods_access_flags, virtual_methods, + virtual_methods_access_flags, class_annotations); + + // --- Clean up local JNI references --- + env->DeleteLocalRef(interfaces); + env->DeleteLocalRef(static_fields); + env->DeleteLocalRef(static_fields_access_flags); + env->DeleteLocalRef(instance_fields); + env->DeleteLocalRef(instance_fields_access_flags); + env->DeleteLocalRef(direct_methods); + env->DeleteLocalRef(direct_methods_access_flags); + env->DeleteLocalRef(virtual_methods); + env->DeleteLocalRef(virtual_methods_access_flags); + env->DeleteLocalRef(class_annotations); + + // --- Visit fields --- + if (member_visitor && env->IsInstanceOf(member_visitor, field_visitor_class)) { + jboolean stopped = JNI_FALSE; + // This structured binding provides a clean way to iterate over both + // static and instance field collections. + for (auto &[fields, fields_access_flags] : + {std::tie(class_data.static_fields, class_data.static_fields_access_flags), + std::tie(class_data.instance_fields, class_data.instance_fields_access_flags)}) { + for (size_t j = 0; j < fields.size(); j++) { + auto field_idx = fields[j]; + auto access_flags = fields_access_flags[j]; + auto &field_annotations = dex.field_annotations[field_idx]; + auto annotations = + env->NewIntArray(static_cast(field_annotations.size())); + env->SetIntArrayRegion(annotations, 0, + static_cast(field_annotations.size()), + field_annotations.data()); + // Call back to Java for this field. + env->CallVoidMethod(member_visitor, visit_field, field_idx, access_flags, + annotations); + env->DeleteLocalRef(annotations); + // Check if the visitor wants to stop iteration. + stopped = env->CallBooleanMethod(member_visitor, stop); + if (stopped == JNI_TRUE) break; + } + if (stopped == JNI_TRUE) break; + } + } + + // --- Visit methods --- + if (member_visitor && env->IsInstanceOf(member_visitor, method_visitor_class)) { + jboolean stopped = JNI_FALSE; + // Iterate over both direct and virtual methods. + for (auto &[methods, methods_access_flags, methods_code] : + {std::tie(class_data.direct_methods, class_data.direct_methods_access_flags, + class_data.direct_methods_code), + std::tie(class_data.virtual_methods, class_data.virtual_methods_access_flags, + class_data.virtual_methods_code)}) { + for (size_t j = 0; j < methods.size(); j++) { + auto method_idx = methods[j]; + auto access_flags = methods_access_flags[j]; + auto code = methods_code[j]; + auto method_annotation = dex.method_annotations[method_idx]; + auto method_annotations = + env->NewIntArray(static_cast(method_annotation.size())); + env->SetIntArrayRegion(method_annotations, 0, + static_cast(method_annotation.size()), + method_annotation.data()); + auto parameter_annotation = dex.parameter_annotations[method_idx]; + auto parameter_annotations = + env->NewIntArray(static_cast(parameter_annotation.size())); + env->SetIntArrayRegion(parameter_annotations, 0, + static_cast(parameter_annotation.size()), + parameter_annotation.data()); + // Call back to Java for this method. + // This may return a "body visitor". + auto body_visitor = env->CallObjectMethod( + member_visitor, visit_method, method_idx, access_flags, code != nullptr, + method_annotations, parameter_annotations); + env->DeleteLocalRef(method_annotations); + env->DeleteLocalRef(parameter_annotations); + // --- Lazily parse the method body if requested --- + if (body_visitor && code != nullptr) { + auto &body = dex.method_bodies[method_idx]; + if (!body.loaded) { + // Using hash sets for efficient collection of unique indices. + phmap::flat_hash_set referred_strings; + phmap::flat_hash_set assigned_fields; + phmap::flat_hash_set accessed_fields; + phmap::flat_hash_set invoked_methods; + + const dex::u2 *inst = code->insns; + const dex::u2 *end = inst + code->insns_size; + // Iterate through the bytecode instructions. + while (inst < end) { + dex::u1 opcode = *inst & kOpcodeMask; + body.opcodes.push_back(static_cast(opcode)); + // Check for opcodes of interest. + if (opcode == kOpcodeConstString) { + auto str_idx = inst[1]; + referred_strings.emplace(str_idx); + } + if (opcode == kOpcodeConstStringJumbo) { + auto str_idx = *reinterpret_cast(&inst[1]); + referred_strings.emplace(static_cast(str_idx)); + } + if ((opcode >= kOpcodeIGetStart && opcode <= kOpcodeIGetEnd) || + (opcode >= kOpcodeSGetStart && opcode <= kOpcodeSGetEnd)) { + auto field_idx = inst[1]; + accessed_fields.emplace(field_idx); + } + if ((opcode >= kOpcodeIPutStart && opcode <= kOpcodeIPutEnd) || + (opcode >= kOpcodeSPutStart && opcode <= kOpcodeSPutEnd)) { + auto field_idx = inst[1]; + assigned_fields.emplace(field_idx); + } + if ((opcode >= kOpcodeInvokeStart && opcode <= kOpcodeInvokeEnd) || + (opcode >= kOpcodeInvokeRangeStart && + opcode <= kOpcodeInvokeRangeEnd)) { + auto callee = inst[1]; + invoked_methods.emplace(callee); + } + // Handle special payload instructions which have variable + // length. + if (opcode == kOpcodeNoOp) { + if (*inst == kInstPackedSwitchPlayLoad) { + inst += inst[1] * 2 + 3; + } else if (*inst == kInstSparseSwitchPlayLoad) { + inst += inst[1] * 4 + 1; + } else if (*inst == kInstFillArrayDataPlayLoad) { + inst += (*reinterpret_cast(&inst[2]) * + inst[1] + + 1) / + 2 + + 3; + } + } + // Advance instruction pointer by the known length of + // the current opcode. + inst += dex::opcode_len[opcode]; + } + // Copy the collected unique indices into the body's vectors. + body.referred_strings.assign(referred_strings.begin(), + referred_strings.end()); + body.assigned_fields.assign(assigned_fields.begin(), + assigned_fields.end()); + body.accessed_fields.assign(accessed_fields.begin(), + accessed_fields.end()); + body.invoked_methods.assign(invoked_methods.begin(), + invoked_methods.end()); + body.loaded = true; + } + // --- Prepare arguments and call back for the method body --- + auto referred_strings = + env->NewIntArray(static_cast(body.referred_strings.size())); + env->SetIntArrayRegion(referred_strings, 0, + static_cast(body.referred_strings.size()), + body.referred_strings.data()); + auto accessed_fields = + env->NewIntArray(static_cast(body.accessed_fields.size())); + env->SetIntArrayRegion(accessed_fields, 0, + static_cast(body.accessed_fields.size()), + body.accessed_fields.data()); + auto assigned_fields = + env->NewIntArray(static_cast(body.assigned_fields.size())); + env->SetIntArrayRegion(assigned_fields, 0, + static_cast(body.assigned_fields.size()), + body.assigned_fields.data()); + auto invoked_methods = + env->NewIntArray(static_cast(body.invoked_methods.size())); + env->SetIntArrayRegion(invoked_methods, 0, + static_cast(body.invoked_methods.size()), + body.invoked_methods.data()); + auto opcodes = env->NewByteArray(static_cast(body.opcodes.size())); + env->SetByteArrayRegion(opcodes, 0, static_cast(body.opcodes.size()), + body.opcodes.data()); + env->CallVoidMethod(body_visitor, visit_method_body, referred_strings, + invoked_methods, accessed_fields, assigned_fields, + opcodes); + } + stopped = env->CallBooleanMethod(member_visitor, stop); + if (stopped == JNI_TRUE) break; + } + if (stopped == JNI_TRUE) break; + } + } + // Check if the top-level visitor wants to stop. + if (env->CallBooleanMethod(visitor, stop) == JNI_TRUE) break; + } +} + +// Array of native method descriptors for JNI registration. +static JNINativeMethod gMethods[] = { + VECTOR_NATIVE_METHOD(DexParserBridge, openDex, "(Ljava/nio/ByteBuffer;[J)Ljava/lang/Object;"), + VECTOR_NATIVE_METHOD(DexParserBridge, closeDex, "(J)V"), + VECTOR_NATIVE_METHOD(DexParserBridge, visitClass, + "(JLjava/lang/Object;Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/" + "reflect/Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/" + "Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"), +}; + +/** + * @brief Registers the native methods with the JVM. + */ +void RegisterDexParserBridge(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(DexParserBridge); } + +} // namespace vector::native::jni diff --git a/native/src/jni/hook_bridge.cpp b/native/src/jni/hook_bridge.cpp new file mode 100644 index 000000000..38cc45faf --- /dev/null +++ b/native/src/jni/hook_bridge.cpp @@ -0,0 +1,575 @@ +#include + +#include +#include +#include + +#include "jni/jni_bridge.h" +#include "jni/jni_hooks.h" + +namespace { +/** + * @struct ModuleCallback + * @brief Stores the jmethodIDs for the "modern" callback API. + * This API separates the logic that runs before and after the original method. + */ +struct ModuleCallback { + jmethodID before_method; + jmethodID after_method; +}; + +/** + * @struct HookItem + * @brief Holds all state associated with a single hooked method. + * + * This includes lists of all registered callback functions + * (both modern and legacy), sorted by priority. + * + * It also manages a thread-safe "backup" object, + * which is a handle to the original, un-hooked method. + */ +struct HookItem { + // Callbacks are stored in multimaps, keyed by priority. + // std::greater<> ensures that higher priority numbers are processed first. + std::multimap> legacy_callbacks; + std::multimap> modern_callbacks; + +private: + // The backup is an atomic jobject. + // This is crucial for thread safety during the initial hooking process. + // It can be in one of three states: + // - nullptr: The hook has not been initialized yet. + // - FAILED: The hook attempt failed. + // - A valid jobject: A handle to the original method. + std::atomic backup{nullptr}; + static_assert(decltype(backup)::is_always_lock_free); + // A sentinel value to indicate that the hooking process failed. + inline static jobject FAILED = reinterpret_cast(std::numeric_limits::max()); + +public: + /** + * @brief Atomically and safely retrieves the backup method handle. + * If another thread is currently setting up the hook, this method will wait until + * the process is complete, to prevent race conditions. + */ + jobject GetBackup() { + // Wait until the 'backup' atomic is no longer nullptr. + backup.wait(nullptr, std::memory_order_acquire); + if (auto bk = backup.load(std::memory_order_relaxed); bk != FAILED) { + return bk; + } else { + return nullptr; + } + } + + /** + * @brief Atomically sets the backup method handle once after hooking. + * This method uses compare_exchange_strong to ensure it only sets the value once. + * After setting, it notifies any waiting threads. + */ + void SetBackup(jobject newBackup) { + jobject null = nullptr; + // Attempt to transition from nullptr to the new backup (or FAILED). + // memory_order_acq_rel ensures memory synchronization + // with both waiting threads (acquire) and subsequent reads (release). + backup.compare_exchange_strong(null, newBackup ? newBackup : FAILED, + std::memory_order_acq_rel, std::memory_order_relaxed); + // Wake up all threads that were waiting in GetBackup(). + backup.notify_all(); + } +}; + +// A type alias for a thread-safe parallel hash map. +// This map is the central registry, mapping a method's ID to its HookItem. +// It uses a std::shared_mutex to allow concurrent reads but exclusive writes. +template , + class Eq = phmap::priv::hash_default_eq, + class Alloc = phmap::priv::Allocator>, size_t N = 4> +using SharedHashMap = phmap::parallel_flat_hash_map; + +// The global map of all hooked methods. +SharedHashMap> hooked_methods; + +// Cached JNI method and field IDs for performance. +// Looking these up frequently is slow, so they are cached on first use. +jmethodID invoke = nullptr; +jmethodID callback_ctor = nullptr; +jfieldID before_method_field = nullptr; +jfieldID after_method_field = nullptr; +} // namespace + +namespace vector::native::jni { +/** + * @brief JNI method to install a hook on a given method or constructor. + * @param useModernApi Distinguishes between the legacy and modern callback + * types. + * @param hookMethod The java.lang.reflect.Executable to be hooked. + * @param hooker The Java class that acts as the hook trampoline. + * @param priority The priority of this callback. + * @param callback The Java callback object. + * @return JNI_TRUE on success, JNI_FALSE on failure. + */ +VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jboolean useModernApi, + jobject hookMethod, jclass hooker, jint priority, jobject callback) { + bool newHook = false; + +#ifndef NDEBUG + // Simple RAII struct for performance timing in debug builds. + struct finally { + std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); + bool &newHook; + ~finally() { + auto finish = std::chrono::steady_clock::now(); + if (newHook) { + LOGV("New hook took {}us", + std::chrono::duration_cast(finish - start).count()); + } + } + } finally{.newHook = newHook}; +#endif + + auto target = env->FromReflectedMethod(hookMethod); + HookItem *hook_item = nullptr; + + // Atomically find or create an entry for the target method. + // This is a highly concurrent operation. + hooked_methods.lazy_emplace_l( + target, + // Lambda for existing element: just get the pointer. + [&hook_item](auto &it) { hook_item = it.second.get(); }, + // Lambda for new element: create the HookItem and mark it as a new hook. + [&hook_item, &target, &newHook](const auto &ctor) { + auto ptr = std::make_unique(); + hook_item = ptr.get(); + ctor(target, std::move(ptr)); + newHook = true; + }); + + // If this is the first time this method is being hooked, + // we need to perform the actual native hook using lsplant. + if (newHook) { + auto init = env->GetMethodID(hooker, "", "(Ljava/lang/reflect/Executable;)V"); + auto callback_method = env->ToReflectedMethod( + hooker, env->GetMethodID(hooker, "callback", "([Ljava/lang/Object;)Ljava/lang/Object;"), + false); + auto hooker_object = env->NewObject(hooker, init, hookMethod); + // Use lsplant to replace the target method with our trampoline. + // The returned jobject is a handle to the original method. + hook_item->SetBackup(lsplant::Hook(env, hookMethod, hooker_object, callback_method)); + env->DeleteLocalRef(hooker_object); + } + + // Wait for the backup to become available (it might be set by another thread). + jobject backup = hook_item->GetBackup(); + if (!backup) return JNI_FALSE; + + // Use an RAII monitor to lock the backup object, + // ensuring thread-safe modification of the callback lists. + lsplant::JNIMonitor monitor(env, backup); + + if (useModernApi) { + // Lazy initialization of JNI IDs for the modern API. + if (before_method_field == nullptr) { + auto callback_class = env->GetObjectClass(callback); + callback_ctor = + env->GetMethodID(callback_class, "", + "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"); + before_method_field = + env->GetFieldID(callback_class, "beforeInvocation", "Ljava/lang/reflect/Method;"); + after_method_field = + env->GetFieldID(callback_class, "afterInvocation", "Ljava/lang/reflect/Method;"); + } + // Extract the before/after methods from the Java callback object. + auto before_method = env->GetObjectField(callback, before_method_field); + auto after_method = env->GetObjectField(callback, after_method_field); + auto callback_type = ModuleCallback{ + .before_method = env->FromReflectedMethod(before_method), + .after_method = env->FromReflectedMethod(after_method), + }; + hook_item->modern_callbacks.emplace(priority, callback_type); + } else { + // For the legacy API, store a global reference to the callback object itself. + hook_item->legacy_callbacks.emplace(priority, env->NewGlobalRef(callback)); + } + return JNI_TRUE; +} + +/** + * @brief JNI method to remove a previously installed hook callback. + */ +VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jboolean useModernApi, + jobject hookMethod, jobject callback) { + auto target = env->FromReflectedMethod(hookMethod); + HookItem *hook_item = nullptr; + // Find the HookItem for the target method. + hooked_methods.if_contains(target, + [&hook_item](const auto &it) { hook_item = it.second.get(); }); + if (!hook_item) return JNI_FALSE; + + jobject backup = hook_item->GetBackup(); + if (!backup) return JNI_FALSE; + + // Lock to safely modify the callback list. + lsplant::JNIMonitor monitor(env, backup); + + if (useModernApi) { + auto before_method = env->GetObjectField(callback, before_method_field); + auto before = env->FromReflectedMethod(before_method); + // Find the callback by comparing the before_method's ID. + for (auto i = hook_item->modern_callbacks.begin(); i != hook_item->modern_callbacks.end(); + ++i) { + if (before == i->second.before_method) { + hook_item->modern_callbacks.erase(i); + return JNI_TRUE; + } + } + } else { + // Find the callback by comparing the jobject directly. + for (auto i = hook_item->legacy_callbacks.begin(); i != hook_item->legacy_callbacks.end(); + ++i) { + if (env->IsSameObject(i->second, callback)) { + env->DeleteGlobalRef(i->second); // Clean up the global reference. + hook_item->legacy_callbacks.erase(i); + return JNI_TRUE; + } + } + } + return JNI_FALSE; +} + +/** + * @brief JNI method to request de-optimization of a method. + * This can be necessary for some types of hooks to work correctly on JIT-compiled methods. + */ +VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, deoptimizeMethod, jobject hookMethod) { + return lsplant::Deoptimize(env, hookMethod); +} + +/** + * @brief JNI method to invoke the original, un-hooked method. + */ +VECTOR_DEF_NATIVE_METHOD(jobject, HookBridge, invokeOriginalMethod, jobject hookMethod, + jobject thiz, jobjectArray args) { + auto target = env->FromReflectedMethod(hookMethod); + HookItem *hook_item = nullptr; + hooked_methods.if_contains(target, + [&hook_item](const auto &it) { hook_item = it.second.get(); }); + + // If a hook item exists, invoke its backup. Otherwise, invoke the method directly + // (though this case should be rare if called from a hook callback). + jobject method_to_invoke = hook_item ? hook_item->GetBackup() : hookMethod; + if (!method_to_invoke) { + // Hooking might have failed or is not complete. + return nullptr; + } + return env->CallObjectMethod(method_to_invoke, invoke, thiz, args); +} + +/** + * @brief JNI wrapper around AllocObject. + */ +VECTOR_DEF_NATIVE_METHOD(jobject, HookBridge, allocateObject, jclass cls) { + return env->AllocObject(cls); +} + +/** + * @brief A high-performance, low-level implementation of Method.invoke for super.method() calls. + * + * This function manually unboxes arguments from a jobject array into a jvalue C-style array, + * calls the appropriate JNI `CallNonvirtual...MethodA` function, + * and then boxes the return value back into a jobject. + * This avoids the overhead of Java reflection. + * + * @warning This is a very sensitive function. + * The `shorty` descriptor must perfectly match the method's actual signature. + */ +VECTOR_DEF_NATIVE_METHOD(jobject, HookBridge, invokeSpecialMethod, jobject method, + jcharArray shorty, jclass cls, jobject thiz, jobjectArray args) { + // --- Cache all necessary MethodIDs for boxing/unboxing primitive wrappers + // --- This is a major performance optimization, done only once. + static auto *const get_int = + env->GetMethodID(env->FindClass("java/lang/Integer"), "intValue", "()I"); + static auto *const get_double = + env->GetMethodID(env->FindClass("java/lang/Double"), "doubleValue", "()D"); + static auto *const get_long = + env->GetMethodID(env->FindClass("java/lang/Long"), "longValue", "()J"); + static auto *const get_float = + env->GetMethodID(env->FindClass("java/lang/Float"), "floatValue", "()F"); + static auto *const get_short = + env->GetMethodID(env->FindClass("java/lang/Short"), "shortValue", "()S"); + static auto *const get_byte = + env->GetMethodID(env->FindClass("java/lang/Byte"), "byteValue", "()B"); + static auto *const get_char = + env->GetMethodID(env->FindClass("java/lang/Character"), "charValue", "()C"); + static auto *const get_boolean = + env->GetMethodID(env->FindClass("java/lang/Boolean"), "booleanValue", "()Z"); + static auto *const set_int = env->GetStaticMethodID(env->FindClass("java/lang/Integer"), + "valueOf", "(I)Ljava/lang/Integer;"); + static auto *const set_double = env->GetStaticMethodID(env->FindClass("java/lang/Double"), + "valueOf", "(D)Ljava/lang/Double;"); + static auto *const set_long = + env->GetStaticMethodID(env->FindClass("java/lang/Long"), "valueOf", "(J)Ljava/lang/Long;"); + static auto *const set_float = env->GetStaticMethodID(env->FindClass("java/lang/Float"), + "valueOf", "(F)Ljava/lang/Float;"); + static auto *const set_short = env->GetStaticMethodID(env->FindClass("java/lang/Short"), + "valueOf", "(S)Ljava/lang/Short;"); + static auto *const set_byte = + env->GetStaticMethodID(env->FindClass("java/lang/Byte"), "valueOf", "(B)Ljava/lang/Byte;"); + static auto *const set_char = env->GetStaticMethodID(env->FindClass("java/lang/Character"), + "valueOf", "(C)Ljava/lang/Character;"); + static auto *const set_boolean = env->GetStaticMethodID(env->FindClass("java/lang/Boolean"), + "valueOf", "(Z)Ljava/lang/Boolean;"); + + auto target = env->FromReflectedMethod(method); + auto param_len = env->GetArrayLength(shorty) - 1; // First char is return type. + + // --- Argument Validation --- + if (env->GetArrayLength(args) != param_len) { + env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), + "args.length does not match parameter count"); + return nullptr; + } + if (thiz == nullptr) { + env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), + "`this` cannot be null for a non-virtual call"); + return nullptr; + } + + // --- Unbox Arguments --- + std::vector a(param_len); + auto *const shorty_char = env->GetCharArrayElements(shorty, nullptr); + for (jint i = 0; i != param_len; ++i) { + jobject element = env->GetObjectArrayElement(args, i); + if (env->ExceptionCheck()) { + env->ReleaseCharArrayElements(shorty, shorty_char, JNI_ABORT); + return nullptr; + } + + // The shorty string at index i+1 describes the type of the i-th parameter. + switch (shorty_char[i + 1]) { + case 'I': + a[i].i = env->CallIntMethod(element, get_int); + break; + case 'D': + a[i].d = env->CallDoubleMethod(element, get_double); + break; + case 'J': + a[i].j = env->CallLongMethod(element, get_long); + break; + case 'F': + a[i].f = env->CallFloatMethod(element, get_float); + break; + case 'S': + a[i].s = env->CallShortMethod(element, get_short); + break; + case 'B': + a[i].b = env->CallByteMethod(element, get_byte); + break; + case 'C': + a[i].c = env->CallCharMethod(element, get_char); + break; + case 'Z': + a[i].z = env->CallBooleanMethod(element, get_boolean); + break; + default: // Assumes 'L' or '[' for object types + a[i].l = element; + // Set element to null so we don't delete the local ref twice. + // The reference is stored in the jvalue array and is still valid. + element = nullptr; + break; + } + + // Clean up the local reference for the wrapper object if it was created. + if (element) env->DeleteLocalRef(element); + + // Check for exceptions during the unboxing call (e.g., + // NullPointerException). + if (env->ExceptionCheck()) { + env->ReleaseCharArrayElements(shorty, shorty_char, JNI_ABORT); + return nullptr; + } + } + + // --- Call Non-virtual Method and Box Return Value --- + jobject value = nullptr; + // The shorty string at index 0 describes the return type. + switch (shorty_char[0]) { + case 'I': + value = + env->CallStaticObjectMethod(jclass{nullptr}, + set_int, // Use Integer.valueOf() to box + env->CallNonvirtualIntMethodA(thiz, cls, target, a.data())); + break; + case 'D': + value = env->CallStaticObjectMethod( + jclass{nullptr}, set_double, + env->CallNonvirtualDoubleMethodA(thiz, cls, target, a.data())); + break; + case 'J': + value = env->CallStaticObjectMethod( + jclass{nullptr}, set_long, env->CallNonvirtualLongMethodA(thiz, cls, target, a.data())); + break; + case 'F': + value = env->CallStaticObjectMethod( + jclass{nullptr}, set_float, + env->CallNonvirtualFloatMethodA(thiz, cls, target, a.data())); + break; + case 'S': + value = env->CallStaticObjectMethod( + jclass{nullptr}, set_short, + env->CallNonvirtualShortMethodA(thiz, cls, target, a.data())); + break; + case 'B': + value = env->CallStaticObjectMethod( + jclass{nullptr}, set_byte, env->CallNonvirtualByteMethodA(thiz, cls, target, a.data())); + break; + case 'C': + value = env->CallStaticObjectMethod( + jclass{nullptr}, set_char, env->CallNonvirtualCharMethodA(thiz, cls, target, a.data())); + break; + case 'Z': + value = env->CallStaticObjectMethod( + jclass{nullptr}, set_boolean, + env->CallNonvirtualBooleanMethodA(thiz, cls, target, a.data())); + break; + case 'L': // Return type is an object, no boxing needed. + value = env->CallNonvirtualObjectMethodA(thiz, cls, target, a.data()); + break; + default: // Assumes 'V' for void return type. + case 'V': + env->CallNonvirtualVoidMethodA(thiz, cls, target, a.data()); + break; + } + + env->ReleaseCharArrayElements(shorty, shorty_char, JNI_ABORT); + return value; +} + +/** + * @brief JNI wrapper around IsInstanceOf. + */ +VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, instanceOf, jobject object, jclass expected_class) { + return env->IsInstanceOf(object, expected_class); +} + +/** + * @brief JNI wrapper to mark a DEX file loaded from memory as trusted. + */ +VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, setTrusted, jobject cookie) { + return lsplant::MakeDexFileTrusted(env, cookie); +} + +/** + * @brief Creates a snapshot of all registered callbacks for a given method. + * This is useful for debugging and introspection from the Java side. + + * @return An Object[2][] array where index 0 contains modern callbacks and + * index 1 contains legacy callbacks. + */ +VECTOR_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass callback_class, + jobject method) { + auto target = env->FromReflectedMethod(method); + HookItem *hook_item = nullptr; + hooked_methods.if_contains(target, + [&hook_item](const auto &it) { hook_item = it.second.get(); }); + if (!hook_item) return nullptr; + + jobject backup = hook_item->GetBackup(); + if (!backup) return nullptr; + + // Lock to ensure a consistent snapshot of the callback lists. + lsplant::JNIMonitor monitor(env, backup); + + auto res = env->NewObjectArray(2, env->FindClass("[Ljava/lang/Object;"), nullptr); + auto modern = env->NewObjectArray((jsize)hook_item->modern_callbacks.size(), + env->FindClass("java/lang/Object"), nullptr); + auto legacy = env->NewObjectArray((jsize)hook_item->legacy_callbacks.size(), + env->FindClass("java/lang/Object"), nullptr); + + jsize i = 0; + for (const auto &callback_pair : hook_item->modern_callbacks) { + // The clazz argument refers to the Java class where the native method is + // declared, provided by the macro VECTOR_DEF_NATIVE_METHOD. + auto before_method = + env->ToReflectedMethod(clazz, callback_pair.second.before_method, JNI_FALSE); + auto after_method = + env->ToReflectedMethod(clazz, callback_pair.second.after_method, JNI_FALSE); + // Re-create the Java callback object from the stored method IDs. + auto callback_object = + env->NewObject(callback_class, callback_ctor, before_method, after_method); + env->SetObjectArrayElement(modern, i++, callback_object); + // Clean up local references created during object construction. + env->DeleteLocalRef(before_method); + env->DeleteLocalRef(after_method); + env->DeleteLocalRef(callback_object); + } + + i = 0; + for (const auto &callback_pair : hook_item->legacy_callbacks) { + // The legacy list already stores a global ref to the callback object. + env->SetObjectArrayElement(legacy, i++, callback_pair.second); + } + + env->SetObjectArrayElement(res, 0, modern); + env->SetObjectArrayElement(res, 1, legacy); + env->DeleteLocalRef(modern); + env->DeleteLocalRef(legacy); + return res; +} + +/** + * @brief Retrieves the static initializer () of a class as a Method object. + * @param target_class The class to inspect. + * @return A Method object for the static initializer, or null if it doesn't exist. + */ +VECTOR_DEF_NATIVE_METHOD(jobject, HookBridge, getStaticInitializer, jclass target_class) { + // is the internal name for a static initializer. + // Its signature is always ()V (no arguments, void return). + jmethodID mid = env->GetStaticMethodID(target_class, "", "()V"); + if (!mid) { + // If GetStaticMethodID fails, it throws an exception. + // We clear it and return null to let the Java side handle it gracefully. + env->ExceptionClear(); + return nullptr; + } + // Convert the method ID to a java.lang.reflect.Method object. + // The last parameter must be JNI_TRUE because it's a static method. + return env->ToReflectedMethod(target_class, mid, JNI_TRUE); +} + +// Array of native method descriptors for JNI registration. +static JNINativeMethod gMethods[] = { + VECTOR_NATIVE_METHOD(HookBridge, hookMethod, + "(ZLjava/lang/reflect/Executable;Ljava/lang/Class;ILjava/" + "lang/Object;)Z"), + VECTOR_NATIVE_METHOD(HookBridge, unhookMethod, + "(ZLjava/lang/reflect/Executable;Ljava/lang/Object;)Z"), + VECTOR_NATIVE_METHOD(HookBridge, deoptimizeMethod, "(Ljava/lang/reflect/Executable;)Z"), + VECTOR_NATIVE_METHOD(HookBridge, invokeOriginalMethod, + "(Ljava/lang/reflect/Executable;Ljava/lang/Object;[Ljava/" + "lang/Object;)Ljava/lang/Object;"), + VECTOR_NATIVE_METHOD(HookBridge, invokeSpecialMethod, + "(Ljava/lang/reflect/Executable;[CLjava/lang/Class;Ljava/" + "lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"), + VECTOR_NATIVE_METHOD(HookBridge, allocateObject, "(Ljava/lang/Class;)Ljava/lang/Object;"), + VECTOR_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"), + VECTOR_NATIVE_METHOD(HookBridge, setTrusted, "(Ljava/lang/Object;)Z"), + VECTOR_NATIVE_METHOD(HookBridge, callbackSnapshot, + "(Ljava/lang/Class;Ljava/lang/reflect/" + "Executable;)[[Ljava/lang/Object;"), + VECTOR_NATIVE_METHOD(HookBridge, getStaticInitializer, + "(Ljava/lang/Class;)Ljava/lang/reflect/Method;"), +}; + +/** + * @brief Registers all native methods with the JVM when the library is loaded. + */ +void RegisterHookBridge(JNIEnv *env) { + // Cache the Method.invoke methodID for use in invokeOriginalMethod. + jclass method = env->FindClass("java/lang/reflect/Method"); + invoke = env->GetMethodID(method, "invoke", + "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"); + env->DeleteLocalRef(method); + REGISTER_VECTOR_NATIVE_METHODS(HookBridge); +} +} // namespace vector::native::jni diff --git a/native/src/jni/native_api_bridge.cpp b/native/src/jni/native_api_bridge.cpp new file mode 100644 index 000000000..a051c0a70 --- /dev/null +++ b/native/src/jni/native_api_bridge.cpp @@ -0,0 +1,16 @@ +#include "core/native_api.h" +#include "jni/jni_bridge.h" +#include "jni/jni_hooks.h" + +namespace vector::native::jni { +VECTOR_DEF_NATIVE_METHOD(void, NativeAPI, recordNativeEntrypoint, jstring jstr) { + lsplant::JUTFString str(env, jstr); + vector::native::RegisterNativeLib(str); +} + +static JNINativeMethod gMethods[] = { + VECTOR_NATIVE_METHOD(NativeAPI, recordNativeEntrypoint, "(Ljava/lang/String;)V")}; + +void RegisterNativeApiBridge(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(NativeAPI); } + +} // namespace vector::native::jni diff --git a/native/src/jni/resources_hook.cpp b/native/src/jni/resources_hook.cpp new file mode 100644 index 000000000..ec43f7b56 --- /dev/null +++ b/native/src/jni/resources_hook.cpp @@ -0,0 +1,307 @@ +#include + +#include "common/config.h" +#include "common/utils.h" +#include "elf/elf_image.h" +#include "elf/symbol_cache.h" +#include "framework/android_types.h" +#include "jni/jni_bridge.h" +#include "jni/jni_hooks.h" + +namespace vector::native::jni { + +// --- Type Aliases for Native Android Framework Functions --- + +// Signature for android::ResXMLParser::getAttributeNameID(int) +using TYPE_GET_ATTR_NAME_ID = int32_t (*)(void *, int); +// Signature for android::ResStringPool::stringAt(int, size_t*) +using TYPE_STRING_AT = char16_t *(*)(const void *, int32_t, size_t *); +// Signature for android::ResXMLParser::restart() +using TYPE_RESTART = void (*)(void *); +// Signature for android::ResXMLParser::next() +using TYPE_NEXT = int32_t (*)(void *); + +// --- JNI Globals & Cached IDs --- +static jclass classXResources; +static jmethodID methodXResourcesTranslateAttrId; +static jmethodID methodXResourcesTranslateResId; + +// --- Native Function Pointers --- +// To store the memory addresses of the private Android framework functions. +static TYPE_NEXT ResXMLParser_next = nullptr; +static TYPE_RESTART ResXMLParser_restart = nullptr; +static TYPE_GET_ATTR_NAME_ID ResXMLParser_getAttributeNameID = nullptr; + +/** + * @brief Constructs the JNI class name for the XResources class at runtime. + * + * @return The JNI-style class name (e.g., "org/some/obfuscated/XResources"). + */ +static std::string GetXResourcesClassName() { + // Use a static local variable to ensure this lookup and string manipulation + // only happens once. + static std::string name = []() { + auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); + if (obfs_map.empty()) { + LOGW("GetXResourcesClassName: obfuscation_map is empty."); + } + // The key is the original, unobfuscated class name prefix. + // The value is the new, obfuscated prefix. + // TODO: The key "android.content.res.XRes" is hardcoded and fragile. + auto it = obfs_map.find("android.content.res.XRes"); + if (it == obfs_map.end()) { + LOGE("Could not find obfuscated name for XResources."); + return std::string(); + } + // The map gives something like "a.b.c." and we transform it into + // the full JNI class name "a/b/c/XResources". + std::string jni_name = JavaNameToSignature(it->second).substr(1); // "a/b/c/" + jni_name += "ources"; // This seems to be a hardcoded way to append "XResources" + LOGD("Resolved XResources class name to: {}", jni_name.c_str()); + return jni_name; + }(); + return name; +} + +/** + * @brief Finds and caches the addresses of private functions in libframework.so. + * + * It uses the ElfImage utility to parse the Android framework's shared library in memory, + * find functions by their C++ mangled names, and + * store their addresses in our global function pointers. + * + * @return True if all required symbols were found, false otherwise. + */ +static bool PrepareSymbols() { + ElfImage fw(kFrameworkLibraryName); + if (!fw.IsValid()) { + LOGE("Failed to open Android framework library."); + return false; + }; + + // The mangled names are specific to the compiler and architecture. + // This is a very fragile part of the hook. + + // Find android::ResXMLParser::next() + if (!(ResXMLParser_next = fw.getSymbAddress("_ZN7android12ResXMLParser4nextEv"))) { + LOGE("Failed to find symbol: ResXMLParser::next"); + return false; + } + // Find android::ResXMLParser::restart() + if (!(ResXMLParser_restart = + fw.getSymbAddress("_ZN7android12ResXMLParser7restartEv"))) { + LOGE("Failed to find symbol: ResXMLParser::restart"); + return false; + }; + // Find android::ResXMLParser::getAttributeNameID(unsigned int/long) + if (!(ResXMLParser_getAttributeNameID = fw.getSymbAddress( + LP_SELECT("_ZNK7android12ResXMLParser18getAttributeNameIDEj", + "_ZNK7android12ResXMLParser18getAttributeNameIDEm")))) { + LOGE("Failed to find symbol: ResXMLParser::getAttributeNameID"); + return false; + } + // Initialize another part of the resource framework that we depend on. + return android::ResStringPool::setup(lsplant::InitInfo{ + .art_symbol_resolver = [&](auto s) { return fw.template getSymbAddress<>(s); }}); +} + +/** + * @brief JNI entry point to initialize the entire native resources hook. + */ +VECTOR_DEF_NATIVE_METHOD(jboolean, ResourcesHook, initXResourcesNative) { + const auto x_resources_class_name = GetXResourcesClassName(); + if (x_resources_class_name.empty()) { + return JNI_FALSE; + } + + if (auto classXResources_ = + Context::GetInstance()->FindClassFromCurrentLoader(env, x_resources_class_name)) { + classXResources = JNI_NewGlobalRef(env, classXResources_); + } else { + LOGE("Error while loading XResources class '{}'", x_resources_class_name.c_str()); + return JNI_FALSE; + } + + // Dynamically build the method signature using the (possibly obfuscated) class name. + methodXResourcesTranslateResId = env->GetStaticMethodID( + classXResources, "translateResId", + fmt::format("(IL{};Landroid/content/res/Resources;)I", "L" + x_resources_class_name + ";") + .c_str()); + if (!methodXResourcesTranslateResId) { + LOGE("Failed to find method: XResources.translateResId"); + return JNI_FALSE; + } + + methodXResourcesTranslateAttrId = env->GetStaticMethodID( + classXResources, "translateAttrId", + fmt::format("(Ljava/lang/String;L{};)I", "L" + x_resources_class_name + ";").c_str()); + if (!methodXResourcesTranslateAttrId) { + LOGE("Failed to find method: XResources.translateAttrId"); + return JNI_FALSE; + } + + if (!PrepareSymbols()) { + LOGE("Failed to prepare native symbols for resource hooking."); + return JNI_FALSE; + } + return JNI_TRUE; +} + +/** + * @brief Removes the 'final' modifier from a Java class at runtime. + * This allows the framework to create subclasses of what are normally final classes. + */ +VECTOR_DEF_NATIVE_METHOD(jboolean, ResourcesHook, makeInheritable, jclass target_class) { + if (lsplant::MakeClassInheritable(env, target_class)) { + return JNI_TRUE; + } + return JNI_FALSE; +} + +/** + * @brief Builds a new ClassLoader in memory containing dynamically generated classes. + * + * This function creates a DEX file on-the-fly. + * The DEX file contains dummy classes that inherit from key Android resource classes. + * This allows the framework to inject its own logic by later creating classes that + * inherit from these dummies. + * + * @return A new dalvik.system.InMemoryDexClassLoader instance. + */ +VECTOR_DEF_NATIVE_METHOD(jobject, ResourcesHook, buildDummyClassLoader, jobject parent, + jstring resource_super_class, jstring typed_array_super_class) { + using namespace startop::dex; + + // Cache the class and constructor for InMemoryDexClassLoader. + static auto in_memory_classloader = + (jclass)env->NewGlobalRef(env->FindClass("dalvik/system/InMemoryDexClassLoader")); + static jmethodID initMid = env->GetMethodID(in_memory_classloader, "", + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); + + DexBuilder dex_file; + + // Create a class named "xposed.dummy.XResourcesSuperClass". + ClassBuilder xresource_builder{dex_file.MakeClass("xposed/dummy/XResourcesSuperClass")}; + // Set its superclass to the one specified by the Java caller. + xresource_builder.setSuperClass( + TypeDescriptor::FromClassname(lsplant::JUTFString(env, resource_super_class).get())); + + // Create a class named "xposed.dummy.XTypedArraySuperClass". + ClassBuilder xtypearray_builder{dex_file.MakeClass("xposed/dummy/XTypedArraySuperClass")}; + // Set its superclass. + xtypearray_builder.setSuperClass( + TypeDescriptor::FromClassname(lsplant::JUTFString(env, typed_array_super_class).get())); + + // Finalize the DEX file into a memory buffer. + slicer::MemView image{dex_file.CreateImage()}; + + // Wrap the memory buffer in a Java ByteBuffer. + auto dex_buffer = env->NewDirectByteBuffer(const_cast(image.ptr()), image.size()); + + // Create and return a new InMemoryDexClassLoader instance. + return env->NewObject(in_memory_classloader, initMid, dex_buffer, parent); +} + +/** + * @brief The core resource rewriting function. + * + * This method iterates through a binary XML file as it's being parsed by the Android framework. + * For each attribute and value, it calls back to Java to see + * if the resource ID should be replaced with a different one. + * + * @param parserPtr A raw pointer to the native android::ResXMLParser object. + * @param origRes The original XResources object. + * @param repRes The replacement Resources object. + */ +VECTOR_DEF_NATIVE_METHOD(void, ResourcesHook, rewriteXmlReferencesNative, jlong parserPtr, + jobject origRes, jobject repRes) { + // Cast the long from Java back to a native C++ pointer. + // This is dangerous and assumes the Java code provides a valid pointer. + auto parser = (android::ResXMLParser *)parserPtr; + + if (parser == nullptr) return; + + const android::ResXMLTree &mTree = parser->mTree; + auto mResIds = (uint32_t *)mTree.mResIds; + android::ResXMLTree_attrExt *tag; + int attrCount; + + // This loop iterates through all tokens in the binary XML file. + do { + // Call the native android::ResXMLParser::next() function via our pointer. + switch (ResXMLParser_next(parser)) { + case android::ResXMLParser::START_TAG: + tag = (android::ResXMLTree_attrExt *)parser->mCurExt; + attrCount = tag->attributeCount; + // Loop through all attributes of the current XML tag. + for (int idx = 0; idx < attrCount; idx++) { + auto attr = + (android::ResXMLTree_attribute *)(((const uint8_t *)tag) + tag->attributeStart + + tag->attributeSize * idx); + + // Translate the attribute name's resource ID --- + // e.g., for 'android:textColor', translate the ID for 'textColor'. + int32_t attrNameID = ResXMLParser_getAttributeNameID(parser, idx); + + // Only replace IDs that belong to the app's package (0x7f...). + if (attrNameID >= 0 && (size_t)attrNameID < mTree.mNumResIds && + mResIds[attrNameID] >= 0x7f000000) { + auto attrName = mTree.mStrings.stringAt(attrNameID); + jstring attrNameStr = + env->NewString((const jchar *)attrName.data_, attrName.length_); + if (env->ExceptionCheck()) goto leave; // Critical check + + // Call back to Java: XResources.translateAttrId(String name, ...) + jint attrResID = env->CallStaticIntMethod( + classXResources, methodXResourcesTranslateAttrId, attrNameStr, origRes); + env->DeleteLocalRef(attrNameStr); + if (env->ExceptionCheck()) goto leave; + + // Directly modify the resource ID table in the parser's memory. + mResIds[attrNameID] = attrResID; + } + + // Translate the attribute's value if it's a reference --- + // e.g., for 'android:textColor="@color/my_text"', translate the ID for + // '@color/my_text'. + if (attr->typedValue.dataType != android::Res_value::TYPE_REFERENCE) continue; + + jint oldValue = attr->typedValue.data; + if (oldValue < 0x7f000000) continue; + + // Call back to Java: XResources.translateResId(int id, ...) + jint newValue = env->CallStaticIntMethod( + classXResources, methodXResourcesTranslateResId, oldValue, origRes, repRes); + if (env->ExceptionCheck()) goto leave; + + // If the ID was changed, update the value directly in the parser's + // memory. + if (newValue != oldValue) attr->typedValue.data = newValue; + } + continue; + case android::ResXMLParser::END_DOCUMENT: + case android::ResXMLParser::BAD_DOCUMENT: + goto leave; // Exit the loop. + default: + continue; // Process next XML token. + } + } while (true); + +// A single exit point for the function. +leave: + // Reset the parser to its initial state so it can be read again. + ResXMLParser_restart(parser); +} + +// JNI method registration table. +static JNINativeMethod gMethods[] = { + VECTOR_NATIVE_METHOD(ResourcesHook, initXResourcesNative, "()Z"), + VECTOR_NATIVE_METHOD(ResourcesHook, makeInheritable, "(Ljava/lang/Class;)Z"), + VECTOR_NATIVE_METHOD(ResourcesHook, buildDummyClassLoader, + "(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/" + "String;)Ljava/lang/ClassLoader;"), + VECTOR_NATIVE_METHOD(ResourcesHook, rewriteXmlReferencesNative, + "(JLxposed/dummy/XResourcesSuperClass;Landroid/content/res/Resources;)V")}; + +void RegisterResourcesHook(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(ResourcesHook); } +} // namespace vector::native::jni From 4c8c7ec97c33cea48d9499afb9fbe79e00c0fb90 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 25 Jan 2026 18:58:10 +0100 Subject: [PATCH 02/24] Add kotlin definition of JNI functions Note that the signature of rewriteXmlReferencesNative is changed to separate sub-projects xposed and core. --- xposed/build.gradle.kts | 1 + .../vector/impl/utils/VectorDexParser.kt | 2 +- .../vector/nativebridge/DexParserBridge.kt | 3 +- .../matrix/vector/nativebridge/HookBridge.kt | 55 +++++++++++++++++++ .../matrix/vector/nativebridge/NativeAPI.kt | 6 ++ .../vector/nativebridge/ResourcesHook.kt | 24 ++++++++ 6 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index 9b2ea6edf..346c3fd25 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -20,4 +20,5 @@ android { dependencies { compileOnly(libs.androidx.annotation) + compileOnly(projects.hiddenapi.stubs) } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt index 9f232b494..ac42e64ad 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt @@ -4,7 +4,7 @@ import io.github.libxposed.api.utils.DexParser import io.github.libxposed.api.utils.DexParser.* import java.io.IOException import java.nio.ByteBuffer -import org.lsposed.lspd.nativebridge.DexParserBridge +import org.matrix.vector.nativebridge.DexParserBridge /** * Kotlin implementation of [DexParser] for Vector. diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt index 17bda9e76..35d46ea5b 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt @@ -1,5 +1,4 @@ -package org.lsposed.lspd.nativebridge -// TODO: refactor the JNI and thus change the package name +package org.matrix.vector.nativebridge import dalvik.annotation.optimization.FastNative import io.github.libxposed.api.utils.DexParser diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt new file mode 100644 index 000000000..632b532e6 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt @@ -0,0 +1,55 @@ +package org.matrix.vector.nativebridge + +import dalvik.annotation.optimization.FastNative +import java.lang.reflect.Executable +import java.lang.reflect.Method +import java.lang.reflect.InvocationTargetException + +object HookBridge { + @JvmStatic + external fun hookMethod( + useModernApi: Boolean, + hookMethod: Executable, + hooker: Class<*>, + priority: Int, + callback: Any? + ): Boolean + + @JvmStatic + external fun unhookMethod(useModernApi: Boolean, hookMethod: Executable, callback: Any?): Boolean + + @JvmStatic + external fun deoptimizeMethod(method: Executable): Boolean + + @JvmStatic + @Throws(InstantiationException::class) + external fun allocateObject(clazz: Class): T + + @JvmStatic + @Throws(IllegalAccessException::class, IllegalArgumentException::class, InvocationTargetException::class) + external fun invokeOriginalMethod(method: Executable, thisObject: Any?, vararg args: Any?): Any? + + @JvmStatic + @Throws(IllegalAccessException::class, IllegalArgumentException::class, InvocationTargetException::class) + external fun invokeSpecialMethod( + method: Executable, + shorty: CharArray, + clazz: Class, + thisObject: Any?, + vararg args: Any? + ): Any? + + @JvmStatic + @FastNative + external fun instanceOf(obj: Any?, clazz: Class<*>): Boolean + + @JvmStatic + @FastNative + external fun setTrusted(cookie: Any?): Boolean + + @JvmStatic + external fun callbackSnapshot(hooker_callback: Class<*>, method: Executable): Array> + + @JvmStatic + external fun getStaticInitializer(clazz: Class<*>): Method +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt new file mode 100644 index 000000000..114ae0ad9 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt @@ -0,0 +1,6 @@ +package org.matrix.vector.nativebridge + +object NativeAPI { + @JvmStatic + external fun recordNativeEntrypoint(library_name: String) +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt new file mode 100644 index 000000000..b41ee398d --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt @@ -0,0 +1,24 @@ +package org.matrix.vector.nativebridge + +import android.content.res.Resources +import dalvik.annotation.optimization.FastNative +import xposed.dummy.XResourcesSuperClass + +object ResourcesHook { + @JvmStatic + external fun initXResourcesNative(): Boolean + + @JvmStatic + external fun makeInheritable(clazz: Class<*>): Boolean + + @JvmStatic + external fun buildDummyClassLoader( + parent: ClassLoader, + resourceSuperClass: String, + typedArraySuperClass: String + ): ClassLoader + + @JvmStatic + @FastNative + external fun rewriteXmlReferencesNative(parserPtr: Long, origRes: XResourcesSuperClass, repRes: Resources) +} From a00c106ec9af75d4cf8dcbc8445965079b8b953b Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 25 Jan 2026 19:01:34 +0100 Subject: [PATCH 03/24] Update compilation chain of CMake --- build.gradle.kts | 28 +++++++++++-------- core/src/main/jni/CMakeLists.txt | 4 +-- daemon/src/main/jni/CMakeLists.txt | 2 +- dex2oat/src/main/cpp/CMakeLists.txt | 2 +- magisk-loader/src/main/jni/CMakeLists.txt | 2 +- .../src/main/jni/src/magisk_loader.cpp | 2 +- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 50028f667..e90c19ccf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,17 +32,30 @@ plugins { alias(libs.plugins.ktfmt) } +val repo = jgit.repo() +val commitCount = (repo?.commitCount("refs/remotes/origin/master") ?: 1) + 4200 +val latestTag = repo?.latestTag?.removePrefix("v") ?: "1.0" + +val injectedPackageName by extra("com.android.shell") +val injectedPackageUid by extra(2000) + +val defaultManagerPackageName by extra("org.lsposed.manager") +val verCode by extra(commitCount) +val verName by extra(latestTag) + + cmaker { default { arguments.addAll( arrayOf( - "-DEXTERNAL_ROOT=${File(rootDir.absolutePath, "external")}", - "-DCORE_ROOT=${File(rootDir.absolutePath, "core/src/main/jni")}", + "-DVECTOR_ROOT=${rootDir.absolutePath}", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", ) ) val flags = arrayOf( - "-DINJECTED_AID=$injectedPackageUid", + "-DINJECTED_UID=$injectedPackageUid", + "-DVERSION_CODE=${verCode}", + "-DVERSION_NAME='\"${verName}\"'", "-Wno-gnu-string-literal-operator-template", "-Wno-c++2b-extensions", ) @@ -59,16 +72,7 @@ cmaker { } } -val repo = jgit.repo() -val commitCount = (repo?.commitCount("refs/remotes/origin/master") ?: 1) + 4200 -val latestTag = repo?.latestTag?.removePrefix("v") ?: "1.0" - -val injectedPackageName by extra("com.android.shell") -val injectedPackageUid by extra(2000) -val defaultManagerPackageName by extra("org.lsposed.manager") -val verCode by extra(commitCount) -val verName by extra(latestTag) val androidTargetSdkVersion by extra(36) val androidMinSdkVersion by extra(27) val androidBuildToolsVersion by extra("36.0.0") diff --git a/core/src/main/jni/CMakeLists.txt b/core/src/main/jni/CMakeLists.txt index dce214922..8a51dc703 100644 --- a/core/src/main/jni/CMakeLists.txt +++ b/core/src/main/jni/CMakeLists.txt @@ -3,7 +3,7 @@ project(core) set(CMAKE_CXX_STANDARD 23) -add_subdirectory(${EXTERNAL_ROOT} external) +add_subdirectory(${VECTOR_ROOT}/external external) aux_source_directory(src SRC_LIST) aux_source_directory(src/jni SRC_LIST) @@ -19,7 +19,7 @@ set(IGNORED_WARNINGS -Wno-zero-length-array) target_include_directories(${PROJECT_NAME} PUBLIC include) -target_include_directories(${PROJECT_NAME} PRIVATE src ${EXTERNAL_ROOT}/xz-embedded/linux/include) +target_include_directories(${PROJECT_NAME} PRIVATE src ${VECTOR_ROOT}/external/xz-embedded/linux/include) target_compile_options(${PROJECT_NAME} PRIVATE -Wpedantic ${IGNORED_WARNINGS}) target_link_libraries(${PROJECT_NAME} PUBLIC dobby_static lsplant_static xz_static log fmt-header-only) diff --git a/daemon/src/main/jni/CMakeLists.txt b/daemon/src/main/jni/CMakeLists.txt index 2e0a89296..049bbcd80 100644 --- a/daemon/src/main/jni/CMakeLists.txt +++ b/daemon/src/main/jni/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.10) project(daemon) -add_subdirectory(${EXTERNAL_ROOT} external) +add_subdirectory(${VECTOR_ROOT}/external external) set(SOURCES dex2oat.cpp diff --git a/dex2oat/src/main/cpp/CMakeLists.txt b/dex2oat/src/main/cpp/CMakeLists.txt index c4f5ba333..ae4c3ae9c 100644 --- a/dex2oat/src/main/cpp/CMakeLists.txt +++ b/dex2oat/src/main/cpp/CMakeLists.txt @@ -5,7 +5,7 @@ add_executable(dex2oat dex2oat.cpp) add_library(oat_hook SHARED oat_hook.cpp) OPTION(LSPLT_BUILD_SHARED OFF) -add_subdirectory(${EXTERNAL_ROOT}/lsplt/lsplt/src/main/jni external) +add_subdirectory(${VECTOR_ROOT}/external/lsplt/lsplt/src/main/jni external) target_include_directories(oat_hook PUBLIC include) target_include_directories(dex2oat PUBLIC include) diff --git a/magisk-loader/src/main/jni/CMakeLists.txt b/magisk-loader/src/main/jni/CMakeLists.txt index 82600d7a6..43cad4be5 100644 --- a/magisk-loader/src/main/jni/CMakeLists.txt +++ b/magisk-loader/src/main/jni/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.10) project(lspd) -add_subdirectory(${CORE_ROOT} core) +add_subdirectory(${VECTOR_ROOT}/core/src/main/jni core) configure_file(template/loader.cpp src/loader.cpp) diff --git a/magisk-loader/src/main/jni/src/magisk_loader.cpp b/magisk-loader/src/main/jni/src/magisk_loader.cpp index 9c2d7e317..dd0a3fe71 100644 --- a/magisk-loader/src/main/jni/src/magisk_loader.cpp +++ b/magisk-loader/src/main/jni/src/magisk_loader.cpp @@ -47,7 +47,7 @@ constexpr int LAST_APP_ZYGOTE_ISOLATED_UID = 98999; constexpr int SHARED_RELRO_UID = 1037; constexpr int PER_USER_RANGE = 100000; -static constexpr uid_t kAidInjected = INJECTED_AID; +static constexpr uid_t kAidInjected = INJECTED_UID; static constexpr uid_t kAidInet = 3003; void MagiskLoader::LoadDex(JNIEnv *env, PreloadedDex &&dex) { From 249786abed3fb5b3dd840a56316c568aa7a69ecc Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 25 Jan 2026 19:03:33 +0100 Subject: [PATCH 04/24] Update to use Vector JNI packages --- core/src/main/java/android/content/res/XResources.java | 2 +- core/src/main/java/de/robv/android/xposed/XposedBridge.java | 4 ++-- core/src/main/java/de/robv/android/xposed/XposedInit.java | 4 ++-- .../java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java | 2 +- .../main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java | 2 +- core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java | 2 +- core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java | 4 ++-- daemon/src/main/jni/obfuscation.cpp | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/android/content/res/XResources.java b/core/src/main/java/android/content/res/XResources.java index 2ccdbd376..349a885f5 100644 --- a/core/src/main/java/android/content/res/XResources.java +++ b/core/src/main/java/android/content/res/XResources.java @@ -20,7 +20,7 @@ package android.content.res; -import static org.lsposed.lspd.nativebridge.ResourcesHook.rewriteXmlReferencesNative; +import static org.matrix.vector.nativebridge.ResourcesHook.rewriteXmlReferencesNative; import static de.robv.android.xposed.XposedHelpers.decrementMethodDepth; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; import static de.robv.android.xposed.XposedHelpers.getBooleanField; diff --git a/core/src/main/java/de/robv/android/xposed/XposedBridge.java b/core/src/main/java/de/robv/android/xposed/XposedBridge.java index b7730431e..95f8b9ef6 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedBridge.java +++ b/core/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -27,8 +27,8 @@ import org.lsposed.lspd.impl.LSPosedBridge; import org.lsposed.lspd.impl.LSPosedHookCallback; -import org.lsposed.lspd.nativebridge.HookBridge; -import org.lsposed.lspd.nativebridge.ResourcesHook; +import org.matrix.vector.nativebridge.HookBridge; +import org.matrix.vector.nativebridge.ResourcesHook; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Executable; diff --git a/core/src/main/java/de/robv/android/xposed/XposedInit.java b/core/src/main/java/de/robv/android/xposed/XposedInit.java index 97436936c..39bbe4be5 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedInit.java +++ b/core/src/main/java/de/robv/android/xposed/XposedInit.java @@ -42,8 +42,8 @@ import org.lsposed.lspd.impl.LSPosedContext; import org.lsposed.lspd.models.PreLoadedApk; -import org.lsposed.lspd.nativebridge.NativeAPI; -import org.lsposed.lspd.nativebridge.ResourcesHook; +import org.matrix.vector.nativebridge.NativeAPI; +import org.matrix.vector.nativebridge.ResourcesHook; import org.lsposed.lspd.util.LspModuleClassLoader; import org.lsposed.lspd.util.Utils.Log; diff --git a/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java b/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java index 8c14877f7..ad0b104cc 100644 --- a/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java +++ b/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java @@ -24,7 +24,7 @@ import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_BOOT_IMAGE_MIUI_RES; import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_SYSTEM_SERVER; -import org.lsposed.lspd.nativebridge.HookBridge; +import org.matrix.vector.nativebridge.HookBridge; import org.lsposed.lspd.util.Hookers; import org.lsposed.lspd.util.Utils; diff --git a/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java index acbef26d0..f50e14768 100644 --- a/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java +++ b/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java @@ -3,7 +3,7 @@ import android.os.Build; import org.lsposed.lspd.impl.LSPosedBridge; -import org.lsposed.lspd.nativebridge.HookBridge; +import org.matrix.vector.nativebridge.HookBridge; import io.github.libxposed.api.XposedInterface; diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java index 8dd201dd6..53e033af7 100644 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java +++ b/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java @@ -2,7 +2,7 @@ import androidx.annotation.NonNull; -import org.lsposed.lspd.nativebridge.HookBridge; +import org.matrix.vector.nativebridge.HookBridge; import org.lsposed.lspd.util.Utils.Log; import java.lang.reflect.Executable; diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java index a4f64d142..25a3add6a 100644 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java +++ b/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java @@ -14,8 +14,8 @@ import org.lsposed.lspd.core.BuildConfig; import org.lsposed.lspd.models.Module; -import org.lsposed.lspd.nativebridge.HookBridge; -import org.lsposed.lspd.nativebridge.NativeAPI; +import org.matrix.vector.nativebridge.HookBridge; +import org.matrix.vector.nativebridge.NativeAPI; import org.lsposed.lspd.service.ILSPInjectedModuleService; import org.lsposed.lspd.util.LspModuleClassLoader; import org.lsposed.lspd.util.Utils.Log; diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 133ca0202..74a69dfa6 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -45,7 +45,7 @@ std::map signatures = { { "Landroid/content/res/XRes", ""}, { "Landroid/content/res/XModule", ""}, { "Lorg/lsposed/lspd/core/", ""}, - { "Lorg/lsposed/lspd/nativebridge/", ""}, + { "Lorg/matrix/vector/nativebridge/", ""}, { "Lorg/lsposed/lspd/service/", ""}, }; From 0a219d0b87edc92da0f19c7b8097b091129625c6 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 15 Feb 2026 02:54:03 +0100 Subject: [PATCH 05/24] Update build script --- build.gradle.kts | 71 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e90c19ccf..96f84bb29 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,26 +1,12 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - import com.android.build.api.dsl.ApplicationDefaultConfig import com.android.build.api.dsl.CommonExtension import com.android.build.gradle.api.AndroidBasePlugin import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask +import java.io.ByteArrayOutputStream +import javax.inject.Inject +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters +import org.gradle.process.ExecOperations plugins { alias(libs.plugins.lsplugin.cmaker) @@ -32,6 +18,53 @@ plugins { alias(libs.plugins.ktfmt) } +/** A ValueSource that executes 'git rev-list --count' to get the total commit count. */ +abstract class GitCommitCountValueSource : ValueSource { + @get:Inject abstract val execOperations: ExecOperations + + override fun obtain(): String { + val output = ByteArrayOutputStream() + val result = + execOperations.exec { + commandLine("git", "rev-list", "--count", "refs/remotes/origin/master") + standardOutput = output + isIgnoreExitValue = true + } + // Return the count if successful, otherwise a default of "1". + return if (result.exitValue == 0 && output.toString().isNotBlank()) { + output.toString().trim() + } else { + "1" + } + } +} + +/** A ValueSource that executes 'git tag' to get the latest version tag. */ +abstract class GitLatestTagValueSource : ValueSource { + @get:Inject abstract val execOperations: ExecOperations + + override fun obtain(): String { + val output = ByteArrayOutputStream() + val result = + execOperations.exec { + commandLine("git", "tag", "--list", "--sort=-v:refname") + standardOutput = output + isIgnoreExitValue = true + } + // If successful, parse the first line. Provide a default if no tags are found. + return if (result.exitValue == 0 && output.toString().isNotBlank()) { + output.toString().lineSequence().first().removePrefix("v") + } else { + "1.0" + } + } +} + +// This defers the execution of the git commands and allows Gradle to cache the results. +val versionCodeProvider by extra(providers.of(GitCommitCountValueSource::class.java) {}) +val versionNameProvider by extra(providers.of(GitLatestTagValueSource::class.java) {}) + + val repo = jgit.repo() val commitCount = (repo?.commitCount("refs/remotes/origin/master") ?: 1) + 4200 val latestTag = repo?.latestTag?.removePrefix("v") ?: "1.0" From 8c154d32a8b190d7a9575edf590e782e6c440a51 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 15 Feb 2026 11:01:14 +0100 Subject: [PATCH 06/24] Rewrite Zygisk module --- .../lspd/service/ConfigFileManager.java | 2 +- .../lsposed/lspd/service/Dex2OatService.java | 4 +- .../lspd/service/LSPApplicationService.java | 2 + daemon/src/main/jni/obfuscation.cpp | 4 +- settings.gradle.kts | 1 + zygisk/.gitignore | 3 + zygisk/README.md | 75 +++ zygisk/build.gradle.kts | 192 ++++++ .../META-INF/com/google/android/update-binary | 29 + .../com/google/android/updater-script | 1 + zygisk/module/action.sh | 4 + zygisk/module/customize.sh | 153 +++++ zygisk/module/daemon | 40 ++ zygisk/module/module.prop | 7 + zygisk/module/sepolicy.rule | 14 + zygisk/module/service.sh | 25 + zygisk/module/system.prop | 1 + zygisk/module/uninstall.sh | 1 + zygisk/module/util_functions.sh | 63 ++ zygisk/module/verify.sh | 71 +++ zygisk/proguard-rules.pro | 18 + zygisk/src/main/AndroidManifest.xml | 1 + zygisk/src/main/cpp/CMakeLists.txt | 24 + zygisk/src/main/cpp/include/ipc_bridge.h | 182 ++++++ zygisk/src/main/cpp/include/zygisk.hpp | 392 ++++++++++++ zygisk/src/main/cpp/ipc_bridge.cpp | 587 ++++++++++++++++++ zygisk/src/main/cpp/module.cpp | 457 ++++++++++++++ .../matrix/vector/ParasiticManagerHooker.kt | 435 +++++++++++++ .../vector/ParasiticManagerSystemHooker.kt | 118 ++++ .../kotlin/org/matrix/vector/core/Main.kt | 55 ++ .../matrix/vector/service/BridgeService.kt | 189 ++++++ .../org/matrix/vector/service/ParcelUtils.kt | 39 ++ 32 files changed, 3184 insertions(+), 5 deletions(-) create mode 100644 zygisk/.gitignore create mode 100644 zygisk/README.md create mode 100644 zygisk/build.gradle.kts create mode 100644 zygisk/module/META-INF/com/google/android/update-binary create mode 100644 zygisk/module/META-INF/com/google/android/updater-script create mode 100644 zygisk/module/action.sh create mode 100644 zygisk/module/customize.sh create mode 100644 zygisk/module/daemon create mode 100644 zygisk/module/module.prop create mode 100644 zygisk/module/sepolicy.rule create mode 100644 zygisk/module/service.sh create mode 100644 zygisk/module/system.prop create mode 100644 zygisk/module/uninstall.sh create mode 100644 zygisk/module/util_functions.sh create mode 100644 zygisk/module/verify.sh create mode 100644 zygisk/proguard-rules.pro create mode 100644 zygisk/src/main/AndroidManifest.xml create mode 100644 zygisk/src/main/cpp/CMakeLists.txt create mode 100644 zygisk/src/main/cpp/include/ipc_bridge.h create mode 100644 zygisk/src/main/cpp/include/zygisk.hpp create mode 100644 zygisk/src/main/cpp/ipc_bridge.cpp create mode 100644 zygisk/src/main/cpp/module.cpp create mode 100644 zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt create mode 100644 zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt create mode 100644 zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt create mode 100644 zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt create mode 100644 zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java index 1f4d08e09..4caa1aaac 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java @@ -79,7 +79,7 @@ import hidden.HiddenApiBridge; public class ConfigFileManager { - static final Path basePath = Paths.get("/data/adb/lspd"); + static final Path basePath = Paths.get("/data/adb/vector"); static final Path modulePath = basePath.resolve("modules"); static final Path daemonApkPath = Paths.get(System.getProperty("java.class.path", null)); static final Path managerApkPath = daemonApkPath.getParent().resolve("manager.apk"); diff --git a/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java b/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java index bd57a4162..f4da23ebb 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java @@ -138,8 +138,8 @@ public Dex2OatService() { checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd64"); } - openDex2oat(4, "/data/adb/modules/zygisk_lsposed/bin/liboat_hook32.so"); - openDex2oat(5, "/data/adb/modules/zygisk_lsposed/bin/liboat_hook64.so"); + openDex2oat(4, "/data/adb/modules/zygisk_vector/bin/liboat_hook32.so"); + openDex2oat(5, "/data/adb/modules/zygisk_vector/bin/liboat_hook64.so"); var enforce = Paths.get("/sys/fs/selinux/enforce"); var policy = Paths.get("/sys/fs/selinux/policy"); diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java index 259980267..0ff6c301c 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java @@ -87,6 +87,7 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws case DEX_TRANSACTION_CODE: { var shm = ConfigManager.getInstance().getPreloadDex(); if (shm == null) return false; + reply.writeNoException(); // assume that write only a fd shm.writeToParcel(reply, 0); reply.writeLong(shm.getSize()); @@ -95,6 +96,7 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws case OBFUSCATION_MAP_TRANSACTION_CODE: { var obfuscation = ConfigManager.getInstance().dexObfuscate(); var signatures = ObfuscationManager.getSignatures(); + reply.writeNoException(); reply.writeInt(signatures.size() * 2); for (Map.Entry entry : signatures.entrySet()) { reply.writeString(entry.getKey()); diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 74a69dfa6..e7adca26e 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -44,9 +44,9 @@ std::map signatures = { { "Landroid/app/AndroidApp", ""}, { "Landroid/content/res/XRes", ""}, { "Landroid/content/res/XModule", ""}, - { "Lorg/lsposed/lspd/core/", ""}, + { "Lorg/matrix/vector/core/", ""}, { "Lorg/matrix/vector/nativebridge/", ""}, - { "Lorg/lsposed/lspd/service/", ""}, + { "Lorg/matrix/vector/service/", ""}, }; jclass class_file_descriptor; diff --git a/settings.gradle.kts b/settings.gradle.kts index fcb822daf..9d3f89ac8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,4 +30,5 @@ include( ":services:manager-service", ":services:daemon-service", ":xposed", + ":zygisk", ) diff --git a/zygisk/.gitignore b/zygisk/.gitignore new file mode 100644 index 000000000..b5e4b9f39 --- /dev/null +++ b/zygisk/.gitignore @@ -0,0 +1,3 @@ +/build +/release +/.cxx diff --git a/zygisk/README.md b/zygisk/README.md new file mode 100644 index 000000000..61639cd22 --- /dev/null +++ b/zygisk/README.md @@ -0,0 +1,75 @@ +# Vector Zygisk Module & Framework Loader + +## Overview + +This sub-project constitutes the injection engine of the Vector framework. It acts as the bridge between the Android Zygote process and the high-level Xposed API. + +The project is a hybrid system consisting of two distinct layers: +1. **Native Layer (C++)**: A Zygisk module that hooks process creation, filters targets, and bootstraps the environment. +2. **Loader Layer (Kotlin)**: The initial Java-world payload that initializes the Xposed bridge, establishes high-level IPC, and manages the "Parasitic" execution environment for the Manager. + +Its primary responsibility is to inject the Vector framework into the target process's memory at the earliest possible stage of its lifecycle, ensuring a robust and stealthy environment. + +--- + +## Part 1: The Native Zygisk Layer + +The native layer (`libzygisk.so`) is the entry point. It hooks into the Zygote process creation lifecycle via the Zygisk API (e.g., `preAppSpecialize`, `postAppSpecialize`). It is architected to have minimal internal logic, delegating heavy lifting (like ART hooking and ELF parsing) to the core [native](../native) library. + +### Core Responsibilities +* **Target Filtering**: Implements logic to skip isolated processes, application zygotes, and non-target system components to minimize footprint. +* **IPC Communication**: Establishes a secure Binder IPC connection with the daemon manager service via a "Rendezvous" system service to fetch the framework DEX and configuration data (e.g., obfuscation maps). +* **DEX Loading**: Uses `InMemoryDexClassLoader` to load the framework's bytecode directly from memory, avoiding disk I/O signatures. +* **JNI Interception**: Installs a low-level JNI hook on `CallBooleanMethodV`. This intercepts `Binder.execTransact` calls, allowing the framework to patch into the system's IPC flow without registering standard Android Services. + +### Key Components (C++) +* **`VectorModule` (`module.cpp`)**: The central orchestrator implementing `zygisk::ModuleBase`. It manages the injection state machine and inherits from `vector::native::Context` to gain core injection capabilities. +* **`IPCBridge` (`ipc_bridge.cpp`)**: A singleton handling raw Binder transactions. It manages the two-step connection protocol (Rendezvous -> Dedicated Binder) and contains the JNI table override logic. + +--- + +## Part 2: The Kotlin Framework Loader + +Once the native layer successfully loads the DEX, control is handed off to the Kotlin layer via JNI. This layer handles high-level Android framework manipulation, Xposed initialization, and identity spoofing. + +### Core Responsibilities +* **Bootstrapping**: `Main.forkCommon` acts as the Java entry point. It differentiates between the `system_server` and standard applications. +* **Parasitic Injection**: Implements the logic to run the full LSPosed Manager application inside a host process (currently `com.android.shell`). This allows the Manager to run with elevated privileges without being installed as a system app. +* **Manual Bridge Service**: Provides the Java-side handling for the intercepted Binder transactions. + +### Key Components (Kotlin) +* **`Main`**: The singleton entry point. It initializes the Xposed bridge (`Startup`) and decides whether to load the standard Xposed environment or the Parasitic Manager. +* **`BridgeService`**: The peer to the C++ `IPCBridge`. It decodes custom `_LSP` transactions, manages the distribution of the system service binder, and handles communication between the injected framework and the root daemon. +* **`ParasiticManagerHooker`**: The complex logic for identity transplantation. + * **App Swap**: Swaps the host's `ApplicationInfo` with the Manager's info during `handleBindApplication`. + * **State Persistence**: Since the Android System is unaware the host process is running Manager activities, this component manually captures and restores `Bundle` states to prevent data loss during lifecycle events. + * **Resource Spoofing**: Hooks `WebView` and `ContentProvider` installation to satisfy package name validations. + +--- + +## Injection & Execution Flow + +The full lifecycle of a Vector-instrumented process follows this sequence: + +1. **Zygote Fork**: Zygisk triggers the `preAppSpecialize` callback in C++. +2. **Native Decision**: `VectorModule` checks the UID/Process Name. If valid, it initializes the `IPCBridge`. +3. **DEX Fetch**: The C++ layer connects to the root daemon, fetches the Framework DEX file descriptor and the Obfuscation Map. +4. **Memory Loading**: `postAppSpecialize` triggers the creation of an `InMemoryDexClassLoader`. +5. **JNI Hand-off**: The native module calls the static Kotlin method `org.lsposed.lspd.core.Main.forkCommon`. +6. **Identity Check (Kotlin)**: + * **If Manager Package**: `ParasiticManagerHooker.start()` is called. The process is "hijacked" to run the Manager UI. + * **If Standard App**: `Startup.bootstrapXposed()` is called. Third-party modules are loaded. +7. **Live Interception**: Throughout the process life, the C++ JNI hook redirects specific `Binder.execTransact` calls to `BridgeService.execTransact` in Kotlin. + +--- + +## Maintenance & Technical Notes + +### The IPC Protocol +The communication between the native loader and the Kotlin framework relies on specific conventions: +* **Transaction Code**: The custom code `_LSP` (bitwise constructed) must remain synchronized between `ipc_bridge.cpp` (Native) and `BridgeService.kt` (Kotlin). +* **The "Out-Parameter" List**: In `ParasiticManagerHooker.start()`, you will see an empty list `mutableListOf()`. +It is used as an "out-parameter" for the Binder call, allowing the root daemon to push the Manager Service Binder back to the loader. + +### System Server Hooks +The `ParasiticManagerSystemHooker` runs *only* in the `system_server`. It uses `XposedHooker` to intercept `ActivityTaskSupervisor.resolveActivity`. It detects Intents tagged with `LAUNCH_MANAGER` and forcefully redirects them to the parasitic process (e.g., `Shell`), modifying the `ActivityInfo` on the fly to ensure the Manager launches correctly. diff --git a/zygisk/build.gradle.kts b/zygisk/build.gradle.kts new file mode 100644 index 000000000..7c44f43aa --- /dev/null +++ b/zygisk/build.gradle.kts @@ -0,0 +1,192 @@ +import java.security.MessageDigest +import org.apache.commons.codec.binary.Hex +import org.apache.tools.ant.filters.ReplaceTokens + +plugins { + alias(libs.plugins.agp.app) + alias(libs.plugins.kotlin) + alias(libs.plugins.ktfmt) +} + +ktfmt { kotlinLangStyle() } + +val versionCodeProvider: Provider by rootProject.extra +val versionNameProvider: Provider by rootProject.extra + +android { + namespace = "org.matrix.vector" + + defaultConfig { multiDexEnabled = false } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles("proguard-rules.pro") + } + } + + externalNativeBuild { cmake { path("src/main/cpp/CMakeLists.txt") } } +} + +abstract class Injected @Inject constructor(val moduleDir: String) { + @get:Inject abstract val factory: ObjectFactory +} + +dependencies { + implementation(projects.core) + implementation(projects.hiddenapi.bridge) + implementation(projects.services.managerService) + implementation(projects.services.daemonService) + compileOnly(libs.androidx.annotation) + compileOnly(projects.hiddenapi.stubs) +} + +val zipAll = tasks.register("zipAll") { group = "Vector" } + +androidComponents { + onVariants(selector().all()) { variant -> + val variantCapped = variant.name.replaceFirstChar { it.uppercase() } + val variantLowered = variant.name.lowercase() + + // --- Define output locations and file names --- + // Stage all files in a temporary directory inside 'build' before zipping + val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}") + val zipFileName = + "Vector-v${versionNameProvider.get()}-${versionCodeProvider.get()}-$variantCapped.zip" + + // Using Sync ensures that stale files from previous runs are removed. + val prepareModuleFilesTask = + tasks.register("prepareModuleFiles$variantCapped") { + group = "Vector Module Packaging" + dependsOn( + "assemble$variantCapped", + ":app:package$variantCapped", + ":daemon:package$variantCapped", + ":dex2oat:externalNativeBuild$variantCapped", + ) + into(tempModuleDir) + from("${rootProject.projectDir}/README.md") + from("$projectDir/module") { exclude("module.prop", "customize.sh", "daemon") } + from("$projectDir/module") { + include("module.prop") + expand( + "versionName" to "v${versionNameProvider.get()}", + "versionCode" to versionCodeProvider.get(), + ) + } + from("$projectDir/module") { + include("customize.sh", "daemon") + val tokens = + mapOf("DEBUG" to if (variantLowered == "debug") "true" else "false") + filter("tokens" to tokens) + } + from(project(":app").tasks.getByName("package$variantCapped").outputs) { + include("*.apk") + rename(".*\\.apk", "manager.apk") + } + from(project(":daemon").tasks.getByName("package$variantCapped").outputs) { + include("*.apk") + rename(".*\\.apk", "daemon.apk") + } + into("lib") { + val libDir = variantLowered + "/strip${variantCapped}DebugSymbols" + from( + layout.buildDirectory.dir( + "intermediates/stripped_native_libs/$libDir/out/lib" + ) + ) { + include("**/libzygisk.so") + } + } + into("bin") { + from( + project(":dex2oat") + .layout + .buildDirectory + .dir("intermediates/cmake/$variantLowered/obj") + ) { + include("**/dex2oat") + include("**/liboat_hook.so") + } + } + val dexOutPath = + if (variantLowered == "release") + layout.buildDirectory.dir( + "intermediates/dex/$variantLowered/minify${variantCapped}WithR8" + ) + else + layout.buildDirectory.dir( + "intermediates/dex/$variantLowered/mergeDex$variantCapped" + ) + into("framework") { + from(dexOutPath) + rename("classes.dex", "lspd.dex") + } + val injected = objects.newInstance(tempModuleDir.get().asFile.path) + doLast { + injected.factory.fileTree().from(injected.moduleDir).visit { + if (isDirectory) return@visit + val md = MessageDigest.getInstance("SHA-256") + file.forEachBlock(4096) { bytes, size -> md.update(bytes, 0, size) } + File(file.path + ".sha256").writeText(Hex.encodeHexString(md.digest())) + } + } + } + + val zipTask = + tasks.register("zip${variantCapped}") { + group = "Vector Module Packaging" + dependsOn(prepareModuleFilesTask) + archiveFileName = zipFileName + destinationDirectory = file("$projectDir/release") + from(tempModuleDir) + } + + zipAll.configure { dependsOn(zipTask) } + + // A helper function to create installation tasks for different root providers. + fun createInstallTasks(rootProvider: String, installCli: String) { + val pushTask = + tasks.register("push${rootProvider}Module${variantCapped}") { + group = "Zygisk Module Installation" + description = + "Pushes the ${variant.name} build to the device for $rootProvider." + dependsOn(zipTask) + commandLine( + "adb", + "push", + zipTask.get().archiveFile.get().asFile, + "/data/local/tmp", + ) + } + + val installTask = + tasks.register("install${rootProvider}${variantCapped}") { + group = "Zygisk Module Installation" + description = "Installs the ${variant.name} build via $rootProvider." + dependsOn(pushTask) + commandLine( + "adb", + "shell", + "su", + "-c", + "$installCli /data/local/tmp/$zipFileName", + ) + } + tasks.register("install${rootProvider}AndReboot${variantCapped}") { + group = "Zygisk Module Installation" + description = "Installs the ${variant.name} build via $rootProvider and reboots." + dependsOn(installTask) + commandLine("adb", "reboot") + } + } + + createInstallTasks("Magisk", "magisk --install-module") + createInstallTasks("Ksu", "ksud module install") + createInstallTasks("Apatch", "/data/adb/apd module install") + } +} + +evaluationDependsOn(":app") + +evaluationDependsOn(":daemon") diff --git a/zygisk/module/META-INF/com/google/android/update-binary b/zygisk/module/META-INF/com/google/android/update-binary new file mode 100644 index 000000000..26911f4be --- /dev/null +++ b/zygisk/module/META-INF/com/google/android/update-binary @@ -0,0 +1,29 @@ +################# +# Initialization +################# + +umask 022 + +# echo before loading util_functions +ui_print() { echo "$1"; } + +require_new_magisk() { + ui_print "*******************************" + ui_print " Please install Magisk v20.4+! " + ui_print "*******************************" + exit 1 +} + +######################### +# Load util_functions.sh +######################### + +OUTFD=$2 +ZIPFILE=$3 + +[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk +. /data/adb/magisk/util_functions.sh +[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk + +install_module +exit 0 diff --git a/zygisk/module/META-INF/com/google/android/updater-script b/zygisk/module/META-INF/com/google/android/updater-script new file mode 100644 index 000000000..11d5c96e0 --- /dev/null +++ b/zygisk/module/META-INF/com/google/android/updater-script @@ -0,0 +1 @@ +#MAGISK diff --git a/zygisk/module/action.sh b/zygisk/module/action.sh new file mode 100644 index 000000000..05a2b4267 --- /dev/null +++ b/zygisk/module/action.sh @@ -0,0 +1,4 @@ +DEFAULT_MANAGER_PACKAGE_NAME="org.lsposed.manager" +INJECTED_PACKAGE_NAME="com.android.shell" + +am start -c "${DEFAULT_MANAGER_PACKAGE_NAME}.LAUNCH_MANAGER" "${INJECTED_PACKAGE_NAME}/.BugreportWarningActivity" diff --git a/zygisk/module/customize.sh b/zygisk/module/customize.sh new file mode 100644 index 000000000..935ecf6c9 --- /dev/null +++ b/zygisk/module/customize.sh @@ -0,0 +1,153 @@ +# +# This file is part of LSPosed. +# +# LSPosed is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# LSPosed is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LSPosed. If not, see . +# +# Copyright (C) 2020 EdXposed Contributors +# Copyright (C) 2021 LSPosed Contributors +# + +# shellcheck disable=SC2034 +SKIPUNZIP=1 + +enforce_install_from_magisk_app() { + if $BOOTMODE; then + ui_print "- Installing from Magisk app" + else + ui_print "*********************************************************" + ui_print "! Install from recovery is NOT supported" + ui_print "! Some recovery has broken implementations, please thus install from Magisk app" + abort "*********************************************************" + fi +} + +VERSION=$(grep_prop version "${TMPDIR}/module.prop") +ui_print "- LSPosed version ${VERSION}" + +# Extract verify.sh +ui_print "- Extracting verify.sh" +unzip -o "$ZIPFILE" 'verify.sh' -d "$TMPDIR" >&2 +if [ ! -f "$TMPDIR/verify.sh" ]; then + ui_print "*********************************************************" + ui_print "! Unable to extract verify.sh!" + ui_print "! This zip may be corrupted, please try downloading again" + abort "*********************************************************" +fi +. "$TMPDIR/verify.sh" + +# Base check +extract "$ZIPFILE" 'customize.sh' "$TMPDIR" +extract "$ZIPFILE" 'verify.sh' "$TMPDIR" +extract "$ZIPFILE" 'util_functions.sh' "$TMPDIR" +. "$TMPDIR/util_functions.sh" +check_android_version +if [ -z "$KSU" ] && [ -z "$APATCH" ]; then + check_magisk_version +fi +check_incompatible_module + +enforce_install_from_magisk_app + +# Check architecture +if [ "$ARCH" != "arm" ] && [ "$ARCH" != "arm64" ] && [ "$ARCH" != "x86" ] && [ "$ARCH" != "x64" ]; then + abort "! Unsupported platform: $ARCH" +else + ui_print "- Device platform: $ARCH" +fi + +# Extract libs +ui_print "- Extracting module files" + +extract "$ZIPFILE" 'module.prop' "$MODPATH" +extract "$ZIPFILE" 'action.sh' "$MODPATH" +extract "$ZIPFILE" 'service.sh' "$MODPATH" +extract "$ZIPFILE" 'uninstall.sh' "$MODPATH" +extract "$ZIPFILE" 'sepolicy.rule' "$MODPATH" +extract "$ZIPFILE" 'framework/lspd.dex' "$MODPATH" +extract "$ZIPFILE" 'daemon.apk' "$MODPATH" +extract "$ZIPFILE" 'daemon' "$MODPATH" +rm -f /data/adb/vector/manager.apk +extract "$ZIPFILE" 'manager.apk' "$MODPATH" + + mkdir -p "$MODPATH/zygisk" + +if [ "$ARCH" = "arm" ] || [ "$ARCH" = "arm64" ]; then + extract "$ZIPFILE" "lib/armeabi-v7a/libzygisk.so" "$MODPATH/zygisk" true + mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/armeabi-v7a.so" + + if [ "$IS64BIT" = true ]; then + extract "$ZIPFILE" "lib/arm64-v8a/libzygisk.so" "$MODPATH/zygisk" true + mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/arm64-v8a.so" + fi +fi + +if [ "$ARCH" = "x86" ] || [ "$ARCH" = "x64" ]; then + extract "$ZIPFILE" "lib/x86/libzygisk.so" "$MODPATH/zygisk" true + mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/x86.so" + + if [ "$IS64BIT" = true ]; then + extract "$ZIPFILE" "lib/x86_64/libzygisk.so" "$MODPATH/zygisk" true + mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/x86_64.so" + fi +fi + +if [ "$API" -ge 29 ]; then + ui_print "- Extracting dex2oat binaries" + mkdir "$MODPATH/bin" + + if [ "$ARCH" = "arm" ] || [ "$ARCH" = "arm64" ]; then + extract "$ZIPFILE" "bin/armeabi-v7a/dex2oat" "$MODPATH/bin" true + extract "$ZIPFILE" "bin/armeabi-v7a/liboat_hook.so" "$MODPATH/bin" true + mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat32" + mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook32.so" + + if [ "$IS64BIT" = true ]; then + extract "$ZIPFILE" "bin/arm64-v8a/dex2oat" "$MODPATH/bin" true + extract "$ZIPFILE" "bin/arm64-v8a/liboat_hook.so" "$MODPATH/bin" true + mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat64" + mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook64.so" + fi + elif [ "$ARCH" == "x86" ] || [ "$ARCH" == "x64" ]; then + extract "$ZIPFILE" "bin/x86/dex2oat" "$MODPATH/bin" true + extract "$ZIPFILE" "bin/x86/liboat_hook.so" "$MODPATH/bin" true + mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat32" + mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook32.so" + + if [ "$IS64BIT" = true ]; then + extract "$ZIPFILE" "bin/x86_64/dex2oat" "$MODPATH/bin" true + extract "$ZIPFILE" "bin/x86_64/liboat_hook.so" "$MODPATH/bin" true + mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat64" + mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook64.so" + fi + fi + + ui_print "- Patching binaries" + DEV_PATH=$(tr -dc 'a-z0-9' < /dev/urandom | head -c 32) + sed -i "s/5291374ceda0aef7c5d86cd2a4f6a3ac/$DEV_PATH/g" "$MODPATH/daemon.apk" + sed -i "s/5291374ceda0aef7c5d86cd2a4f6a3ac/$DEV_PATH/" "$MODPATH/bin/dex2oat32" + sed -i "s/5291374ceda0aef7c5d86cd2a4f6a3ac/$DEV_PATH/" "$MODPATH/bin/dex2oat64" +else + extract "$ZIPFILE" 'system.prop' "$MODPATH" +fi + +set_perm_recursive "$MODPATH" 0 0 0755 0644 +set_perm_recursive "$MODPATH/bin" 0 2000 0755 0755 u:object_r:xposed_file:s0 +chmod 0744 "$MODPATH/daemon" + +if [ "$(grep_prop ro.maple.enable)" == "1" ]; then + ui_print "- Add ro.maple.enable=0" + echo "ro.maple.enable=0" >> "$MODPATH/system.prop" +fi + +ui_print "- Welcome to LSPosed!" diff --git a/zygisk/module/daemon b/zygisk/module/daemon new file mode 100644 index 000000000..ec39cffde --- /dev/null +++ b/zygisk/module/daemon @@ -0,0 +1,40 @@ +#!/system/bin/sh + +dir=${0%/*} +tmpLspdApk="/data/local/tmp/daemon.apk" +debug=@DEBUG@ + +if [ -r $tmpLspdApk ]; then + java_options="-Djava.class.path=$tmpLspdApk" + debug="true" +else + java_options="-Djava.class.path=$dir/daemon.apk" +fi + +if [ $debug = "true" ]; then + os_version=$(getprop ro.build.version.sdk) + if [ "$os_version" -eq "27" ]; then + java_options="$java_options -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable" + elif [ "$os_version" -eq "28" ]; then + java_options="$java_options -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable" + else + java_options="$java_options -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y" + fi +fi + +mount tmpfs -t tmpfs /data/resource-cache + +if [ ! -S "/dev/socket/zygote" ]; then + timeout 0.5 inotifyd - /dev/socket:near | while read -r line; do + $debug && log -p v -t "LSPosed" "inotify: $line" + if [ -S "/dev/socket/zygote" ]; then + $debug && log -p v -t "LSPosed" "zygote started" + touch /dev/socket& + exit + fi + done +fi +$debug && log -p d -t "LSPosed" "start daemon $*" + +# shellcheck disable=SC2086 +exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.lsposed.lspd.Main "$@" >/dev/null 2>&1 diff --git a/zygisk/module/module.prop b/zygisk/module/module.prop new file mode 100644 index 000000000..46ed43a6f --- /dev/null +++ b/zygisk/module/module.prop @@ -0,0 +1,7 @@ +id=zygisk_vector +name=Vector +version=${versionName} (${versionCode}) +versionCode=${versionCode} +author=JingMatrix +description=A modern, Xposed-compatible framework for Android application hooking. (Android 8.1 ~ 16) +updateJson=https://raw.githubusercontent.com/JingMatrix/Vector/master/zygisk/update.json diff --git a/zygisk/module/sepolicy.rule b/zygisk/module/sepolicy.rule new file mode 100644 index 000000000..d0b254e05 --- /dev/null +++ b/zygisk/module/sepolicy.rule @@ -0,0 +1,14 @@ +allow dex2oat dex2oat_exec file execute_no_trans +allow dex2oat system_linker_exec file execute_no_trans + +allow shell shell dir write + +type xposed_file file_type +typeattribute xposed_file mlstrustedobject +allow {dex2oat installd isolated_app shell} xposed_file {file dir} * + +allow dex2oat unlabeled file * + +type xposed_data file_type +typeattribute xposed_data mlstrustedobject +allow * xposed_data {file dir} * diff --git a/zygisk/module/service.sh b/zygisk/module/service.sh new file mode 100644 index 000000000..87e6e9ed6 --- /dev/null +++ b/zygisk/module/service.sh @@ -0,0 +1,25 @@ +# +# This file is part of LSPosed. +# +# LSPosed is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# LSPosed is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LSPosed. If not, see . +# +# Copyright (C) 2021 LSPosed Contributors +# + +MODDIR=${0%/*} + +cd "$MODDIR" + +# To avoid delaying the normal mount timing of zygote, we start LSPosed service daemon in late_start service mode instead of post-fs-data mode +unshare --propagation slave -m sh -c "$MODDIR/daemon --system-server-max-retry=3 $@&" diff --git a/zygisk/module/system.prop b/zygisk/module/system.prop new file mode 100644 index 000000000..64184152a --- /dev/null +++ b/zygisk/module/system.prop @@ -0,0 +1 @@ +dalvik.vm.dex2oat-flags=--inline-max-code-units=0 diff --git a/zygisk/module/uninstall.sh b/zygisk/module/uninstall.sh new file mode 100644 index 000000000..2b4ad0d6c --- /dev/null +++ b/zygisk/module/uninstall.sh @@ -0,0 +1 @@ +rm -rf /data/adb/vector diff --git a/zygisk/module/util_functions.sh b/zygisk/module/util_functions.sh new file mode 100644 index 000000000..4bb9ea87f --- /dev/null +++ b/zygisk/module/util_functions.sh @@ -0,0 +1,63 @@ +# +# This file is part of LSPosed. +# +# LSPosed is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# LSPosed is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LSPosed. If not, see . +# +# Copyright (C) 2020 EdXposed Contributors +# Copyright (C) 2021 LSPosed Contributors +# + +check_magisk_version() { + ui_print "- Magisk version: $MAGISK_VER_CODE" + if [ "$FLAVOR" == "riru" ] || [ "$FLAVOR" == "zygisk" ]; then + if [ "$MAGISK_VER_CODE" -lt 26000 ]; then + ui_print "*********************************************************" + ui_print "! Please install Magisk v26+" + abort "*********************************************************" + fi + else + ui_print "*********************************************************" + ui_print "! Unsupported flavor $FLAVOR" + abort "*********************************************************" + fi +} + +require_new_android() { + ui_print "*********************************************************" + ui_print "! Unsupported Android version ${1} (below Oreo MR1)" + ui_print "! Learn more from our GitHub" + [ "$BOOTMODE" == "true" ] && am start -a android.intent.action.VIEW -d https://github.com/JingMatrix/LSPosed/#supported-versions + abort "*********************************************************" +} + +check_android_version() { + if [ "$API" -ge 27 ]; then + ui_print "- Android SDK version: $API" + else + require_new_android "$API" + fi +} + +check_incompatible_module() { + MODULEDIR="$(magisk --path)/.magisk/modules" + for id in "riru_dreamland" "riru_edxposed" "riru_edxposed_sandhook" "taichi"; do + if [ -d "$MODULEDIR/$id" ] && [ ! -f "$MODULEDIR/$id/disable" ] && [ ! -f "$MODULEDIR/$id/remove" ]; then + ui_print "*********************************************************" + ui_print "! Please disable or uninstall incompatible frameworks:" + ui_print "! $id" + abort "*********************************************************" + break + fi + done +} diff --git a/zygisk/module/verify.sh b/zygisk/module/verify.sh new file mode 100644 index 000000000..2e1f4efb8 --- /dev/null +++ b/zygisk/module/verify.sh @@ -0,0 +1,71 @@ +# +# This file is part of LSPosed. +# +# LSPosed is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# LSPosed is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LSPosed. If not, see . +# +# Copyright (C) 2020 EdXposed Contributors +# Copyright (C) 2021 LSPosed Contributors +# + +TMPDIR_FOR_VERIFY="$TMPDIR/.vunzip" +mkdir "$TMPDIR_FOR_VERIFY" + +abort_verify() { + ui_print "*********************************************************" + ui_print "! $1" + ui_print "! This zip may be corrupted, please try downloading again" + abort "*********************************************************" +} + +# extract +extract() { + zip=$1 + file=$2 + dir=$3 + junk_paths=$4 + [ -z "$junk_paths" ] && junk_paths=false + opts="-o" + [ $junk_paths = true ] && opts="-oj" + + file_path="" + hash_path="" + if [ $junk_paths = true ]; then + file_path="$dir/$(basename "$file")" + hash_path="$TMPDIR_FOR_VERIFY/$(basename "$file").sha256" + else + file_path="$dir/$file" + hash_path="$TMPDIR_FOR_VERIFY/$file.sha256" + fi + + unzip $opts "$zip" "$file" -d "$dir" >&2 + [ -f "$file_path" ] || abort_verify "$file not exists" + + unzip $opts "$zip" "$file.sha256" -d "$TMPDIR_FOR_VERIFY" >&2 + [ -f "$hash_path" ] || abort_verify "$file.sha256 not exists" + + (echo "$(cat "$hash_path") $file_path" | sha256sum -c -s -) || abort_verify "Failed to verify $file" + ui_print "- Verified $file" >&1 +} + +file="META-INF/com/google/android/update-binary" +file_path="$TMPDIR_FOR_VERIFY/$file" +hash_path="$file_path.sha256" +unzip -o "$ZIPFILE" "META-INF/com/google/android/*" -d "$TMPDIR_FOR_VERIFY" >&2 +[ -f "$file_path" ] || abort_verify "$file not exists" +if [ -f "$hash_path" ]; then + (echo "$(cat "$hash_path") $file_path" | sha256sum -c -s -) || abort_verify "Failed to verify $file" + ui_print "- Verified $file" >&1 +else + ui_print "- Download from Magisk app" +fi diff --git a/zygisk/proguard-rules.pro b/zygisk/proguard-rules.pro new file mode 100644 index 000000000..7eee05bdc --- /dev/null +++ b/zygisk/proguard-rules.pro @@ -0,0 +1,18 @@ +-keepclasseswithmembers class org.matrix.vector.core.Main { + public static void forkCommon(boolean, java.lang.String, java.lang.String, android.os.IBinder); +} +-keepclasseswithmembers,includedescriptorclasses class * { + native ; +} +-keepclasseswithmembers class org.matrix.vector.service.BridgeService { + public static boolean *(android.os.IBinder, int, long, long, int); +} + +-assumenosideeffects class android.util.Log { + public static *** v(...); + public static *** d(...); +} +-repackageclasses +-allowaccessmodification +-dontwarn org.lsposed.lspd.core.* +-dontwarn org.lsposed.lspd.util.Hookers diff --git a/zygisk/src/main/AndroidManifest.xml b/zygisk/src/main/AndroidManifest.xml new file mode 100644 index 000000000..cc947c567 --- /dev/null +++ b/zygisk/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/zygisk/src/main/cpp/CMakeLists.txt b/zygisk/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..52a4794fa --- /dev/null +++ b/zygisk/src/main/cpp/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.10) + +project(zygisk) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_subdirectory(${VECTOR_ROOT}/native native) + +add_library(${PROJECT_NAME} SHARED module.cpp ipc_bridge.cpp) +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} native log) + +if (DEFINED DEBUG_SYMBOLS_PATH) + message(STATUS "Debug symbols will be placed at ${DEBUG_SYMBOLS_PATH}") + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} + COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ + ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug + COMMAND ${CMAKE_STRIP} --strip-all $ + COMMAND ${CMAKE_OBJCOPY} --add-gnu-debuglink ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug + $) +endif() diff --git a/zygisk/src/main/cpp/include/ipc_bridge.h b/zygisk/src/main/cpp/include/ipc_bridge.h new file mode 100644 index 000000000..753225060 --- /dev/null +++ b/zygisk/src/main/cpp/include/ipc_bridge.h @@ -0,0 +1,182 @@ +#pragma once + +#include + +#include +#include +#include +#include + +// This module is a client of the 'native' library. +// We include the JNI bridge from 'native' to leverage its helper functions. +#include + +namespace vector::native::module { + +/** + * @class IPCBridge + * @brief Manages Binder IPC communication with the Vector host service. + * + * This singleton class is the communication arm of the Zygisk module. Its key + * responsibilities are: + * 1. Discovering and connecting to the central host service (the "manager"). + * 2. Requesting the framework's DEX file and obfuscation map from the service. + * 3. Caching all necessary JNI class and method IDs for efficient reuse. + */ +class IPCBridge { +public: + // Enforce singleton pattern. + IPCBridge(const IPCBridge &) = delete; + IPCBridge &operator=(const IPCBridge &) = delete; + + /** + * @brief Gets the singleton instance of the IPCBridge. + */ + static IPCBridge &GetInstance(); + + /** + * @brief Caches JNI class and method IDs needed for Binder communication. + * @param env A valid JNI environment pointer. + */ + void Initialize(JNIEnv *env); + + /** + * @brief Requests an application-specific Binder from the host service. + * @param env JNI environment pointer. + * @param nice_name The process name. + * @return A ScopedLocalRef to the Binder object, or nullptr on failure. + */ + lsplant::ScopedLocalRef RequestAppBinder(JNIEnv *env, jstring nice_name); + + /** + * @brief Requests the system_server's dedicated Binder from the host service. + * @param env JNI environment pointer. + * @return A ScopedLocalRef to the Binder object, or nullptr on failure. + */ + lsplant::ScopedLocalRef RequestSystemServerBinder(JNIEnv *env); + + /** + * @brief Asks the system_server binder for the application manager binder. + * @param env JNI environment pointer. + * @param system_server_binder The binder connected to the system server + * service. + * @return A ScopedLocalRef to the application manager Binder, or nullptr on + * failure. + */ + lsplant::ScopedLocalRef RequestManagerBinderFromSystemServer( + JNIEnv *env, jobject system_server_binder); + + /** + * @brief Fetches the framework DEX file via the provided Binder connection. + * @param env JNI environment pointer. + * @param binder A live Binder connection to the host service. + * @return A tuple containing the file descriptor and size of the DEX file. + * Returns {-1, 0} on failure. + */ + std::tuple FetchFrameworkDex(JNIEnv *env, jobject binder); + + /** + * @brief Fetches the framework's obfuscation map via the provided Binder. + * @param env JNI environment pointer. + * @param binder A live Binder connection to the host service. + * @return A map of original names to obfuscated names. + */ + std::map FetchObfuscationMap(JNIEnv *env, jobject binder); + + /** + * @brief Sets up the JNI hook to intercept Binder transactions. + * + * This is the core of the IPC interception mechanism. It replaces the JNI + * function pointer for CallBooleanMethodV to inspect calls to + * Binder.execTransact, allowing the framework to handle its own custom + * transaction codes directly. + * @param env JNI environment pointer. + */ + void HookBridge(JNIEnv *env); + +private: + /** + * @class ParcelWrapper + * @brief A private RAII wrapper to ensure Parcel objects are always recycled. + * + * As a private nested class, it has access to the private members of + * IPCBridge (like parcel_class_ and recycle_method_) without needing a + * 'friend' declaration. This is a clean, encapsulated implementation detail. + */ + class ParcelWrapper { + public: + ParcelWrapper(JNIEnv *env, IPCBridge *bridge); + ~ParcelWrapper(); + // Disable copy operations + ParcelWrapper(const ParcelWrapper &) = delete; + ParcelWrapper &operator=(const ParcelWrapper &) = delete; + + lsplant::ScopedLocalRef data; + lsplant::ScopedLocalRef reply; + + private: + JNIEnv *env_; + IPCBridge *bridge_; + }; + + // Private constructor for singleton. + IPCBridge() = default; + + bool initialized_ = false; + + // --- Cached JNI References --- + // These are initialized once and stored as global references for performance. + + // android.os.ServiceManager + jclass service_manager_class_ = nullptr; + jmethodID get_service_method_ = nullptr; + + // android.os.IBinder + jmethodID transact_method_ = nullptr; + + // android.os.Binder + jclass binder_class_ = nullptr; + jmethodID binder_ctor_ = nullptr; + + // android.os.Parcel + jclass parcel_class_ = nullptr; + jmethodID obtain_method_ = nullptr; + jmethodID recycle_method_ = nullptr; + jmethodID write_interface_token_method_ = nullptr; + jmethodID write_int_method_ = nullptr; + jmethodID write_string_method_ = nullptr; + jmethodID write_strong_binder_method_ = nullptr; + jmethodID read_exception_method_ = nullptr; + jmethodID read_strong_binder_method_ = nullptr; + jmethodID read_file_descriptor_method_ = nullptr; + jmethodID read_int_method_ = nullptr; + jmethodID read_long_method_ = nullptr; + jmethodID read_string_method_ = nullptr; + + // android.os.ParcelFileDescriptor + jclass parcel_fd_class_ = nullptr; + jmethodID detach_fd_method_ = nullptr; + + // --- JNI Hooking Members --- + // These are required to store the state for our JNI function table override. + + // The C++ hook function that will replace the original CallBooleanMethodV. + static jboolean JNICALL CallBooleanMethodV_Hook(JNIEnv *env, jobject obj, jmethodID methodId, + va_list args); + // The helper function that handles our specific transaction code. + static jboolean ExecTransact_Replace(jboolean *res, JNIEnv *env, jobject obj, va_list args); + + // A complete copy of the original JNI function table, with our hook installed. + JNINativeInterface native_interface_hook_{}; + // The original jmethodID for android.os.Binder.execTransact(). + jmethodID exec_transact_backup_method_id_ = nullptr; + // A function pointer to the original CallBooleanMethodV implementation. + jboolean (*call_boolean_method_v_backup_)(JNIEnv *, jobject, jmethodID, va_list) = nullptr; + + // A global reference to the framework's Java BridgeService class. + jclass bridge_service_class_ = nullptr; + // The jmethodID for the static Java method that handles the intercepted transaction. + jmethodID exec_transact_replace_method_id_ = nullptr; +}; + +} // namespace vector::native::module diff --git a/zygisk/src/main/cpp/include/zygisk.hpp b/zygisk/src/main/cpp/include/zygisk.hpp new file mode 100644 index 000000000..6bd6c6f6f --- /dev/null +++ b/zygisk/src/main/cpp/include/zygisk.hpp @@ -0,0 +1,392 @@ +/* Copyright 2022-2023 John "topjohnwu" Wu + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +// This is the public API for Zygisk modules. +// DO NOT MODIFY ANY CODE IN THIS HEADER. + +#pragma once + +#include +#include + +#define ZYGISK_API_VERSION 4 + +/* + +*************** +* Introduction +*************** + +On Android, all app processes are forked from a special daemon called "Zygote". +For each new app process, zygote will fork a new process and perform "specialization". +This specialization operation enforces the Android security sandbox on the newly forked +process to make sure that 3rd party application code is only loaded after it is being +restricted within a sandbox. + +On Android, there is also this special process called "system_server". This single +process hosts a significant portion of system services, which controls how the +Android operating system and apps interact with each other. + +The Zygisk framework provides a way to allow developers to build modules and run custom +code before and after system_server and any app processes' specialization. +This enable developers to inject code and alter the behavior of system_server and app processes. + +Please note that modules will only be loaded after zygote has forked the child process. +THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON! + +********************* +* Development Guide +********************* + +Define a class and inherit zygisk::ModuleBase to implement the functionality of your module. +Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk. + +Example code: + +static jint (*orig_logger_entry_max)(JNIEnv *env); +static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); } + +class ExampleModule : public zygisk::ModuleBase { +public: + void onLoad(zygisk::Api *api, JNIEnv *env) override { + this->api = api; + this->env = env; + } + void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { + JNINativeMethod methods[] = { + { "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max }, + }; + api->hookJniNativeMethods(env, "android/util/Log", methods, 1); + *(void **) &orig_logger_entry_max = methods[0].fnPtr; + } +private: + zygisk::Api *api; + JNIEnv *env; +}; + +REGISTER_ZYGISK_MODULE(ExampleModule) + +----------------------------------------------------------------------------------------- + +Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize, +or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class +never runs in a true superuser environment. + +If your module require access to superuser permissions, you can create and register +a root companion handler function. This function runs in a separate root companion +daemon process, and an Unix domain socket is provided to allow you to perform IPC between +your target process and the root companion process. + +Example code: + +static void example_handler(int socket) { ... } + +REGISTER_ZYGISK_COMPANION(example_handler) + +*/ + +namespace zygisk { + +struct Api; +struct AppSpecializeArgs; +struct ServerSpecializeArgs; + +class ModuleBase { +public: + + // This method is called as soon as the module is loaded into the target process. + // A Zygisk API handle will be passed as an argument. + virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {} + + // This method is called before the app process is specialized. + // At this point, the process just got forked from zygote, but no app specific specialization + // is applied. This means that the process does not have any sandbox restrictions and + // still runs with the same privilege of zygote. + // + // All the arguments that will be sent and used for app specialization is passed as a single + // AppSpecializeArgs object. You can read and overwrite these arguments to change how the app + // process will be specialized. + // + // If you need to run some operations as superuser, you can call Api::connectCompanion() to + // get a socket to do IPC calls with a root companion process. + // See Api::connectCompanion() for more info. + virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {} + + // This method is called after the app process is specialized. + // At this point, the process has all sandbox restrictions enabled for this application. + // This means that this method runs with the same privilege of the app's own code. + virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {} + + // This method is called before the system server process is specialized. + // See preAppSpecialize(args) for more info. + virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {} + + // This method is called after the system server process is specialized. + // At this point, the process runs with the privilege of system_server. + virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {} +}; + +struct AppSpecializeArgs { + // Required arguments. These arguments are guaranteed to exist on all Android versions. + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jobjectArray &rlimits; + jint &mount_external; + jstring &se_info; + jstring &nice_name; + jstring &instruction_set; + jstring &app_data_dir; + + // Optional arguments. Please check whether the pointer is null before de-referencing + jintArray *const fds_to_ignore; + jboolean *const is_child_zygote; + jboolean *const is_top_app; + jobjectArray *const pkg_data_info_list; + jobjectArray *const whitelisted_data_info_list; + jboolean *const mount_data_dirs; + jboolean *const mount_storage_dirs; + + AppSpecializeArgs() = delete; +}; + +struct ServerSpecializeArgs { + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jlong &permitted_capabilities; + jlong &effective_capabilities; + + ServerSpecializeArgs() = delete; +}; + +namespace internal { +struct api_table; +template void entry_impl(api_table *, JNIEnv *); +} + +// These values are used in Api::setOption(Option) +enum Option : int { + // Force Magisk's denylist unmount routines to run on this process. + // + // Setting this option only makes sense in preAppSpecialize. + // The actual unmounting happens during app process specialization. + // + // Set this option to force all Magisk and modules' files to be unmounted from the + // mount namespace of the process, regardless of the denylist enforcement status. + FORCE_DENYLIST_UNMOUNT = 0, + + // When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize. + // Be aware that after dlclose-ing your module, all of your code will be unmapped from memory. + // YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS. + DLCLOSE_MODULE_LIBRARY = 1, +}; + +// Bit masks of the return value of Api::getFlags() +enum StateFlag : uint32_t { + // The user has granted root access to the current process + PROCESS_GRANTED_ROOT = (1u << 0), + + // The current process was added on the denylist + PROCESS_ON_DENYLIST = (1u << 1), +}; + +// All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded +// from the specialized process afterwards. +struct Api { + + // Connect to a root companion process and get a Unix domain socket for IPC. + // + // This API only works in the pre[XXX]Specialize methods due to SELinux restrictions. + // + // The pre[XXX]Specialize methods run with the same privilege of zygote. + // If you would like to do some operations with superuser permissions, register a handler + // function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func). + // Another good use case for a companion process is that if you want to share some resources + // across multiple processes, hold the resources in the companion process and pass it over. + // + // The root companion process is ABI aware; that is, when calling this method from a 32-bit + // process, you will be connected to a 32-bit companion process, and vice versa for 64-bit. + // + // Returns a file descriptor to a socket that is connected to the socket passed to your + // module's companion request handler. Returns -1 if the connection attempt failed. + int connectCompanion(); + + // Get the file descriptor of the root folder of the current module. + // + // This API only works in the pre[XXX]Specialize methods. + // Accessing the directory returned is only possible in the pre[XXX]Specialize methods + // or in the root companion process (assuming that you sent the fd over the socket). + // Both restrictions are due to SELinux and UID. + // + // Returns -1 if errors occurred. + int getModuleDir(); + + // Set various options for your module. + // Please note that this method accepts one single option at a time. + // Check zygisk::Option for the full list of options available. + void setOption(Option opt); + + // Get information about the current process. + // Returns bitwise-or'd zygisk::StateFlag values. + uint32_t getFlags(); + + // Exempt the provided file descriptor from being automatically closed. + // + // This API only make sense in preAppSpecialize; calling this method in any other situation + // is either a no-op (returns true) or an error (returns false). + // + // When false is returned, the provided file descriptor will eventually be closed by zygote. + bool exemptFd(int fd); + + // Hook JNI native methods for a class + // + // Lookup all registered JNI native methods and replace it with your own methods. + // The original function pointer will be saved in each JNINativeMethod's fnPtr. + // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr + // will be set to nullptr. + void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); + + // Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory. + // + // Parsing /proc/[PID]/maps will give you the memory map of a process. As an example: + // + //
+ // 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64 + // (More details: https://man7.org/linux/man-pages/man5/proc.5.html) + // + // The `dev` and `inode` pair uniquely identifies a file being mapped into memory. + // For matching ELFs loaded in memory, replace function `symbol` with `newFunc`. + // If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`. + void pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc); + + // Commit all the hooks that was previously registered. + // Returns false if an error occurred. + bool pltHookCommit(); + +private: + internal::api_table *tbl; + template friend void internal::entry_impl(internal::api_table *, JNIEnv *); +}; + +// Register a class as a Zygisk module + +#define REGISTER_ZYGISK_MODULE(clazz) \ +void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ + zygisk::internal::entry_impl(table, env); \ +} + +// Register a root companion request handler function for your module +// +// The function runs in a superuser daemon process and handles a root companion request from +// your module running in a target process. The function has to accept an integer value, +// which is a Unix domain socket that is connected to the target process. +// See Api::connectCompanion() for more info. +// +// NOTE: the function can run concurrently on multiple threads. +// Be aware of race conditions if you have globally shared resources. + +#define REGISTER_ZYGISK_COMPANION(func) \ +void zygisk_companion_entry(int client) { func(client); } + +/********************************************************* + * The following is internal ABI implementation detail. + * You do not have to understand what it is doing. + *********************************************************/ + +namespace internal { + +struct module_abi { + long api_version; + ModuleBase *impl; + + void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *); + void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *); + void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *); + void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *); + + module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) { + preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); }; + postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); }; + preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); }; + postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); }; + } +}; + +struct api_table { + // Base + void *impl; + bool (*registerModule)(api_table *, module_abi *); + + void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int); + void (*pltHookRegister)(dev_t, ino_t, const char *, void *, void **); + bool (*exemptFd)(int); + bool (*pltHookCommit)(); + int (*connectCompanion)(void * /* impl */); + void (*setOption)(void * /* impl */, Option); + int (*getModuleDir)(void * /* impl */); + uint32_t (*getFlags)(void * /* impl */); +}; + +template +void entry_impl(api_table *table, JNIEnv *env) { + static Api api; + api.tbl = table; + static T module; + ModuleBase *m = &module; + static module_abi abi(m); + if (!table->registerModule(table, &abi)) return; + m->onLoad(&api, env); +} + +} // namespace internal + +inline int Api::connectCompanion() { + return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1; +} +inline int Api::getModuleDir() { + return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1; +} +inline void Api::setOption(Option opt) { + if (tbl->setOption) tbl->setOption(tbl->impl, opt); +} +inline uint32_t Api::getFlags() { + return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0; +} +inline bool Api::exemptFd(int fd) { + return tbl->exemptFd != nullptr && tbl->exemptFd(fd); +} +inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { + if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods); +} +inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc) { + if (tbl->pltHookRegister) tbl->pltHookRegister(dev, inode, symbol, newFunc, oldFunc); +} +inline bool Api::pltHookCommit() { + return tbl->pltHookCommit != nullptr && tbl->pltHookCommit(); +} + +} // namespace zygisk + +extern "C" { + +[[gnu::visibility("default"), maybe_unused]] +void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); + +[[gnu::visibility("default"), maybe_unused]] +void zygisk_companion_entry(int); + +} // extern "C" diff --git a/zygisk/src/main/cpp/ipc_bridge.cpp b/zygisk/src/main/cpp/ipc_bridge.cpp new file mode 100644 index 000000000..b2168f3b3 --- /dev/null +++ b/zygisk/src/main/cpp/ipc_bridge.cpp @@ -0,0 +1,587 @@ +#include "ipc_bridge.h" + +#include +#include +#include + +#include +#include +#include + +using namespace std::literals::string_view_literals; + +#include + +namespace vector::native::module { + +// A file-level static atomic variable to store the ID of the last caller +// whose framework transaction failed. It's initialized to a value that won't match. +static std::atomic g_last_failed_id = ~0; + +/** + * @class BinderCaller + * @brief A helper to get the UID and PID of the current Binder caller. + * + * This class encapsulates the logic for finding and calling private functions + * within libbinder.so to identify the origin of an IPC transaction. + */ +class BinderCaller { +public: + /** + * @brief Initializes the helper by finding the required function pointers. + * This must be called once before GetId() is used. + */ + static void Initialize() { + // Use the native library's symbol cache to find symbols in the loaded libbinder.so + auto libbinder = ElfSymbolCache::GetLibBinder(); + if (!libbinder) { + LOGW("libbinder.so not found in cache, cannot get caller ID."); + return; + } + + s_self_or_null_fn = (IPCThreadState * (*)()) libbinder->getSymbAddress( + "_ZN7android14IPCThreadState10selfOrNullEv"); + s_get_calling_pid_fn = (pid_t(*)(IPCThreadState *))libbinder->getSymbAddress( + "_ZNK7android14IPCThreadState13getCallingPidEv"); + s_get_calling_uid_fn = (uid_t(*)(IPCThreadState *))libbinder->getSymbAddress( + "_ZNK7android14IPCThreadState13getCallingUidEv"); + + if (!s_self_or_null_fn || !s_get_calling_pid_fn || !s_get_calling_uid_fn) { + LOGW("Could not resolve all IPCThreadState symbols. Caller ID check will be disabled."); + } else { + LOGI("IPCThreadState symbols resolved successfully."); + } + } + + /** + * @brief Gets the unique 64-bit ID of the current Binder caller. + * @return A combined UID and PID, or 0 if symbols are not available. + */ + static uint64_t GetId() { + if (!s_self_or_null_fn) [[unlikely]] + return 0; + + IPCThreadState *self = s_self_or_null_fn(); + if (!self) return 0; + + auto uid = s_get_calling_uid_fn(self); + auto pid = s_get_calling_pid_fn(self); + return (static_cast(uid) << 32) | pid; + } + +private: + // Forward declare the opaque struct from libbinder. + struct IPCThreadState; + + inline static IPCThreadState *(*s_self_or_null_fn)() = nullptr; + inline static pid_t (*s_get_calling_pid_fn)(IPCThreadState *) = nullptr; + inline static uid_t (*s_get_calling_uid_fn)(IPCThreadState *) = nullptr; +}; + +// --- Binder IPC Protocol Constants --- +// These are the "secret handshakes" used to communicate with the Vector manager +// service. + +// The service descriptor that the remote Binder service expects. +constexpr auto kBridgeServiceDescriptor = "LSPosed"sv; +// The name of the system service we use as a rendezvous point to find our +// manager service. Using "activity" is a common technique as it's always +// available. +constexpr auto kBridgeServiceName = "activity"sv; +// A different rendezvous point used only by the system_server. +constexpr auto kSystemServerBridgeServiceName = "serial"sv; + +// Transaction codes for specific actions. +constexpr jint kBridgeTransactionCode = 1598837584; +constexpr jint kDexTransactionCode = 1310096052; +constexpr jint kObfuscationMapTransactionCode = 724533732; + +// Action codes sent within a kBridgeTransactionCode transaction. +constexpr jint kActionGetBinder = 2; + +// ========================================================================================= +// Implementation of IPCBridge::ParcelWrapper (Private Nested Class) +// ========================================================================================= + +/** + * @brief Constructs the ParcelWrapper, obtaining two new Parcel objects from + * the pool. + * @param env A valid JNI environment pointer. + * @param bridge A pointer to the parent IPCBridge instance to access its cached + * JNI IDs. + */ +IPCBridge::ParcelWrapper::ParcelWrapper(JNIEnv *env, IPCBridge *bridge) + : data(lsplant::JNI_CallStaticObjectMethod(env, bridge->parcel_class_, bridge->obtain_method_)), + reply( + lsplant::JNI_CallStaticObjectMethod(env, bridge->parcel_class_, bridge->obtain_method_)), + env_(env), + bridge_(bridge) {} + +/** + * @brief Destructs the ParcelWrapper, ensuring both Parcel objects are + * recycled. + * + * This is the core of the RAII pattern for Parcels. This destructor guarantees + * that recycle() is called, preventing resource leaks even if errors occur + * during the transaction. + */ +IPCBridge::ParcelWrapper::~ParcelWrapper() { + // Check if the parcel was successfully obtained before trying to recycle it. + if (data) { + lsplant::JNI_CallVoidMethod(env_, data.get(), bridge_->recycle_method_); + } + if (reply) { + lsplant::JNI_CallVoidMethod(env_, reply.get(), bridge_->recycle_method_); + } +} + +// ========================================================================================= +// IPCBridge Implementation +// ========================================================================================= + +IPCBridge &IPCBridge::GetInstance() { + static IPCBridge instance; + return instance; +} + +void IPCBridge::Initialize(JNIEnv *env) { + if (initialized_) { + return; + } + + // --- Cache JNI Classes and Method IDs --- + // Caching these at startup is more efficient and robust than looking them + // up on every IPC call. If any of these fail, the IPC bridge is unusable. + + // ServiceManager + auto sm_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/ServiceManager")); + if (!sm_class) { + LOGE("IPCBridge: ServiceManager class not found!"); + return; + } + service_manager_class_ = (jclass)env->NewGlobalRef(sm_class.get()); + get_service_method_ = lsplant::JNI_GetStaticMethodID( + env, service_manager_class_, "getService", "(Ljava/lang/String;)Landroid/os/IBinder;"); + if (!get_service_method_) { + LOGE("IPCBridge: ServiceManager.getService method not found!"); + return; + } + + // IBinder + auto ibinder_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/IBinder")); + if (!ibinder_class) { + LOGE("IPCBridge: IBinder class not found!"); + return; + } + transact_method_ = lsplant::JNI_GetMethodID(env, ibinder_class.get(), "transact", + "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z"); + if (!transact_method_) { + LOGE("IPCBridge: IBinder.transact method not found!"); + return; + } + + // Binder + auto binder_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/Binder")); + if (!binder_class) { + LOGE("IPCBridge: Binder class not found!"); + return; + } + binder_class_ = (jclass)env->NewGlobalRef(binder_class.get()); + binder_ctor_ = lsplant::JNI_GetMethodID(env, binder_class_, "", "()V"); + if (!binder_ctor_) { + LOGE("IPCBridge: Binder constructor not found!"); + return; + } + + // Parcel + auto parcel_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/Parcel")); + if (!parcel_class) { + LOGE("IPCBridge: Parcel class not found!"); + return; + } + parcel_class_ = (jclass)env->NewGlobalRef(parcel_class.get()); + obtain_method_ = + lsplant::JNI_GetStaticMethodID(env, parcel_class_, "obtain", "()Landroid/os/Parcel;"); + recycle_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "recycle", "()V"); + write_interface_token_method_ = lsplant::JNI_GetMethodID( + env, parcel_class_, "writeInterfaceToken", "(Ljava/lang/String;)V"); + write_int_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "writeInt", "(I)V"); + write_string_method_ = + lsplant::JNI_GetMethodID(env, parcel_class_, "writeString", "(Ljava/lang/String;)V"); + write_strong_binder_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "writeStrongBinder", + "(Landroid/os/IBinder;)V"); + read_exception_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readException", "()V"); + read_strong_binder_method_ = + lsplant::JNI_GetMethodID(env, parcel_class_, "readStrongBinder", "()Landroid/os/IBinder;"); + read_file_descriptor_method_ = lsplant::JNI_GetMethodID( + env, parcel_class_, "readFileDescriptor", "()Landroid/os/ParcelFileDescriptor;"); + read_int_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readInt", "()I"); + read_long_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readLong", "()J"); + read_string_method_ = + lsplant::JNI_GetMethodID(env, parcel_class_, "readString", "()Ljava/lang/String;"); + + // ParcelFileDescriptor + auto pfd_class = + lsplant::ScopedLocalRef(env, env->FindClass("android/os/ParcelFileDescriptor")); + if (!pfd_class) { + LOGE("IPCBridge: ParcelFileDescriptor class not found!"); + return; + } + parcel_fd_class_ = (jclass)env->NewGlobalRef(pfd_class.get()); + detach_fd_method_ = lsplant::JNI_GetMethodID(env, parcel_fd_class_, "detachFd", "()I"); + if (!detach_fd_method_) { + LOGE("IPCBridge: ParcelFileDescriptor.detachFd method not found!"); + return; + } + + LOGI("IPCBridge initialized successfully."); + initialized_ = true; +} + +lsplant::ScopedLocalRef IPCBridge::RequestAppBinder(JNIEnv *env, jstring nice_name) { + if (!initialized_) { + LOGE("RequestAppBinder failed: IPCBridge not initialized."); + return {env, nullptr}; + } + + // Step 1: Get the rendezvous service from the Android ServiceManager. + auto service_name = lsplant::ScopedLocalRef(env, env->NewStringUTF(kBridgeServiceName.data())); + auto bridge_service = lsplant::JNI_CallStaticObjectMethod( + env, service_manager_class_, get_service_method_, service_name.get()); + if (!bridge_service) { + LOGD("Could not get rendezvous service '{}'. Manager not available?", + kBridgeServiceName.data()); + return {env, nullptr}; + } + + // Step 2: Prepare the IPC transaction. + ParcelWrapper parcels(env, this); + if (!parcels.data || !parcels.reply) { + LOGE("Failed to obtain parcels for IPC."); + return {env, nullptr}; + } + + // This is a "heartbeat" binder. If our process dies, the manager service + // will be notified that this binder has died, allowing it to clean up + // resources. + auto heartbeat_binder = + lsplant::ScopedLocalRef(env, env->NewObject(binder_class_, binder_ctor_)); + if (!heartbeat_binder) { + LOGE("Failed to create heartbeat binder."); + return {env, nullptr}; + } + + // Step 3: Write the request data to the 'data' parcel. + auto descriptor = + lsplant::ScopedLocalRef(env, env->NewStringUTF(kBridgeServiceDescriptor.data())); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_interface_token_method_, + descriptor.get()); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, kActionGetBinder); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_string_method_, nice_name); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, + heartbeat_binder.get()); + + // Step 4: Perform the transaction. + bool success = lsplant::JNI_CallBooleanMethod(env, bridge_service.get(), transact_method_, + kBridgeTransactionCode, parcels.data.get(), + parcels.reply.get(), 0); + + lsplant::ScopedLocalRef result_binder = {env, nullptr}; + if (success) { + // Step 5: Read the reply. CRITICAL: must call readException first. + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); + if (env->ExceptionCheck()) { + LOGW("Remote exception received while requesting app binder."); + env->ExceptionClear(); + } else { + result_binder = + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_strong_binder_method_); + if (result_binder) { + // IMPORTANT: Keep the heartbeat binder alive by making it a global ref. + // If we don't do this, it gets garbage collected and the remote service + // thinks our process has died. + env->NewGlobalRef(heartbeat_binder.get()); + } + } + } else { + LOGW("Transact call to request app binder failed."); + } + + return result_binder; +} + +lsplant::ScopedLocalRef IPCBridge::RequestSystemServerBinder(JNIEnv *env) { + if (!initialized_) { + LOGE("RequestSystemServerBinder failed: IPCBridge not initialized."); + return {env, nullptr}; + } + + auto service_name = + lsplant::ScopedLocalRef(env, env->NewStringUTF(kSystemServerBridgeServiceName.data())); + lsplant::ScopedLocalRef binder = {env, nullptr}; + + // The system_server might start its services slightly after Zygisk injects + // us. We retry a few times to give it a chance to register. + for (int i = 0; i < 3; ++i) { + binder = lsplant::JNI_CallStaticObjectMethod(env, service_manager_class_, + get_service_method_, service_name.get()); + if (binder) { + LOGI("Got system server binder on attempt {}.", i + 1); + return binder; + } + if (i < 2) { + LOGW("Failed to get system server binder, will retry in 1 second..."); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } + + LOGE("Failed to get system server binder after 3 attempts. Aborting."); + return {env, nullptr}; +} + +lsplant::ScopedLocalRef IPCBridge::RequestManagerBinderFromSystemServer( + JNIEnv *env, jobject system_server_binder) { + ParcelWrapper parcels(env, this); + auto heartbeat_binder = + lsplant::ScopedLocalRef(env, env->NewObject(binder_class_, binder_ctor_)); + auto system_name = lsplant::ScopedLocalRef(env, env->NewStringUTF("system")); + + // Populate the request + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, getuid()); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, getpid()); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_string_method_, system_name.get()); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, + heartbeat_binder.get()); + + // Transact + bool success = lsplant::JNI_CallBooleanMethod(env, system_server_binder, transact_method_, + kBridgeTransactionCode, parcels.data.get(), + parcels.reply.get(), 0); + + lsplant::ScopedLocalRef result_binder = {env, nullptr}; + if (success) { + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); + if (env->ExceptionCheck()) { + LOGW("Remote exception while getting manager binder from system_server."); + env->ExceptionClear(); + } else { + result_binder = + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_strong_binder_method_); + if (result_binder) { + env->NewGlobalRef(heartbeat_binder.get()); // Keep heartbeat alive + } + } + } + LOGD("Manager binder from system_server: {}", static_cast(result_binder.get())); + return result_binder; +} + +std::tuple IPCBridge::FetchFrameworkDex(JNIEnv *env, jobject binder) { + if (!initialized_ || !binder) { + return {-1, 0}; + } + + ParcelWrapper parcels(env, this); + bool success = + lsplant::JNI_CallBooleanMethod(env, binder, transact_method_, kDexTransactionCode, + parcels.data.get(), parcels.reply.get(), 0); + + if (!success) { + LOGE("DEX fetch transaction failed."); + return {-1, 0}; + } + + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); + if (env->ExceptionCheck()) { + LOGE("Remote exception received while fetching DEX."); + env->ExceptionClear(); + return {-1, 0}; + } + + auto pfd = + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_file_descriptor_method_); + if (!pfd) { + LOGE("Received null ParcelFileDescriptor for DEX."); + return {-1, 0}; + } + + int fd = lsplant::JNI_CallIntMethod(env, pfd.get(), detach_fd_method_); + size_t size = static_cast( + lsplant::JNI_CallLongMethod(env, parcels.reply.get(), read_long_method_)); + + LOGI("Fetched framework DEX: fd={}, size={}", fd, size); + return {fd, size}; +} + +std::map IPCBridge::FetchObfuscationMap(JNIEnv *env, jobject binder) { + std::map result_map; + if (!initialized_ || !binder) { + return result_map; + } + + ParcelWrapper parcels(env, this); + bool success = lsplant::JNI_CallBooleanMethod(env, binder, transact_method_, + kObfuscationMapTransactionCode, + parcels.data.get(), parcels.reply.get(), 0); + + if (!success) { + LOGE("Obfuscation map fetch transaction failed."); + return result_map; + } + + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); + if (env->ExceptionCheck()) { + LOGE("Remote exception received while fetching obfuscation map."); + env->ExceptionClear(); + return result_map; + } + + int size = lsplant::JNI_CallIntMethod(env, parcels.reply.get(), read_int_method_); + if (size < 0 || (size % 2 != 0)) { + LOGE("Invalid size for obfuscation map received: %d", size); + return result_map; + } + + for (int i = 0; i < size / 2; ++i) { + auto key_jstr = lsplant::JNI_Cast( + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_string_method_)); + + auto val_jstr = lsplant::JNI_Cast( + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_string_method_)); + + if (env->ExceptionCheck() || !key_jstr || !val_jstr) { + LOGE("Error reading string from parcel for obfuscation map."); + env->ExceptionClear(); + result_map.clear(); // Return an empty map on error + return result_map; + } + + lsplant::JUTFString key_str(env, key_jstr.get()); + lsplant::JUTFString val_str(env, val_jstr.get()); + result_map[key_str.get()] = val_str.get(); + } + + LOGI("Fetched obfuscation map with {} entries.", result_map.size()); + return result_map; +} + +jboolean IPCBridge::ExecTransact_Replace(jboolean *res, JNIEnv *env, jobject obj, va_list args) { + va_list copy; + va_copy(copy, args); + // Extract arguments from the va_list for Binder.execTransact(int, long, long, int) + auto code = va_arg(copy, jint); + auto data_obj = va_arg(copy, jlong); + auto reply_obj = va_arg(copy, jlong); + auto flags = va_arg(copy, jint); + va_end(copy); + + // If the transaction code matches our special code, intercept it. + if (code == kBridgeTransactionCode) { + // Call the static Java method in our framework's BridgeService to handle the call. + *res = env->CallStaticBooleanMethod(GetInstance().bridge_service_class_, + GetInstance().exec_transact_replace_method_id_, obj, + code, data_obj, reply_obj, flags); + if (env->ExceptionCheck()) { + LOGW("Exception in Java BridgeService.execTransact handler."); + env->ExceptionClear(); + } + if (*res == JNI_FALSE) { + uint64_t caller_id = BinderCaller::GetId(); + if (caller_id != 0) { + g_last_failed_id.store(caller_id, std::memory_order_relaxed); + } + } + return true; // Return true to indicate we handled the call. + } + return false; // Not our transaction, let the original method run. +} + +jboolean JNICALL IPCBridge::CallBooleanMethodV_Hook(JNIEnv *env, jobject obj, jmethodID methodId, + va_list args) { + uint64_t current_caller_id = BinderCaller::GetId(); + if (current_caller_id != 0) { + uint64_t last_failed = g_last_failed_id.load(std::memory_order_relaxed); + // If this caller is the one that just failed, skip interception and go straight to the original function. + if (current_caller_id == last_failed) { + // We "consume" the failed state by resetting it, so the *next* call is not skipped. + g_last_failed_id.store(~0, std::memory_order_relaxed); + return GetInstance().call_boolean_method_v_backup_(env, obj, methodId, args); + } + } + + // Check if the method being called is the one we want to intercept: Binder.execTransact() + if (methodId == GetInstance().exec_transact_backup_method_id_) { + jboolean res = false; + // Attempt to handle the transaction with our replacement logic. + if (ExecTransact_Replace(&res, env, obj, args)) { + return res; // If we handled it, return the result directly. + } + // If not handled, fall through to call the original method. + } + // Call the original, real CallBooleanMethodV function. + return GetInstance().call_boolean_method_v_backup_(env, obj, methodId, args); +} + +void IPCBridge::HookBridge(JNIEnv *env) { + if (!initialized_) { + LOGE("Cannot hook bridge: IPCBridge is not initialized."); + return; + } + + // --- Step 1: Get framework-specific Java classes and methods --- + const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); + std::string bridge_service_class_name; + bridge_service_class_name = obfs_map.at("org.matrix.vector.service.") + "BridgeService"; + + auto bridge_class_ref = + Context::GetInstance()->FindClassFromCurrentLoader(env, bridge_service_class_name); + if (!bridge_class_ref) { + LOGE("Failed to find BridgeService class '{}'", bridge_service_class_name.c_str()); + return; + } + bridge_service_class_ = lsplant::JNI_NewGlobalRef(env, bridge_class_ref); + + exec_transact_replace_method_id_ = lsplant::JNI_GetStaticMethodID( + env, bridge_service_class_, "execTransact", "(Landroid/os/IBinder;IJJI)Z"); + if (!exec_transact_replace_method_id_) { + LOGE("Failed to find static method BridgeService.execTransact!"); + return; + } + + // --- Step 2: Prepare the JNI hook --- + // Get the original method ID for android.os.Binder.execTransact + exec_transact_backup_method_id_ = + lsplant::JNI_GetMethodID(env, binder_class_, "execTransact", "(IJJI)Z"); + if (!exec_transact_backup_method_id_) { + LOGE("Failed to find original method Binder.execTransact!"); + return; + } + + // Use the native library's API to get the JNI table override function. + auto set_table_override_func = + (void (*)(const JNINativeInterface *))ElfSymbolCache::GetArt()->getSymbAddress( + "_ZN3art9JNIEnvExt16SetTableOverrideEPK18JNINativeInterface"); + if (!set_table_override_func) { + LOGE("Failed to find ART symbol SetTableOverride!"); + return; + } + + // --- Step 3: Install the hook --- + // Make a full copy of the existing JNI function table. + memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); + + // Store a pointer to the original function we are about to replace. + call_boolean_method_v_backup_ = env->functions->CallBooleanMethodV; + + // Overwrite the function pointer in our copy with our hook. + native_interface_hook_.CallBooleanMethodV = &CallBooleanMethodV_Hook; + + // Atomically swap the process's JNI function table with our modified one. + set_table_override_func(&native_interface_hook_); + + BinderCaller::Initialize(); + + LOGI("IPC Bridge JNI hook installed successfully."); +} + +} // namespace vector::native::module diff --git a/zygisk/src/main/cpp/module.cpp b/zygisk/src/main/cpp/module.cpp new file mode 100644 index 000000000..7e7388a61 --- /dev/null +++ b/zygisk/src/main/cpp/module.cpp @@ -0,0 +1,457 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ipc_bridge.h" + +using namespace std::literals::string_view_literals; + +namespace vector::native::module { + +// --- Process UID Constants --- +// Values used to identify special Android processes to avoid injection. +// 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; + +// 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; + +// UID for the process responsible for creating shared RELRO files. +constexpr int SHARED_RELRO_UID = 1037; + +// Android uses this to separate users. UID = AppID + UserID * PER_USER_RANGE. +constexpr int PER_USER_RANGE = 100000; + +constexpr uid_t MANAGER_UID = INJECTED_UID; +constexpr uid_t GID_INET = 3003; // Android's Internet group ID. + +// 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(); } + + virtual obfuscation_map_t &obfuscation_map() override { return obfuscation_map_; } + + virtual void obfuscation_map(obfuscation_map_t m) override { obfuscation_map_ = std::move(m); } + +private: + ConfigImpl() = default; + + friend std::unique_ptr std::make_unique(); + inline static std::map obfuscation_map_; +}; + +/** + * @class VectorModule + * @brief The core implementation of the Zygisk module for the Vector framework. + * + * This class is the main entry point for Zygisk. It inherits from: + * - zygisk::ModuleBase: To receive lifecycle callbacks from the Zygisk loader. + * - vector::native::Context: To gain the core injection capabilities (DEX + * loading, ART hooking) from the 'native' library. + * + * It orchestrates the injection process by deciding which processes to target, + * 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); }, + }; + + // 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) { + LOGD("Loading framework DEX into memory (size: {}).", dex.size()); + + // Step 1: 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; + } + + // Step 2: 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; + } + + // Step 3: 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; + } + + // Store a global reference to our new ClassLoader. + inject_class_loader_ = env->NewGlobalRef(new_cl.get()); + LOGI("Framework ClassLoader created successfully."); +} + +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; + // Assume the native library provides a helper or direct map access. + 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; + } + + // Store a global reference to the entry class. + entry_class_ = lsplant::JNI_NewGlobalRef(env, entry_class); + LOGI("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 == MANAGER_UID) { + lsplant::JUTFString nice_name_str(env_, args->nice_name); + if (nice_name_str.get() == "org.lsposed.manager"sv) { + 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("com.android.shell"); + args->gids = new_gids; + } + } + + IPCBridge::GetInstance().Initialize(env_); + + // --- Injection Decision Logic --- + // Determine if the current process is a valid target for injection. + lsplant::JUTFString nice_name_str(env_, args->nice_name); + + // 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; + } + + // 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; + } + + // 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) { + LOGI("Skipping injection for '{}': is an isolated process (UID: %d).", nice_name_str.get(), + app_id); + return; + } + + // If we passed all checks, mark this process for injection. + should_inject_ = true; + LOGI("Process '{}' (UID: {}) is marked for injection.", nice_name_str.get(), args->uid); +} + +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("org.lsposed.manager"); + } + + // --- 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) { + LOGW("Failed to get IPC binder for '{}'. Skipping injection.", nice_name_str.get()); + SetAllowUnload(true); + 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 obfs_map = ipc_bridge.FetchObfuscationMap(env_, binder.get()); + ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); + + { + 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", + "(ZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_FALSE, + args->nice_name, args->app_data_dir, binder.get(), is_manager_app_); + + LOGI("Successfully 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; + } + + 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")) { + LOGI("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(); + } + } + + // --- Framework Injection for System Server --- + auto &ipc_bridge = IPCBridge::GetInstance(); + auto system_binder = ipc_bridge.RequestSystemServerBinder(env_); + if (!system_binder) { + LOGE("Failed to get system server IPC binder. Aborting injection."); + SetAllowUnload(true); // Allow unload on failure. + return; + } + + 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; + } + + 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); + + 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", + "(ZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_TRUE, + system_name.get(), nullptr, manager_binder.get(), is_manager_app_); + + LOGI("Successfully 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 + +// ========================================================================================= +// Zygisk Module Registration +// ========================================================================================= +REGISTER_ZYGISK_MODULE(vector::native::module::VectorModule); diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt new file mode 100644 index 000000000..609b1a62d --- /dev/null +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -0,0 +1,435 @@ +package org.matrix.vector + +import android.annotation.SuppressLint +import android.app.ActivityThread +import android.app.LoadedApk +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.* +import android.os.* +import android.util.AndroidRuntimeException +import android.util.ArrayMap +import android.webkit.WebViewDelegate +import android.webkit.WebViewFactory +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XC_MethodReplacement +import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers +import hidden.HiddenApiBridge +import java.io.FileInputStream +import java.io.FileOutputStream +import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.ILSPManagerService +import org.lsposed.lspd.util.Hookers +import org.lsposed.lspd.util.Utils +import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient + +/** The "Parasite" logic. Injects the LSPosed Manager APK into a host process (shell). */ +@SuppressLint("StaticFieldLeak") +object ParasiticManagerHooker { + private const val CHROMIUM_WEBVIEW_FACTORY_METHOD = "create" + + private var managerPkgInfo: PackageInfo? = null + private var managerFd: Int = -1 + + // Manually track Activity states since the system is unaware of our spoofed activities + private val states = ConcurrentHashMap() + private val persistentStates = ConcurrentHashMap() + + /** Constructs a hybrid PackageInfo. Combines the Manager's code with the Host's environment. */ + @Synchronized + private fun getManagerPkgInfo(appInfo: ApplicationInfo?): PackageInfo? { + if (managerPkgInfo == null && appInfo != null) { + runCatching { + val ctx: Context = ActivityThread.currentActivityThread().systemContext + var sourcePath = "/proc/self/fd/$managerFd" + + // SDK <= 28 (Android 9) cannot reliably parse APKs via FD paths in all contexts. + // We copy the APK to the host's cache as a workaround. + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + val dstPath = "${appInfo.dataDir}/cache/lsposed.apk" + runCatching { + FileInputStream(sourcePath).use { input -> + FileOutputStream(dstPath).use { output -> + input.channel.transferTo( + 0, + input.channel.size(), + output.channel, + ) + } + } + sourcePath = dstPath + } + .onFailure { Hookers.logE("Failed to copy parasitic APK", it) } + } + + val pkgInfo = + ctx.packageManager.getPackageArchiveInfo( + sourcePath, + PackageManager.GET_ACTIVITIES, + ) ?: throw RuntimeException("PackageManager failed to parse $sourcePath") + + // Transplant identity: Keep host's paths and UID, swap the code source + pkgInfo.applicationInfo!!.apply { + sourceDir = sourcePath + publicSourceDir = sourcePath + nativeLibraryDir = appInfo.nativeLibraryDir + packageName = appInfo.packageName + dataDir = + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(appInfo) + deviceProtectedDataDir = appInfo.deviceProtectedDataDir + processName = appInfo.processName + uid = appInfo.uid + // A14 QPR3 Fix: Ensure the flag for code existence is set + flags = flags or ApplicationInfo.FLAG_HAS_CODE + + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir( + this, + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(appInfo), + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + HiddenApiBridge.ApplicationInfo_overlayPaths( + this, + HiddenApiBridge.ApplicationInfo_overlayPaths(appInfo), + ) + } + HiddenApiBridge.ApplicationInfo_resourceDirs( + this, + HiddenApiBridge.ApplicationInfo_resourceDirs(appInfo), + ) + } + managerPkgInfo = pkgInfo + } + .onFailure { Utils.logE("Failed to construct manager PkgInfo", it) } + } + return managerPkgInfo + } + + /** + * Passes the IPC binder to the Manager's internal [Constants] class so it can communicate back + * to the LSPosed system service. + */ + private fun sendBinderToManager(classLoader: ClassLoader, binder: IBinder) { + runCatching { + val clazz = XposedHelpers.findClass("org.lsposed.manager.Constants", classLoader) + val ok = + XposedHelpers.callStaticMethod( + clazz, + "setBinder", + arrayOf(IBinder::class.java), + binder, + ) as Boolean + if (!ok) throw RuntimeException("setBinder returned false") + } + .onFailure { Utils.logW("Could not send binder to LSPosed Manager", it) } + } + + private fun hookForManager(managerService: ILSPManagerService) { + // Hook 1: Swap ApplicationInfo during host binding + XposedHelpers.findAndHookMethod( + ActivityThread::class.java, + "handleBindApplication", + "android.app.ActivityThread\$AppBindData", + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam<*>) { + Hookers.logD("ActivityThread#handleBindApplication() starts") + val bindData = param.args[0] + val hostAppInfo = + XposedHelpers.getObjectField(bindData, "appInfo") as ApplicationInfo + val parasiticInfo = getManagerPkgInfo(hostAppInfo)?.applicationInfo + XposedHelpers.setObjectField(bindData, "appInfo", parasiticInfo) + } + }, + ) + + // Hook 2: Inject APK path into the ClassLoader + var classLoaderUnhook: XC_MethodHook.Unhook? = null + val classLoaderHook = + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam<*>) { + val pkgInfo = getManagerPkgInfo(null) ?: return + val mAppInfo = + XposedHelpers.getObjectField(param.thisObject, "mApplicationInfo") + + val managerAppInfo = pkgInfo.applicationInfo!! + + if (mAppInfo == managerAppInfo) { + val dexPath = managerAppInfo.sourceDir + val pathClassLoader = param.result as ClassLoader + + Hookers.logD("Injecting DEX into LoadedApk ClassLoader: $pathClassLoader") + val pathList = XposedHelpers.getObjectField(pathClassLoader, "pathList") + val dexPaths = XposedHelpers.callMethod(pathList, "getDexPaths") as List<*> + + if (!dexPaths.contains(dexPath)) { + Utils.logW("Manager APK not found in ClassLoader, adding manually...") + XposedHelpers.callMethod(pathClassLoader, "addDexPath", dexPath) + } + sendBinderToManager(pathClassLoader, managerService.asBinder()) + classLoaderUnhook!!.unhook() // Only need to inject once + } + } + } + classLoaderUnhook = XposedHelpers.findAndHookMethod(LoadedApk::class.java, "getClassLoader", classLoaderHook) + + // Hook 3: Activity Lifecycle & Intent Redirection + val activityClientRecordClass = + XposedHelpers.findClass( + "android.app.ActivityThread\$ActivityClientRecord", + ActivityThread::class.java.classLoader, + ) + val activityHooker = + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam<*>) { + param.args.forEachIndexed { i, arg -> + if (arg is ActivityInfo) { + val pkgInfo = + getManagerPkgInfo(arg.applicationInfo) ?: return@forEachIndexed + pkgInfo.activities?.find { it.name == "org.lsposed.manager.ui.activity.MainActivity" } + ?.let { + it.applicationInfo = pkgInfo.applicationInfo + param.args[i] = it + } + } + if (arg is Intent) { + arg.component = + ComponentName( + arg.component!!.packageName, + "org.lsposed.manager.ui.activity.MainActivity", + ) + } + } + + // Captured State Injection + if (param.method.getName() == "scheduleLaunchActivity") { + var currentAInfo: ActivityInfo? = null + val types = (param.method as Method).parameterTypes + types.forEachIndexed { idx, type -> + when (type) { + ActivityInfo::class.java -> + currentAInfo = param.args[idx] as ActivityInfo + Bundle::class.java -> + currentAInfo?.let { info -> + states[info.name]?.let { param.args[idx] = it } + } + PersistableBundle::class.java -> + currentAInfo?.let { info -> + persistentStates[info.name]?.let { param.args[idx] = it } + } + } + } + } + } + + override fun afterHookedMethod(param: MethodHookParam<*>) { + if (!activityClientRecordClass.isInstance(param.thisObject)) return + param.args.filterIsInstance().forEach { aInfo -> + Hookers.logD("Restoring state for Activity: ${aInfo.name}") + states[aInfo.name]?.let { + XposedHelpers.setObjectField(param.thisObject, "state", it) + } + persistentStates[aInfo.name]?.let { + XposedHelpers.setObjectField(param.thisObject, "persistentState", it) + } + } + } + } + + XposedBridge.hookAllConstructors(activityClientRecordClass, activityHooker) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { + val appThreadClass = + XposedHelpers.findClass( + "android.app.ActivityThread\$ApplicationThread", + ActivityThread::class.java.classLoader, + ) + XposedBridge.hookAllMethods(appThreadClass, "scheduleLaunchActivity", activityHooker) + } + + // Hook 4: Ignore Receivers (Manager doesn't need to handle host receivers) + XposedBridge.hookAllMethods( + ActivityThread::class.java, + "handleReceiver", + object : XC_MethodReplacement() { + override fun replaceHookedMethod(param: MethodHookParam<*>): Any? { + param.args.filterIsInstance().forEach { + it.finish() + } + return null + } + }, + ) + + // Hook 5: Provider Context Spoofing + XposedBridge.hookAllMethods( + ActivityThread::class.java, + "installProvider", + object : XC_MethodHook() { + private var originalContext: Context? = null + + override fun beforeHookedMethod(param: MethodHookParam<*>) { + var ctx: Context? = null + var info: ProviderInfo? = null + var ctxIdx = -1 + + param.args.forEachIndexed { i, arg -> + when (arg) { + is Context -> { + ctx = arg + ctxIdx = i + } + is ProviderInfo -> info = arg + } + } + + val pkgInfo = getManagerPkgInfo(null) + if (ctx != null && info != null && pkgInfo != null) { + val managerPackage = pkgInfo.applicationInfo!!.packageName + if (info.applicationInfo.packageName != managerPackage) return + + if (originalContext == null) { + // Create a fake original context to satisfy internal package checks + info.applicationInfo.packageName = "$managerPackage.origin" + val compatibilityInfo = + HiddenApiBridge.Resources_getCompatibilityInfo(ctx!!.resources) + val originalPkgInfo = + ActivityThread.currentActivityThread() + .getPackageInfoNoCheck(info.applicationInfo, compatibilityInfo) + XposedHelpers.setObjectField( + originalPkgInfo, + "mPackageName", + managerPackage, + ) + + val contextImplClass = + XposedHelpers.findClass("android.app.ContextImpl", null) + originalContext = + XposedHelpers.callStaticMethod( + contextImplClass, + "createAppContext", + ActivityThread.currentActivityThread(), + originalPkgInfo, + ) as Context + info.applicationInfo.packageName = managerPackage + } + param.args[ctxIdx] = originalContext + } + } + }, + ) + + // Hook 6: WebView initialization within Parasitic process + XposedHelpers.findAndHookMethod( + WebViewFactory::class.java, + "getProvider", + object : XC_MethodReplacement() { + override fun replaceHookedMethod(param: MethodHookParam<*>): Any? { + val existing = + XposedHelpers.getStaticObjectField( + WebViewFactory::class.java, + "sProviderInstance", + ) + if (existing != null) return existing + + val providerClass = + XposedHelpers.callStaticMethod( + WebViewFactory::class.java, + "getProviderClass", + ) as Class<*> + return try { + val staticFactory = + providerClass.getMethod( + CHROMIUM_WEBVIEW_FACTORY_METHOD, + WebViewDelegate::class.java, + ) + val delegateCtor = + WebViewDelegate::class.java.getDeclaredConstructor().apply { + isAccessible = true + } + val instance = staticFactory.invoke(null, delegateCtor.newInstance()) + XposedHelpers.setStaticObjectField( + WebViewFactory::class.java, + "sProviderInstance", + instance, + ) + Hookers.logD("WebView provider initialized: $instance") + instance + } catch (e: Exception) { + Hookers.logE("WebView initialization failed", e) + throw AndroidRuntimeException(e) + } + } + }, + ) + + // Hook 7: State Capture on Stop + val stateCaptureHooker = + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam<*>) { + runCatching { + var record = param.args[0] + if (record is IBinder) { + val activities = + XposedHelpers.getObjectField(param.thisObject, "mActivities") + as ArrayMap<*, *> + record = activities[record] ?: return + } + + val saveMethod = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + "callActivityOnSaveInstanceState" + else "callCallActivityOnSaveInstanceState" + XposedHelpers.callMethod(param.thisObject, saveMethod, record) + + val state = XposedHelpers.getObjectField(record, "state") as? Bundle + val pState = + XposedHelpers.getObjectField(record, "persistentState") + as? PersistableBundle + val aInfo = + XposedHelpers.getObjectField(record, "activityInfo") as ActivityInfo + + state?.let { states[aInfo.name] = it } + pState?.let { persistentStates[aInfo.name] = it } + Hookers.logD("Saved state for ${aInfo.name}") + } + .onFailure { Hookers.logE("Failed to save activity state", it) } + } + } + XposedBridge.hookAllMethods( + ActivityThread::class.java, + "performStopActivityInner", + stateCaptureHooker, + ) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { + XposedHelpers.findAndHookMethod( + ActivityThread::class.java, + "performDestroyActivity", + IBinder::class.java, + Boolean::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + Boolean::class.javaPrimitiveType, + stateCaptureHooker, + ) + } + } + + /** Entry point. Checks if the current process should host the parasitic manager. */ + @JvmStatic + fun start(): Boolean { + val binderList = mutableListOf() + return try { + serviceClient.requestInjectedManagerBinder(binderList).use { pfd -> + managerFd = pfd.detachFd() + val managerService = ILSPManagerService.Stub.asInterface(binderList[0]) + hookForManager(managerService) + Utils.logD("Vector manager injected successfully into process.") + true + } + } catch (e: Throwable) { + Utils.logE("Parasitic injection failed", e) + false + } + } +} diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt new file mode 100644 index 000000000..1be3529c3 --- /dev/null +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt @@ -0,0 +1,118 @@ +package org.matrix.vector + +import android.annotation.SuppressLint +import android.app.ProfilerInfo +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ResolveInfo +import io.github.libxposed.api.XposedInterface +import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker +import org.lsposed.lspd.impl.LSPosedHelper +import org.matrix.vector.service.BridgeService +import org.lsposed.lspd.util.Utils + +/** + * Handles System-Server side logic for the Parasitic Manager. + * + * When a user tries to open the LSPosed Manager, the system normally wouldn't know how to handle it + * because it isn't "installed." This class intercepts the activity resolution and tells the system + * to launch it in a special process. + */ +class ParasiticManagerSystemHooker : HandleSystemServerProcessHooker.Callback { + + companion object { + @JvmStatic + fun start() { + // Register this class as the handler for system_server initialization + HandleSystemServerProcessHooker.callback = ParasiticManagerSystemHooker() + } + } + + /** Intercepts Activity resolution in the System Server. */ + object Hooker : XposedInterface.Hooker { + @JvmStatic + fun after(callback: XposedInterface.AfterHookCallback) { + val intent = callback.args[0] as? Intent ?: return + + // Check if this intent is meant for the LSPosed Manager + if (!intent.hasCategory("org.lsposed.manager.LAUNCH_MANAGER")) return + + val result = callback.result as? ActivityInfo ?: return + + // We only intercept if it's currently resolving to the shell/fallback + if (result.packageName != "com.android.shell") return + + // --- Redirection Logic --- + // We create a copy of the ActivityInfo to avoid polluting the system's cache. + val redirectedInfo = + ActivityInfo(result).apply { + // Force the manager to run in its own dedicated process name + processName = "org.lsposed.manager" + + // Set a standard theme so transition animations work correctly + theme = android.R.style.Theme_DeviceDefault_Settings + + // Ensure the activity isn't excluded from recents by host flags + flags = + flags and + (ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS or + ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS) + .inv() + } + + // Notify the bridge service that we are about to start the manager + BridgeService.getService()?.preStartManager() + + // Replace the original ResolveInfo with our parasitic one + callback.result = redirectedInfo + } + } + + @SuppressLint("PrivateApi") + override fun onSystemServerLoaded(classLoader: ClassLoader) { + runCatching { + // Android versions change the name of the internal class responsible for activity + // tracking. + // We check the most likely candidates based on API levels (9.0 through 14+). + val supervisorClass = + try { + // Android 12.0 - 14+ + Class.forName( + "com.android.server.wm.ActivityTaskSupervisor", + false, + classLoader, + ) + } catch (e: ClassNotFoundException) { + try { + // Android 10 - 11 + Class.forName( + "com.android.server.wm.ActivityStackSupervisor", + false, + classLoader, + ) + } catch (e2: ClassNotFoundException) { + // Android 8.1 - 9 + Class.forName( + "com.android.server.am.ActivityStackSupervisor", + false, + classLoader, + ) + } + } + + // Hook the resolution method to inject our redirection logic + LSPosedHelper.hookMethod( + Hooker::class.java, + supervisorClass, + "resolveActivity", + Intent::class.java, + ResolveInfo::class.java, + Int::class.javaPrimitiveType, + ProfilerInfo::class.java, + ) + + Utils.logD("Successfully hooked Activity Supervisor for Manager redirection.") + } + .onFailure { Utils.logE("Failed to hook system server activity resolution", it) } + } +} diff --git a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt new file mode 100644 index 000000000..84c7a6088 --- /dev/null +++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt @@ -0,0 +1,55 @@ +package org.matrix.vector.core + +import android.os.IBinder +import android.os.Process +import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient +import org.lsposed.lspd.util.Utils +import org.lsposed.lspd.service.ILSPApplicationService +import org.lsposed.lspd.core.Startup +import org.matrix.vector.ParasiticManagerHooker +import org.matrix.vector.ParasiticManagerSystemHooker + +/** + * Main entry point for the Java-side loader. This class is invoked via JNI from the Vector Zygisk + * module. + */ +object Main { + + /** + * Shared initialization logic for both System Server and Application processes. + * + * @param isSystem True if this is the system_server process. + * @param niceName The process name (e.g., package name or "system"). + * @param appDir The application's data directory. + * @param binder The Binder token associated with the application service. + */ + @JvmStatic + fun forkCommon(isSystem: Boolean, niceName: String, appDir: String?, binder: IBinder) { + // Step 1: Initialize system-specific resolution hooks if in system_server + if (isSystem) { + ParasiticManagerSystemHooker.start() + } + + // Step 2: Initialize Xposed bridge components + val appService = ILSPApplicationService.Stub.asInterface(binder) + Startup.initXposed(isSystem, niceName, appDir, appService) + + // Step 3: Configure logging levels from the service client + runCatching { Utils.Log.muted = serviceClient.isLogMuted } + .onFailure { t -> Utils.logE("Failed to configure logs from service", t) } + + // Step 4: Check if this process is the designated LSPosed Manager. + // If so, we perform "parasitic" injection into a host (com.android.shell) + // and terminate further standard Xposed loading for this specific process. + if ( + niceName == "org.lsposed.manager" && ParasiticManagerHooker.start() + ) { + Utils.logI("Parasitic manager loaded into host, skipping standard bootstrap.") + return + } + + // Step 5: Standard Xposed module loading for third-party apps + Utils.logI("Loading Vector/Xposed for $niceName (UID: ${Process.myUid()})") + Startup.bootstrapXposed() + } +} diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt new file mode 100644 index 000000000..c8b60ede0 --- /dev/null +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt @@ -0,0 +1,189 @@ +package org.matrix.vector.service + +import android.app.ActivityThread +import android.os.Binder +import android.os.IBinder +import android.os.IBinder.DeathRecipient +import android.os.Parcel +import android.os.Process +import hidden.HiddenApiBridge.Binder_allowBlocking +import hidden.HiddenApiBridge.Context_getActivityToken +import org.lsposed.lspd.util.Utils.Log +import org.lsposed.lspd.service.ILSPosedService + +/** + * Manages manual Binder transactions for the Vector framework. + * + * This service is not registered in ServiceManager. Instead, the Zygisk native module intercepts + * [Binder.execTransact] and redirects calls with the [TRANSACTION_CODE] to this class. + */ +object BridgeService { + private const val TRANSACTION_CODE = + ('_'.code shl 24) or ('L'.code shl 16) or ('S'.code shl 8) or 'P'.code + private const val DESCRIPTOR = "LSPosed" + private const val TAG = "LSPosed-Bridge" + + /** Actions supported by the manual IPC bridge. */ + private enum class Action { + UNKNOWN, + SEND_BINDER, // Daemon sending the system service binder + GET_BINDER, // Process requesting its specific application service + ENABLE_MANAGER, // Toggle manager state + } + + @Volatile private var serviceBinder: IBinder? = null + + @Volatile private var service: ILSPosedService? = null + + /** Cleans up service references if the remote LSPosed process crashes. */ + private val serviceRecipient: DeathRecipient = DeathRecipient { + Log.e(TAG, "LSPosed system service died.") + serviceBinder?.unlinkToDeath(this.serviceRecipient, 0) + serviceBinder = null + service = null + } + + /** Returns the active LSPosed system service interface. */ + @JvmStatic fun getService(): ILSPosedService? = service + + /** + * Initializes the client-side connection to the LSPosed system service. + * + * @param binder The raw binder for [ILSPosedService]. + */ + private fun receiveFromBridge(binder: IBinder?) { + if (binder == null) { + Log.e(TAG, "Received null binder from bridge.") + return + } + + // Cleanup old death recipient if we are re-initializing + val token = Binder.clearCallingIdentity() + try { + serviceBinder?.unlinkToDeath(serviceRecipient, 0) + } finally { + Binder.restoreCallingIdentity(token) + } + + // Allow blocking calls since we are often in a synchronous fork path + val blockingBinder = Binder_allowBlocking(binder) + serviceBinder = blockingBinder + service = ILSPosedService.Stub.asInterface(blockingBinder) + + runCatching { blockingBinder.linkToDeath(serviceRecipient, 0) } + .onFailure { Log.e(TAG, "Failed to link to service death", it) } + + // Provide the system context to the service so it can manage system-wide states + runCatching { + val activityThread = ActivityThread.currentActivityThread() + val at = activityThread.applicationThread as android.app.IApplicationThread + val atBinder = at.asBinder() + val systemCtx = activityThread.systemContext + service?.dispatchSystemServerContext( + atBinder, + Context_getActivityToken(systemCtx), + "Zygisk", + ) + } + .onFailure { Log.e(TAG, "Failed to dispatch system context", it) } + + Log.i(TAG, "LSPosed system service binder linked.") + } + + /** Handles manual parcel transactions. Called via reflection/JNI from the native hook. */ + @JvmStatic + fun onTransact(data: Parcel, reply: Parcel?, flags: Int): Boolean { + return try { + data.enforceInterface(DESCRIPTOR) + val actionIdx = data.readInt() + val action = Action.values().getOrElse(actionIdx) { Action.UNKNOWN } + + Log.d(TAG, "onTransact: action=$action, callerUid=${Binder.getCallingUid()}") + + when (action) { + Action.SEND_BINDER -> { + // Only allow root (UID 0) to push the initial service binder + if (Binder.getCallingUid() == 0) { + receiveFromBridge(data.readStrongBinder()) + reply?.writeNoException() + true + } else false + } + + Action.GET_BINDER -> { + val processName = data.readString() + val heartBeat = data.readStrongBinder() + val appService = + service?.requestApplicationService( + Binder.getCallingUid(), + Binder.getCallingPid(), + processName, + heartBeat, + ) + + if (appService != null && reply != null) { + reply.writeNoException() + reply.writeStrongBinder(appService.asBinder()) + true + } else false + } + + Action.ENABLE_MANAGER -> { + val uid = Binder.getCallingUid() + // Restricted to Root, System, or Shell + if ( + (uid == 0 || uid == Process.SHELL_UID || uid == Process.SYSTEM_UID) && + service != null + ) { + val enabled = data.readInt() == 1 + val result = service?.setManagerEnabled(enabled) ?: false + reply?.writeInt(if (result) 1 else 0) + true + } else false + } + + else -> false + } + } catch (e: Throwable) { + Log.e(TAG, "Error handling bridge transaction", e) + false + } + } + + /** + * Entry point for the JNI hook in [IPCBridge.cpp]. + * + * @param obj The Binder object being called. + * @param code The transaction code. + * @param dataObj Native pointer to the data Parcel. + * @param replyObj Native pointer to the reply Parcel. + * @param flags Transaction flags. + * @return True if the transaction was handled. + */ + @JvmStatic + fun execTransact(obj: IBinder, code: Int, dataObj: Long, replyObj: Long, flags: Int): Boolean { + if (code != TRANSACTION_CODE) return false + + val data = dataObj.asParcel() + val reply = replyObj.asParcel() + + if (data == null || reply == null) { + Log.w(TAG, "Transaction dropped: null parcel pointers.") + return false + } + + return try { + onTransact(data, reply, flags) + } catch (e: Exception) { + if (flags and IBinder.FLAG_ONEWAY == 0) { + reply.setDataPosition(0) + reply.writeException(e) + } + Log.e(TAG, "Exception during execTransact", e) + true // We handled it, even if by returning an exception + } finally { + data.recycle() + reply.recycle() + } + } +} diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt new file mode 100644 index 000000000..077afc42c --- /dev/null +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt @@ -0,0 +1,39 @@ +package org.matrix.vector.service + +import android.annotation.SuppressLint +import android.os.Parcel +import java.lang.reflect.Method + +/** + * Internal utilities for raw [Parcel] manipulation. Used primarily for IPC transactions that bypass + * standard AIDL. + */ +object ParcelUtils { + + @get:SuppressLint("SoonBlockedPrivateApi") + private val obtainMethod: Method by lazy { + Parcel::class.java.getDeclaredMethod("obtain", Long::class.java).apply { + isAccessible = true + } + } + + /** + * Reconstructs a Java [Parcel] object from a native C++ parcel pointer. Required for manual + * Binder transaction interception in [BridgeService]. + * + * @param ptr The native pointer address (long). + * @return A Java Parcel instance wrapping the native pointer, or null if pointer is 0. + */ + @JvmStatic + fun fromNativePointer(ptr: Long): Parcel? { + if (ptr == 0L) return null + return try { + obtainMethod.invoke(null, ptr) as? Parcel + } catch (e: Throwable) { + throw RuntimeException("Failed to obtain Parcel from native pointer: $ptr", e) + } + } +} + +/** Extension to allow [Long] native pointers to be treated as Parcels. */ +fun Long.asParcel(): Parcel? = ParcelUtils.fromNativePointer(this) From 9452cf455a7171598ec2ed2418959341b69ae93e Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 15 Feb 2026 11:33:51 +0100 Subject: [PATCH 07/24] Keep lspd directory Better to change it after database is redesigned --- .../main/java/org/lsposed/lspd/service/ConfigFileManager.java | 2 +- zygisk/module/customize.sh | 2 +- zygisk/module/uninstall.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java index 4caa1aaac..1f4d08e09 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java @@ -79,7 +79,7 @@ import hidden.HiddenApiBridge; public class ConfigFileManager { - static final Path basePath = Paths.get("/data/adb/vector"); + static final Path basePath = Paths.get("/data/adb/lspd"); static final Path modulePath = basePath.resolve("modules"); static final Path daemonApkPath = Paths.get(System.getProperty("java.class.path", null)); static final Path managerApkPath = daemonApkPath.getParent().resolve("manager.apk"); diff --git a/zygisk/module/customize.sh b/zygisk/module/customize.sh index 935ecf6c9..1fdd3e50a 100644 --- a/zygisk/module/customize.sh +++ b/zygisk/module/customize.sh @@ -77,7 +77,7 @@ extract "$ZIPFILE" 'sepolicy.rule' "$MODPATH" extract "$ZIPFILE" 'framework/lspd.dex' "$MODPATH" extract "$ZIPFILE" 'daemon.apk' "$MODPATH" extract "$ZIPFILE" 'daemon' "$MODPATH" -rm -f /data/adb/vector/manager.apk +rm -f /data/adb/lspd/manager.apk extract "$ZIPFILE" 'manager.apk' "$MODPATH" mkdir -p "$MODPATH/zygisk" diff --git a/zygisk/module/uninstall.sh b/zygisk/module/uninstall.sh index 2b4ad0d6c..2b72940bd 100644 --- a/zygisk/module/uninstall.sh +++ b/zygisk/module/uninstall.sh @@ -1 +1 @@ -rm -rf /data/adb/vector +rm -rf /data/adb/lspd From c3082824039eadfb0b233584c4f146a50ada439b Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 15 Feb 2026 20:03:48 +0100 Subject: [PATCH 08/24] Apply kotlin formatting --- build.gradle.kts | 29 ++++++-------- settings.gradle.kts | 1 + xposed/build.gradle.kts | 6 +-- .../matrix/vector/nativebridge/HookBridge.kt | 38 +++++++++++-------- .../matrix/vector/nativebridge/NativeAPI.kt | 3 +- .../vector/nativebridge/ResourcesHook.kt | 14 ++++--- .../matrix/vector/ParasiticManagerHooker.kt | 25 +++++++----- .../vector/ParasiticManagerSystemHooker.kt | 2 +- .../kotlin/org/matrix/vector/core/Main.kt | 8 ++-- .../matrix/vector/service/BridgeService.kt | 4 +- 10 files changed, 67 insertions(+), 63 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 96f84bb29..841dc99c9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -64,7 +64,6 @@ abstract class GitLatestTagValueSource : ValueSource("format") { source = project.fileTree(rootDir) include("*.gradle.kts", "*/build.gradle.kts") dependsOn(":xposed:ktfmtFormat") + dependsOn(":zygisk:ktfmtFormat") } ktfmt { kotlinLangStyle() } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9d3f89ac8..0b57d6a2d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,7 @@ dependencyResolutionManagement { } rootProject.name = "LSPosed" + include( ":app", ":core", diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index 346c3fd25..29b5ffc69 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -11,11 +11,7 @@ android { buildFeatures { androidResources { enable = false } } - sourceSets { - named("main") { - java.srcDirs("src/main/kotlin", "libxposed/api/src/main/java") - } - } + sourceSets { named("main") { java.srcDirs("src/main/kotlin", "libxposed/api/src/main/java") } } } dependencies { diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt index 632b532e6..527fb8307 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt @@ -2,8 +2,8 @@ package org.matrix.vector.nativebridge import dalvik.annotation.optimization.FastNative import java.lang.reflect.Executable -import java.lang.reflect.Method import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method object HookBridge { @JvmStatic @@ -12,44 +12,50 @@ object HookBridge { hookMethod: Executable, hooker: Class<*>, priority: Int, - callback: Any? + callback: Any?, ): Boolean @JvmStatic - external fun unhookMethod(useModernApi: Boolean, hookMethod: Executable, callback: Any?): Boolean + external fun unhookMethod( + useModernApi: Boolean, + hookMethod: Executable, + callback: Any?, + ): Boolean - @JvmStatic - external fun deoptimizeMethod(method: Executable): Boolean + @JvmStatic external fun deoptimizeMethod(method: Executable): Boolean @JvmStatic @Throws(InstantiationException::class) external fun allocateObject(clazz: Class): T @JvmStatic - @Throws(IllegalAccessException::class, IllegalArgumentException::class, InvocationTargetException::class) + @Throws( + IllegalAccessException::class, + IllegalArgumentException::class, + InvocationTargetException::class, + ) external fun invokeOriginalMethod(method: Executable, thisObject: Any?, vararg args: Any?): Any? @JvmStatic - @Throws(IllegalAccessException::class, IllegalArgumentException::class, InvocationTargetException::class) + @Throws( + IllegalAccessException::class, + IllegalArgumentException::class, + InvocationTargetException::class, + ) external fun invokeSpecialMethod( method: Executable, shorty: CharArray, clazz: Class, thisObject: Any?, - vararg args: Any? + vararg args: Any?, ): Any? - @JvmStatic - @FastNative - external fun instanceOf(obj: Any?, clazz: Class<*>): Boolean + @JvmStatic @FastNative external fun instanceOf(obj: Any?, clazz: Class<*>): Boolean - @JvmStatic - @FastNative - external fun setTrusted(cookie: Any?): Boolean + @JvmStatic @FastNative external fun setTrusted(cookie: Any?): Boolean @JvmStatic external fun callbackSnapshot(hooker_callback: Class<*>, method: Executable): Array> - @JvmStatic - external fun getStaticInitializer(clazz: Class<*>): Method + @JvmStatic external fun getStaticInitializer(clazz: Class<*>): Method } diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt index 114ae0ad9..b2fb29201 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt @@ -1,6 +1,5 @@ package org.matrix.vector.nativebridge object NativeAPI { - @JvmStatic - external fun recordNativeEntrypoint(library_name: String) + @JvmStatic external fun recordNativeEntrypoint(library_name: String) } diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt index b41ee398d..42955ed72 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt @@ -5,20 +5,22 @@ import dalvik.annotation.optimization.FastNative import xposed.dummy.XResourcesSuperClass object ResourcesHook { - @JvmStatic - external fun initXResourcesNative(): Boolean + @JvmStatic external fun initXResourcesNative(): Boolean - @JvmStatic - external fun makeInheritable(clazz: Class<*>): Boolean + @JvmStatic external fun makeInheritable(clazz: Class<*>): Boolean @JvmStatic external fun buildDummyClassLoader( parent: ClassLoader, resourceSuperClass: String, - typedArraySuperClass: String + typedArraySuperClass: String, ): ClassLoader @JvmStatic @FastNative - external fun rewriteXmlReferencesNative(parserPtr: Long, origRes: XResourcesSuperClass, repRes: Resources) + external fun rewriteXmlReferencesNative( + parserPtr: Long, + origRes: XResourcesSuperClass, + repRes: Resources, + ) } diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt index 609b1a62d..8c7240115 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -23,9 +23,9 @@ import java.io.FileOutputStream import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap import org.lsposed.lspd.ILSPManagerService +import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient import org.lsposed.lspd.util.Hookers import org.lsposed.lspd.util.Utils -import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient /** The "Parasite" logic. Injects the LSPosed Manager APK into a host process (shell). */ @SuppressLint("StaticFieldLeak") @@ -47,7 +47,8 @@ object ParasiticManagerHooker { val ctx: Context = ActivityThread.currentActivityThread().systemContext var sourcePath = "/proc/self/fd/$managerFd" - // SDK <= 28 (Android 9) cannot reliably parse APKs via FD paths in all contexts. + // SDK <= 28 (Android 9) cannot reliably parse APKs via FD paths in all + // contexts. // We copy the APK to the host's cache as a workaround. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { val dstPath = "${appInfo.dataDir}/cache/lsposed.apk" @@ -173,7 +174,12 @@ object ParasiticManagerHooker { } } } - classLoaderUnhook = XposedHelpers.findAndHookMethod(LoadedApk::class.java, "getClassLoader", classLoaderHook) + classLoaderUnhook = + XposedHelpers.findAndHookMethod( + LoadedApk::class.java, + "getClassLoader", + classLoaderHook, + ) // Hook 3: Activity Lifecycle & Intent Redirection val activityClientRecordClass = @@ -188,7 +194,8 @@ object ParasiticManagerHooker { if (arg is ActivityInfo) { val pkgInfo = getManagerPkgInfo(arg.applicationInfo) ?: return@forEachIndexed - pkgInfo.activities?.find { it.name == "org.lsposed.manager.ui.activity.MainActivity" } + pkgInfo.activities + ?.find { it.name == "org.lsposed.manager.ui.activity.MainActivity" } ?.let { it.applicationInfo = pkgInfo.applicationInfo param.args[i] = it @@ -421,11 +428,11 @@ object ParasiticManagerHooker { val binderList = mutableListOf() return try { serviceClient.requestInjectedManagerBinder(binderList).use { pfd -> - managerFd = pfd.detachFd() - val managerService = ILSPManagerService.Stub.asInterface(binderList[0]) - hookForManager(managerService) - Utils.logD("Vector manager injected successfully into process.") - true + managerFd = pfd.detachFd() + val managerService = ILSPManagerService.Stub.asInterface(binderList[0]) + hookForManager(managerService) + Utils.logD("Vector manager injected successfully into process.") + true } } catch (e: Throwable) { Utils.logE("Parasitic injection failed", e) diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt index 1be3529c3..32a033bc1 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt @@ -8,8 +8,8 @@ import android.content.pm.ResolveInfo import io.github.libxposed.api.XposedInterface import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker import org.lsposed.lspd.impl.LSPosedHelper -import org.matrix.vector.service.BridgeService import org.lsposed.lspd.util.Utils +import org.matrix.vector.service.BridgeService /** * Handles System-Server side logic for the Parasitic Manager. diff --git a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt index 84c7a6088..adb7de65a 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt @@ -3,9 +3,9 @@ package org.matrix.vector.core import android.os.IBinder import android.os.Process import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient -import org.lsposed.lspd.util.Utils -import org.lsposed.lspd.service.ILSPApplicationService import org.lsposed.lspd.core.Startup +import org.lsposed.lspd.service.ILSPApplicationService +import org.lsposed.lspd.util.Utils import org.matrix.vector.ParasiticManagerHooker import org.matrix.vector.ParasiticManagerSystemHooker @@ -41,9 +41,7 @@ object Main { // Step 4: Check if this process is the designated LSPosed Manager. // If so, we perform "parasitic" injection into a host (com.android.shell) // and terminate further standard Xposed loading for this specific process. - if ( - niceName == "org.lsposed.manager" && ParasiticManagerHooker.start() - ) { + if (niceName == "org.lsposed.manager" && ParasiticManagerHooker.start()) { Utils.logI("Parasitic manager loaded into host, skipping standard bootstrap.") return } diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt index c8b60ede0..454704ecb 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt @@ -8,8 +8,8 @@ import android.os.Parcel import android.os.Process import hidden.HiddenApiBridge.Binder_allowBlocking import hidden.HiddenApiBridge.Context_getActivityToken -import org.lsposed.lspd.util.Utils.Log import org.lsposed.lspd.service.ILSPosedService +import org.lsposed.lspd.util.Utils.Log /** * Manages manual Binder transactions for the Vector framework. @@ -94,7 +94,7 @@ object BridgeService { @JvmStatic fun onTransact(data: Parcel, reply: Parcel?, flags: Int): Boolean { return try { - data.enforceInterface(DESCRIPTOR) + data.enforceInterface(DESCRIPTOR) val actionIdx = data.readInt() val action = Action.values().getOrElse(actionIdx) { Action.UNKNOWN } From d7cab09dc757c9cf3bbbb15762bb113a3187488f Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 15 Feb 2026 21:21:50 +0100 Subject: [PATCH 09/24] Improve docs --- zygisk/src/main/cpp/include/ipc_bridge.h | 23 +++---- zygisk/src/main/cpp/include/zygisk.hpp | 57 +++++++--------- zygisk/src/main/cpp/ipc_bridge.cpp | 68 +++++++++---------- zygisk/src/main/cpp/module.cpp | 64 ++++++++--------- .../kotlin/org/matrix/vector/core/Main.kt | 13 ++-- .../matrix/vector/service/BridgeService.kt | 2 +- .../org/matrix/vector/service/ParcelUtils.kt | 1 - 7 files changed, 104 insertions(+), 124 deletions(-) diff --git a/zygisk/src/main/cpp/include/ipc_bridge.h b/zygisk/src/main/cpp/include/ipc_bridge.h index 753225060..2a1d0a4eb 100644 --- a/zygisk/src/main/cpp/include/ipc_bridge.h +++ b/zygisk/src/main/cpp/include/ipc_bridge.h @@ -4,7 +4,6 @@ #include #include -#include #include // This module is a client of the 'native' library. @@ -17,8 +16,7 @@ namespace vector::native::module { * @class IPCBridge * @brief Manages Binder IPC communication with the Vector host service. * - * This singleton class is the communication arm of the Zygisk module. Its key - * responsibilities are: + * This singleton class is the communication arm of the Zygisk module. Its key responsibilities are: * 1. Discovering and connecting to the central host service (the "manager"). * 2. Requesting the framework's DEX file and obfuscation map from the service. * 3. Caching all necessary JNI class and method IDs for efficient reuse. @@ -58,10 +56,8 @@ class IPCBridge { /** * @brief Asks the system_server binder for the application manager binder. * @param env JNI environment pointer. - * @param system_server_binder The binder connected to the system server - * service. - * @return A ScopedLocalRef to the application manager Binder, or nullptr on - * failure. + * @param system_server_binder The binder connected to the system server service. + * @return A ScopedLocalRef to the application manager Binder, or nullptr on failure. */ lsplant::ScopedLocalRef RequestManagerBinderFromSystemServer( JNIEnv *env, jobject system_server_binder); @@ -86,10 +82,10 @@ class IPCBridge { /** * @brief Sets up the JNI hook to intercept Binder transactions. * - * This is the core of the IPC interception mechanism. It replaces the JNI - * function pointer for CallBooleanMethodV to inspect calls to - * Binder.execTransact, allowing the framework to handle its own custom - * transaction codes directly. + * This is the core of the IPC interception mechanism. + * It replaces the JNI function pointer for CallBooleanMethodV to + * inspect calls to Binder.execTransact, allowing the framework to + * handle its own custom transaction codes directly. * @param env JNI environment pointer. */ void HookBridge(JNIEnv *env); @@ -99,9 +95,8 @@ class IPCBridge { * @class ParcelWrapper * @brief A private RAII wrapper to ensure Parcel objects are always recycled. * - * As a private nested class, it has access to the private members of - * IPCBridge (like parcel_class_ and recycle_method_) without needing a - * 'friend' declaration. This is a clean, encapsulated implementation detail. + * As a private nested class, it has access to the private members of IPCBridge + * (like parcel_class_ and recycle_method_). */ class ParcelWrapper { public: diff --git a/zygisk/src/main/cpp/include/zygisk.hpp b/zygisk/src/main/cpp/include/zygisk.hpp index 6bd6c6f6f..ab764954c 100644 --- a/zygisk/src/main/cpp/include/zygisk.hpp +++ b/zygisk/src/main/cpp/include/zygisk.hpp @@ -104,7 +104,6 @@ struct ServerSpecializeArgs; class ModuleBase { public: - // This method is called as soon as the module is loaded into the target process. // A Zygisk API handle will be passed as an argument. virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {} @@ -175,8 +174,9 @@ struct ServerSpecializeArgs { namespace internal { struct api_table; -template void entry_impl(api_table *, JNIEnv *); -} +template +void entry_impl(api_table *, JNIEnv *); +} // namespace internal // These values are used in Api::setOption(Option) enum Option : int { @@ -207,7 +207,6 @@ enum StateFlag : uint32_t { // All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded // from the specialized process afterwards. struct Api { - // Connect to a root companion process and get a Unix domain socket for IPC. // // This API only works in the pre[XXX]Specialize methods due to SELinux restrictions. @@ -258,7 +257,8 @@ struct Api { // The original function pointer will be saved in each JNINativeMethod's fnPtr. // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr // will be set to nullptr. - void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); + void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, + int numMethods); // Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory. // @@ -279,15 +279,16 @@ struct Api { private: internal::api_table *tbl; - template friend void internal::entry_impl(internal::api_table *, JNIEnv *); + template + friend void internal::entry_impl(internal::api_table *, JNIEnv *); }; // Register a class as a Zygisk module -#define REGISTER_ZYGISK_MODULE(clazz) \ -void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ - zygisk::internal::entry_impl(table, env); \ -} +#define REGISTER_ZYGISK_MODULE(clazz) \ + void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ + zygisk::internal::entry_impl(table, env); \ + } // Register a root companion request handler function for your module // @@ -299,8 +300,8 @@ void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ // NOTE: the function can run concurrently on multiple threads. // Be aware of race conditions if you have globally shared resources. -#define REGISTER_ZYGISK_COMPANION(func) \ -void zygisk_companion_entry(int client) { func(client); } +#define REGISTER_ZYGISK_COMPANION(func) \ + void zygisk_companion_entry(int client) { func(client); } /********************************************************* * The following is internal ABI implementation detail. @@ -335,9 +336,9 @@ struct api_table { void (*pltHookRegister)(dev_t, ino_t, const char *, void *, void **); bool (*exemptFd)(int); bool (*pltHookCommit)(); - int (*connectCompanion)(void * /* impl */); + int (*connectCompanion)(void * /* impl */); void (*setOption)(void * /* impl */, Option); - int (*getModuleDir)(void * /* impl */); + int (*getModuleDir)(void * /* impl */); uint32_t (*getFlags)(void * /* impl */); }; @@ -352,34 +353,28 @@ void entry_impl(api_table *table, JNIEnv *env) { m->onLoad(&api, env); } -} // namespace internal +} // namespace internal inline int Api::connectCompanion() { return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1; } -inline int Api::getModuleDir() { - return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1; -} +inline int Api::getModuleDir() { return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1; } inline void Api::setOption(Option opt) { if (tbl->setOption) tbl->setOption(tbl->impl, opt); } -inline uint32_t Api::getFlags() { - return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0; -} -inline bool Api::exemptFd(int fd) { - return tbl->exemptFd != nullptr && tbl->exemptFd(fd); -} -inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { +inline uint32_t Api::getFlags() { return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0; } +inline bool Api::exemptFd(int fd) { return tbl->exemptFd != nullptr && tbl->exemptFd(fd); } +inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, + int numMethods) { if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods); } -inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc) { +inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, + void **oldFunc) { if (tbl->pltHookRegister) tbl->pltHookRegister(dev, inode, symbol, newFunc, oldFunc); } -inline bool Api::pltHookCommit() { - return tbl->pltHookCommit != nullptr && tbl->pltHookCommit(); -} +inline bool Api::pltHookCommit() { return tbl->pltHookCommit != nullptr && tbl->pltHookCommit(); } -} // namespace zygisk +} // namespace zygisk extern "C" { @@ -389,4 +384,4 @@ void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); [[gnu::visibility("default"), maybe_unused]] void zygisk_companion_entry(int); -} // extern "C" +} // extern "C" diff --git a/zygisk/src/main/cpp/ipc_bridge.cpp b/zygisk/src/main/cpp/ipc_bridge.cpp index b2168f3b3..8da52df55 100644 --- a/zygisk/src/main/cpp/ipc_bridge.cpp +++ b/zygisk/src/main/cpp/ipc_bridge.cpp @@ -14,8 +14,8 @@ using namespace std::literals::string_view_literals; namespace vector::native::module { -// A file-level static atomic variable to store the ID of the last caller -// whose framework transaction failed. It's initialized to a value that won't match. +// Store the ID of the last caller whose framework transaction failed. +// It's initialized to a value that won't match. static std::atomic g_last_failed_id = ~0; /** @@ -49,7 +49,7 @@ class BinderCaller { if (!s_self_or_null_fn || !s_get_calling_pid_fn || !s_get_calling_uid_fn) { LOGW("Could not resolve all IPCThreadState symbols. Caller ID check will be disabled."); } else { - LOGI("IPCThreadState symbols resolved successfully."); + LOGV("IPCThreadState symbols resolved successfully."); } } @@ -79,14 +79,12 @@ class BinderCaller { }; // --- Binder IPC Protocol Constants --- -// These are the "secret handshakes" used to communicate with the Vector manager -// service. +// These are the "secret handshakes" used to communicate with the Vector manager service. // The service descriptor that the remote Binder service expects. constexpr auto kBridgeServiceDescriptor = "LSPosed"sv; -// The name of the system service we use as a rendezvous point to find our -// manager service. Using "activity" is a common technique as it's always -// available. +// The name of the system service we use as a rendezvous point to find our manager service. +// Using "activity" is a common technique as it's always available. constexpr auto kBridgeServiceName = "activity"sv; // A different rendezvous point used only by the system_server. constexpr auto kSystemServerBridgeServiceName = "serial"sv; @@ -104,11 +102,9 @@ constexpr jint kActionGetBinder = 2; // ========================================================================================= /** - * @brief Constructs the ParcelWrapper, obtaining two new Parcel objects from - * the pool. + * @brief Constructs the ParcelWrapper, obtaining two new Parcel objects from the pool. * @param env A valid JNI environment pointer. - * @param bridge A pointer to the parent IPCBridge instance to access its cached - * JNI IDs. + * @param bridge A pointer to the parent IPCBridge instance to access its cached JNI IDs. */ IPCBridge::ParcelWrapper::ParcelWrapper(JNIEnv *env, IPCBridge *bridge) : data(lsplant::JNI_CallStaticObjectMethod(env, bridge->parcel_class_, bridge->obtain_method_)), @@ -118,12 +114,11 @@ IPCBridge::ParcelWrapper::ParcelWrapper(JNIEnv *env, IPCBridge *bridge) bridge_(bridge) {} /** - * @brief Destructs the ParcelWrapper, ensuring both Parcel objects are - * recycled. + * @brief Destructs the ParcelWrapper, ensuring both Parcel objects are recycled. * - * This is the core of the RAII pattern for Parcels. This destructor guarantees - * that recycle() is called, preventing resource leaks even if errors occur - * during the transaction. + * This is the core of the RAII pattern for Parcels. + * This destructor guarantees that recycle() is called, preventing resource leaks even if + * errors occur during the transaction. */ IPCBridge::ParcelWrapper::~ParcelWrapper() { // Check if the parcel was successfully obtained before trying to recycle it. @@ -150,8 +145,8 @@ void IPCBridge::Initialize(JNIEnv *env) { } // --- Cache JNI Classes and Method IDs --- - // Caching these at startup is more efficient and robust than looking them - // up on every IPC call. If any of these fail, the IPC bridge is unusable. + // Caching these at startup is more efficient and robust than looking them up on every IPC call. + // If any of these fail, the IPC bridge is unusable. // ServiceManager auto sm_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/ServiceManager")); @@ -234,7 +229,7 @@ void IPCBridge::Initialize(JNIEnv *env) { return; } - LOGI("IPCBridge initialized successfully."); + LOGV("IPCBridge initialized successfully."); initialized_ = true; } @@ -244,26 +239,26 @@ lsplant::ScopedLocalRef IPCBridge::RequestAppBinder(JNIEnv *env, jstrin return {env, nullptr}; } - // Step 1: Get the rendezvous service from the Android ServiceManager. + // Get the rendezvous service from the Android ServiceManager. auto service_name = lsplant::ScopedLocalRef(env, env->NewStringUTF(kBridgeServiceName.data())); auto bridge_service = lsplant::JNI_CallStaticObjectMethod( env, service_manager_class_, get_service_method_, service_name.get()); if (!bridge_service) { - LOGD("Could not get rendezvous service '{}'. Manager not available?", + LOGE("Could not get rendezvous service '{}'. Manager not available?", kBridgeServiceName.data()); return {env, nullptr}; } - // Step 2: Prepare the IPC transaction. + // Prepare the IPC transaction. ParcelWrapper parcels(env, this); if (!parcels.data || !parcels.reply) { LOGE("Failed to obtain parcels for IPC."); return {env, nullptr}; } - // This is a "heartbeat" binder. If our process dies, the manager service - // will be notified that this binder has died, allowing it to clean up - // resources. + // This is a "heartbeat" binder. + // If our process dies, the manager service will be notified that this binder has died, + // allowing it to clean up resources. auto heartbeat_binder = lsplant::ScopedLocalRef(env, env->NewObject(binder_class_, binder_ctor_)); if (!heartbeat_binder) { @@ -271,7 +266,7 @@ lsplant::ScopedLocalRef IPCBridge::RequestAppBinder(JNIEnv *env, jstrin return {env, nullptr}; } - // Step 3: Write the request data to the 'data' parcel. + // Write the request data to the 'data' parcel. auto descriptor = lsplant::ScopedLocalRef(env, env->NewStringUTF(kBridgeServiceDescriptor.data())); lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_interface_token_method_, @@ -281,14 +276,14 @@ lsplant::ScopedLocalRef IPCBridge::RequestAppBinder(JNIEnv *env, jstrin lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, heartbeat_binder.get()); - // Step 4: Perform the transaction. + // Perform the transaction. bool success = lsplant::JNI_CallBooleanMethod(env, bridge_service.get(), transact_method_, kBridgeTransactionCode, parcels.data.get(), parcels.reply.get(), 0); lsplant::ScopedLocalRef result_binder = {env, nullptr}; if (success) { - // Step 5: Read the reply. CRITICAL: must call readException first. + // Read the reply. CRITICAL: must call readException first. lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); if (env->ExceptionCheck()) { LOGW("Remote exception received while requesting app binder."); @@ -320,8 +315,8 @@ lsplant::ScopedLocalRef IPCBridge::RequestSystemServerBinder(JNIEnv *en lsplant::ScopedLocalRef(env, env->NewStringUTF(kSystemServerBridgeServiceName.data())); lsplant::ScopedLocalRef binder = {env, nullptr}; - // The system_server might start its services slightly after Zygisk injects - // us. We retry a few times to give it a chance to register. + // The system_server might start its services slightly after Zygisk injects us. + // We retry a few times to give it a chance to register. for (int i = 0; i < 3; ++i) { binder = lsplant::JNI_CallStaticObjectMethod(env, service_manager_class_, get_service_method_, service_name.get()); @@ -409,7 +404,7 @@ std::tuple IPCBridge::FetchFrameworkDex(JNIEnv *env, jobject binder size_t size = static_cast( lsplant::JNI_CallLongMethod(env, parcels.reply.get(), read_long_method_)); - LOGI("Fetched framework DEX: fd={}, size={}", fd, size); + LOGV("Fetched framework DEX: fd={}, size={}", fd, size); return {fd, size}; } @@ -461,7 +456,7 @@ std::map IPCBridge::FetchObfuscationMap(JNIEnv *env, j result_map[key_str.get()] = val_str.get(); } - LOGI("Fetched obfuscation map with {} entries.", result_map.size()); + LOGV("Fetched obfuscation map with {} entries.", result_map.size()); return result_map; } @@ -501,7 +496,8 @@ jboolean JNICALL IPCBridge::CallBooleanMethodV_Hook(JNIEnv *env, jobject obj, jm uint64_t current_caller_id = BinderCaller::GetId(); if (current_caller_id != 0) { uint64_t last_failed = g_last_failed_id.load(std::memory_order_relaxed); - // If this caller is the one that just failed, skip interception and go straight to the original function. + // If this caller is the one that just failed, + // skip interception and go straight to the original function. if (current_caller_id == last_failed) { // We "consume" the failed state by resetting it, so the *next* call is not skipped. g_last_failed_id.store(~0, std::memory_order_relaxed); @@ -528,7 +524,7 @@ void IPCBridge::HookBridge(JNIEnv *env) { return; } - // --- Step 1: Get framework-specific Java classes and methods --- + // Get framework-specific Java classes and methods --- const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); std::string bridge_service_class_name; bridge_service_class_name = obfs_map.at("org.matrix.vector.service.") + "BridgeService"; @@ -548,7 +544,7 @@ void IPCBridge::HookBridge(JNIEnv *env) { return; } - // --- Step 2: Prepare the JNI hook --- + // --- Prepare the JNI hook --- // Get the original method ID for android.os.Binder.execTransact exec_transact_backup_method_id_ = lsplant::JNI_GetMethodID(env, binder_class_, "execTransact", "(IJJI)Z"); diff --git a/zygisk/src/main/cpp/module.cpp b/zygisk/src/main/cpp/module.cpp index 7e7388a61..43f5c4a96 100644 --- a/zygisk/src/main/cpp/module.cpp +++ b/zygisk/src/main/cpp/module.cpp @@ -31,11 +31,11 @@ constexpr int LAST_APP_ZYGOTE_ISOLATED_UID = 98999; // UID for the process responsible for creating shared RELRO files. constexpr int SHARED_RELRO_UID = 1037; -// Android uses this to separate users. UID = AppID + UserID * PER_USER_RANGE. +// Android uses this to separate users. UID = AppID + UserID * 10000. constexpr int PER_USER_RANGE = 100000; -constexpr uid_t MANAGER_UID = INJECTED_UID; -constexpr uid_t GID_INET = 3003; // Android's Internet group ID. +constexpr uid_t MANAGER_UID = 2000; // com.android.shell +constexpr uid_t GID_INET = 3003; // Android's Internet group ID. // A simply ConfigBridge implemnetation holding obfuscation maps in memory using obfuscation_map_t = std::map; @@ -51,7 +51,7 @@ class ConfigImpl : public ConfigBridge { ConfigImpl() = default; friend std::unique_ptr std::make_unique(); - inline static std::map obfuscation_map_; + obfuscation_map_t obfuscation_map_; }; /** @@ -59,9 +59,9 @@ class ConfigImpl : public ConfigBridge { * @brief The core implementation of the Zygisk module for the Vector framework. * * This class is the main entry point for Zygisk. It inherits from: - * - zygisk::ModuleBase: To receive lifecycle callbacks from the Zygisk loader. - * - vector::native::Context: To gain the core injection capabilities (DEX - * loading, ART hooking) from the 'native' library. + * - zygisk::ModuleBase: To receive lifecycle callbacks from the Zygisk loader. + * - vector::native::Context: To gain the core injection capabilities (DEX loading, ART hooking) + * from the 'native' library. * * It orchestrates the injection process by deciding which processes to target, * using the IPCBridge to fetch the framework from the manager service, and then @@ -80,8 +80,8 @@ class VectorModule : public zygisk::ModuleBase, public vector::native::Context { * @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. + * must be implemented here. + * It uses an InMemoryDexClassLoader to load our framework into the target process. */ void LoadDex(JNIEnv *env, PreloadedDex &&dex) override; @@ -89,16 +89,15 @@ class VectorModule : public zygisk::ModuleBase, public vector::native::Context { * @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. + * 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. + * @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. @@ -133,10 +132,9 @@ class VectorModule : public zygisk::ModuleBase, public vector::native::Context { // ========================================================================================= void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { - LOGD("Loading framework DEX into memory (size: {}).", dex.size()); + LOGV("Loading framework DEX into memory (size: {}).", dex.size()); - // Step 1: Get the system ClassLoader. This will be the parent of our new - // loader. + // 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"); @@ -151,7 +149,7 @@ void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { return; } - // Step 2: Create a Java ByteBuffer wrapping our in-memory DEX data. + // 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"); @@ -164,7 +162,7 @@ void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { return; } - // Step 3: Create an InMemoryDexClassLoader instance. + // 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."); @@ -188,7 +186,7 @@ void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { // Store a global reference to our new ClassLoader. inject_class_loader_ = env->NewGlobalRef(new_cl.get()); - LOGI("Framework ClassLoader created successfully."); + LOGV("Framework ClassLoader created successfully."); } void VectorModule::SetupEntryClass(JNIEnv *env) { @@ -200,7 +198,6 @@ void VectorModule::SetupEntryClass(JNIEnv *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; - // Assume the native library provides a helper or direct map access. entry_class_name = obfs_map.at("org.matrix.vector.core.") + "Main"; // We must find the class through our custom ClassLoader. @@ -212,7 +209,7 @@ void VectorModule::SetupEntryClass(JNIEnv *env) { // Store a global reference to the entry class. entry_class_ = lsplant::JNI_NewGlobalRef(env, entry_class); - LOGI("Framework entry class '{}' located.", entry_class_name.c_str()); + LOGV("Framework entry class '{}' located.", entry_class_name.c_str()); } void VectorModule::onLoad(zygisk::Api *api, JNIEnv *env) { @@ -231,8 +228,8 @@ void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { 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. + // We identify our manager app by a special UID and + // grant it internet permissions by adding it to the INET group. if (args->uid == MANAGER_UID) { lsplant::JUTFString nice_name_str(env_, args->nice_name); if (nice_name_str.get() == "org.lsposed.manager"sv) { @@ -272,8 +269,7 @@ void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { return; } - // Child Zygotes are specialized zygotes for apps like WebView and are not - // targets. + // 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; @@ -284,14 +280,14 @@ void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { 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) { - LOGI("Skipping injection for '{}': is an isolated process (UID: %d).", nice_name_str.get(), + LOGV("Skipping injection for '{}': is an isolated process (UID: %d).", nice_name_str.get(), app_id); return; } // If we passed all checks, mark this process for injection. should_inject_ = true; - LOGI("Process '{}' (UID: {}) is marked for injection.", nice_name_str.get(), args->uid); + LOGV("Process '{}' (UID: {}) is marked for injection.", nice_name_str.get(), args->uid); } void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { @@ -311,7 +307,7 @@ void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { auto &ipc_bridge = IPCBridge::GetInstance(); auto binder = ipc_bridge.RequestAppBinder(env_, args->nice_name); if (!binder) { - LOGW("Failed to get IPC binder for '{}'. Skipping injection.", nice_name_str.get()); + LOGD("No IPC binder obtained for '{}'. Skipping injection.", nice_name_str.get()); SetAllowUnload(true); return; } @@ -345,7 +341,7 @@ void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { "(ZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_FALSE, args->nice_name, args->app_data_dir, binder.get(), is_manager_app_); - LOGI("Successfully injected Vector framework into '{}'.", nice_name_str.get()); + LOGV("Injected Vector framework into '{}'.", nice_name_str.get()); SetAllowUnload(false); // We are injected, PREVENT module unloading. } @@ -370,7 +366,7 @@ void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args // 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")) { - LOGI("Applying ZTE-specific workaround: setting argv[0] to system_server."); + 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 = @@ -398,8 +394,8 @@ void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args auto manager_binder = ipc_bridge.RequestManagerBinderFromSystemServer(env_, system_binder.get()); - // Use either the direct manager binder if available, otherwise proxy through - // the system binder. + // 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); @@ -429,7 +425,7 @@ void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args "(ZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_TRUE, system_name.get(), nullptr, manager_binder.get(), is_manager_app_); - LOGI("Successfully injected Vector framework into system_server."); + LOGI("Injected Vector framework into system_server."); SetAllowUnload(false); // We are injected, PREVENT module unloading. } diff --git a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt index adb7de65a..a2cf9ede3 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt @@ -10,8 +10,7 @@ import org.matrix.vector.ParasiticManagerHooker import org.matrix.vector.ParasiticManagerSystemHooker /** - * Main entry point for the Java-side loader. This class is invoked via JNI from the Vector Zygisk - * module. + * Main entry point for the Java-side loader, invoked via JNI from the Vector Zygisk module. */ object Main { @@ -25,20 +24,20 @@ object Main { */ @JvmStatic fun forkCommon(isSystem: Boolean, niceName: String, appDir: String?, binder: IBinder) { - // Step 1: Initialize system-specific resolution hooks if in system_server + // Initialize system-specific resolution hooks if in system_server if (isSystem) { ParasiticManagerSystemHooker.start() } - // Step 2: Initialize Xposed bridge components + // Initialize Xposed bridge components val appService = ILSPApplicationService.Stub.asInterface(binder) Startup.initXposed(isSystem, niceName, appDir, appService) - // Step 3: Configure logging levels from the service client + // Configure logging levels from the service client runCatching { Utils.Log.muted = serviceClient.isLogMuted } .onFailure { t -> Utils.logE("Failed to configure logs from service", t) } - // Step 4: Check if this process is the designated LSPosed Manager. + // Check if this process is the designated Vector Manager. // If so, we perform "parasitic" injection into a host (com.android.shell) // and terminate further standard Xposed loading for this specific process. if (niceName == "org.lsposed.manager" && ParasiticManagerHooker.start()) { @@ -46,7 +45,7 @@ object Main { return } - // Step 5: Standard Xposed module loading for third-party apps + // Standard Xposed module loading for third-party apps Utils.logI("Loading Vector/Xposed for $niceName (UID: ${Process.myUid()})") Startup.bootstrapXposed() } diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt index 454704ecb..965872ef7 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt @@ -21,7 +21,7 @@ object BridgeService { private const val TRANSACTION_CODE = ('_'.code shl 24) or ('L'.code shl 16) or ('S'.code shl 8) or 'P'.code private const val DESCRIPTOR = "LSPosed" - private const val TAG = "LSPosed-Bridge" + private const val TAG = "Vector-Bridge" /** Actions supported by the manual IPC bridge. */ private enum class Action { diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt index 077afc42c..0c96b3917 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt @@ -10,7 +10,6 @@ import java.lang.reflect.Method */ object ParcelUtils { - @get:SuppressLint("SoonBlockedPrivateApi") private val obtainMethod: Method by lazy { Parcel::class.java.getDeclaredMethod("obtain", Long::class.java).apply { isAccessible = true From e2e69fe8a64ab353f32dd713cd989e3ca15e856a Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 15 Feb 2026 23:34:35 +0100 Subject: [PATCH 10/24] Rename enforced data descriptor for parcels Java string created via NewStringUTF is detectable by searching the JVM heap. Renaming `LSPosed` to another string, is stupid but can defeat detectors such as https://github.com/RiRiRawrZ615/Known-Root-Detectors/blob/main/Risk%20Detector_1.6.apk Of course, there are plenty of robust solutions to overcome this detection point. This commit is just for fun, in the process of refactoring LSPosed into Vector. --- .../src/main/java/org/lsposed/lspd/service/BridgeService.java | 2 +- zygisk/src/main/cpp/ipc_bridge.cpp | 2 +- .../src/main/kotlin/org/matrix/vector/service/BridgeService.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java b/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java index a0af8c84f..2773939c1 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java @@ -20,7 +20,7 @@ public class BridgeService { static final int TRANSACTION_CODE = ('_' << 24) | ('L' << 16) | ('S' << 8) | 'P'; // 1598837584 - private static final String DESCRIPTOR = "LSPosed"; + private static final String DESCRIPTOR = "Vector"; private static final String SERVICE_NAME = "activity"; enum ACTION { diff --git a/zygisk/src/main/cpp/ipc_bridge.cpp b/zygisk/src/main/cpp/ipc_bridge.cpp index 8da52df55..ec55caf09 100644 --- a/zygisk/src/main/cpp/ipc_bridge.cpp +++ b/zygisk/src/main/cpp/ipc_bridge.cpp @@ -82,7 +82,7 @@ class BinderCaller { // These are the "secret handshakes" used to communicate with the Vector manager service. // The service descriptor that the remote Binder service expects. -constexpr auto kBridgeServiceDescriptor = "LSPosed"sv; +constexpr auto kBridgeServiceDescriptor = "Vector"sv; // The name of the system service we use as a rendezvous point to find our manager service. // Using "activity" is a common technique as it's always available. constexpr auto kBridgeServiceName = "activity"sv; diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt index 965872ef7..0fb0360bf 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt @@ -20,7 +20,7 @@ import org.lsposed.lspd.util.Utils.Log object BridgeService { private const val TRANSACTION_CODE = ('_'.code shl 24) or ('L'.code shl 16) or ('S'.code shl 8) or 'P'.code - private const val DESCRIPTOR = "LSPosed" + private const val DESCRIPTOR = "Vector" private const val TAG = "Vector-Bridge" /** Actions supported by the manual IPC bridge. */ From 0e1ff4c1c4a61cfc48c3c98dde09cfdb2ac180b6 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 4 Mar 2026 22:52:39 +0100 Subject: [PATCH 11/24] Update Magisk module template Add more comments and simplify logic --- zygisk/module/customize.sh | 234 +++++++++++++++----------------- zygisk/module/daemon | 36 +++-- zygisk/module/service.sh | 28 +--- zygisk/module/util_functions.sh | 63 --------- zygisk/module/verify.sh | 71 ---------- 5 files changed, 136 insertions(+), 296 deletions(-) delete mode 100644 zygisk/module/util_functions.sh delete mode 100644 zygisk/module/verify.sh diff --git a/zygisk/module/customize.sh b/zygisk/module/customize.sh index 1fdd3e50a..7a9451dc8 100644 --- a/zygisk/module/customize.sh +++ b/zygisk/module/customize.sh @@ -1,153 +1,141 @@ -# -# This file is part of LSPosed. -# -# LSPosed is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# LSPosed is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with LSPosed. If not, see . -# -# Copyright (C) 2020 EdXposed Contributors -# Copyright (C) 2021 LSPosed Contributors -# - -# shellcheck disable=SC2034 SKIPUNZIP=1 -enforce_install_from_magisk_app() { - if $BOOTMODE; then - ui_print "- Installing from Magisk app" - else - ui_print "*********************************************************" - ui_print "! Install from recovery is NOT supported" - ui_print "! Some recovery has broken implementations, please thus install from Magisk app" - abort "*********************************************************" - fi -} +# ========================================================= +# Utils functions to extract and verify installation package -VERSION=$(grep_prop version "${TMPDIR}/module.prop") -ui_print "- LSPosed version ${VERSION}" +TMPDIR_FOR_VERIFY="$TMPDIR/.vunzip" +mkdir "$TMPDIR_FOR_VERIFY" -# Extract verify.sh -ui_print "- Extracting verify.sh" -unzip -o "$ZIPFILE" 'verify.sh' -d "$TMPDIR" >&2 -if [ ! -f "$TMPDIR/verify.sh" ]; then +abort_verify() { ui_print "*********************************************************" - ui_print "! Unable to extract verify.sh!" + ui_print "! $1" ui_print "! This zip may be corrupted, please try downloading again" abort "*********************************************************" -fi -. "$TMPDIR/verify.sh" - -# Base check -extract "$ZIPFILE" 'customize.sh' "$TMPDIR" -extract "$ZIPFILE" 'verify.sh' "$TMPDIR" -extract "$ZIPFILE" 'util_functions.sh' "$TMPDIR" -. "$TMPDIR/util_functions.sh" -check_android_version -if [ -z "$KSU" ] && [ -z "$APATCH" ]; then - check_magisk_version -fi -check_incompatible_module +} -enforce_install_from_magisk_app +# Usage: extract [junk_paths: true|false] +extract() { + local zip="$1" + local file="$2" + local dir="$3" + local junk_paths="${4:-false}" # Defaults to false if not provided + local opts="-o" + local file_path hash_path file_basename + + file_basename=$(basename "$file") + + if [ "$junk_paths" = "true" ]; then + opts="-oj" + file_path="$dir/$file_basename" + hash_path="${TMPDIR_FOR_VERIFY}/$file_basename.sha256" + else + file_path="$dir/$file" + hash_path="${TMPDIR_FOR_VERIFY}/$file.sha256" + fi -# Check architecture -if [ "$ARCH" != "arm" ] && [ "$ARCH" != "arm64" ] && [ "$ARCH" != "x86" ] && [ "$ARCH" != "x64" ]; then - abort "! Unsupported platform: $ARCH" -else - ui_print "- Device platform: $ARCH" -fi + # Extract the file and its hash + unzip $opts "$zip" "$file" -d "$dir" >/dev/null 2>&1 + [ -f "$file_path" ] || abort_verify "Extracted $file does not exist" -# Extract libs -ui_print "- Extracting module files" - -extract "$ZIPFILE" 'module.prop' "$MODPATH" -extract "$ZIPFILE" 'action.sh' "$MODPATH" -extract "$ZIPFILE" 'service.sh' "$MODPATH" -extract "$ZIPFILE" 'uninstall.sh' "$MODPATH" -extract "$ZIPFILE" 'sepolicy.rule' "$MODPATH" -extract "$ZIPFILE" 'framework/lspd.dex' "$MODPATH" -extract "$ZIPFILE" 'daemon.apk' "$MODPATH" -extract "$ZIPFILE" 'daemon' "$MODPATH" -rm -f /data/adb/lspd/manager.apk -extract "$ZIPFILE" 'manager.apk' "$MODPATH" - - mkdir -p "$MODPATH/zygisk" - -if [ "$ARCH" = "arm" ] || [ "$ARCH" = "arm64" ]; then - extract "$ZIPFILE" "lib/armeabi-v7a/libzygisk.so" "$MODPATH/zygisk" true - mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/armeabi-v7a.so" - - if [ "$IS64BIT" = true ]; then - extract "$ZIPFILE" "lib/arm64-v8a/libzygisk.so" "$MODPATH/zygisk" true - mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/arm64-v8a.so" - fi -fi + unzip $opts "$zip" "$file.sha256" -d "${TMPDIR_FOR_VERIFY}" >/dev/null 2>&1 + [ -f "$hash_path" ] || abort_verify "Hash file $file.sha256 does not exist" -if [ "$ARCH" = "x86" ] || [ "$ARCH" = "x64" ]; then - extract "$ZIPFILE" "lib/x86/libzygisk.so" "$MODPATH/zygisk" true - mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/x86.so" + # Read the expected hash and verify it + local expected_hash + read -r expected_hash < "$hash_path" + expected_hash="${expected_hash%% *}" # Strip anything after the actual hash string - if [ "$IS64BIT" = true ]; then - extract "$ZIPFILE" "lib/x86_64/libzygisk.so" "$MODPATH/zygisk" true - mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/x86_64.so" + if ! echo "$expected_hash $file_path" | sha256sum -c -s -; then + abort_verify "Failed to verify $file" fi + + ui_print "- Verified $file" +} +# ========================================================= + +VERSION=$(grep_prop version "${TMPDIR}/module.prop") +ui_print "- Vector version ${VERSION}" + +# Disable existing LSPosed installation +LSPOSED_DIR="/data/adb/modules/zygisk_lsposed" +if [ -d "$LSPOSED_DIR" ]; then + ui_print "*********************************************************" + ui_print "LSPosed installation detected, disabling it for Vector" + touch "$LSPOSED_DIR/disable" + ui_print "*********************************************************" fi -if [ "$API" -ge 29 ]; then - ui_print "- Extracting dex2oat binaries" - mkdir "$MODPATH/bin" +# 1. Map architecture to standard ABI paths, eliminating duplicate logic +case "$ARCH" in + arm|arm64) + ABI32="armeabi-v7a" + ABI64="arm64-v8a" + ;; + x86|x64) + ABI32="x86" + ABI64="x86_64" + ;; + *) + abort "! Unsupported platform: $ARCH" + ;; +esac +ui_print "- Device platform: $ARCH ($ABI32 / $ABI64)" + +ui_print "- Extracting root module files" +for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framework/lspd.dex daemon.apk daemon manager.apk; do + extract "$ZIPFILE" "$file" "$MODPATH" +done + +ui_print "- Extracting Zygisk libraries" +mkdir -p "$MODPATH/zygisk" + +# Extract 32-bit lib +extract "$ZIPFILE" "lib/$ABI32/libzygisk.so" "$MODPATH/zygisk" true +mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/${ABI32}.so" + +# Extract 64-bit lib if supported +if [ "$IS64BIT" = true ]; then + extract "$ZIPFILE" "lib/$ABI64/libzygisk.so" "$MODPATH/zygisk" true + mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/${ABI64}.so" +fi - if [ "$ARCH" = "arm" ] || [ "$ARCH" = "arm64" ]; then - extract "$ZIPFILE" "bin/armeabi-v7a/dex2oat" "$MODPATH/bin" true - extract "$ZIPFILE" "bin/armeabi-v7a/liboat_hook.so" "$MODPATH/bin" true - mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat32" - mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook32.so" +if [ "$API" -ge 29 ]; then + ui_print "- Extracting dex2oat binaries" + mkdir -p "$MODPATH/bin" - if [ "$IS64BIT" = true ]; then - extract "$ZIPFILE" "bin/arm64-v8a/dex2oat" "$MODPATH/bin" true - extract "$ZIPFILE" "bin/arm64-v8a/liboat_hook.so" "$MODPATH/bin" true - mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat64" - mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook64.so" - fi - elif [ "$ARCH" == "x86" ] || [ "$ARCH" == "x64" ]; then - extract "$ZIPFILE" "bin/x86/dex2oat" "$MODPATH/bin" true - extract "$ZIPFILE" "bin/x86/liboat_hook.so" "$MODPATH/bin" true + # Extract 32-bit binaries + extract "$ZIPFILE" "bin/$ABI32/dex2oat" "$MODPATH/bin" true + extract "$ZIPFILE" "bin/$ABI32/liboat_hook.so" "$MODPATH/bin" true mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat32" mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook32.so" + # Extract 64-bit binaries if [ "$IS64BIT" = true ]; then - extract "$ZIPFILE" "bin/x86_64/dex2oat" "$MODPATH/bin" true - extract "$ZIPFILE" "bin/x86_64/liboat_hook.so" "$MODPATH/bin" true - mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat64" - mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook64.so" + extract "$ZIPFILE" "bin/$ABI64/dex2oat" "$MODPATH/bin" true + extract "$ZIPFILE" "bin/$ABI64/liboat_hook.so" "$MODPATH/bin" true + mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat64" + mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook64.so" fi - fi - ui_print "- Patching binaries" - DEV_PATH=$(tr -dc 'a-z0-9' < /dev/urandom | head -c 32) - sed -i "s/5291374ceda0aef7c5d86cd2a4f6a3ac/$DEV_PATH/g" "$MODPATH/daemon.apk" - sed -i "s/5291374ceda0aef7c5d86cd2a4f6a3ac/$DEV_PATH/" "$MODPATH/bin/dex2oat32" - sed -i "s/5291374ceda0aef7c5d86cd2a4f6a3ac/$DEV_PATH/" "$MODPATH/bin/dex2oat64" + ui_print "- Patching binaries for anti-detection" + DEV_PATH=$(tr -dc 'a-z0-9' > "$MODPATH/system.prop" +if [ "$(grep_prop ro.maple.enable)" = "1" ]; then + ui_print "- Add ro.maple.enable=0" + echo "ro.maple.enable=0" >>"$MODPATH/system.prop" fi -ui_print "- Welcome to LSPosed!" +ui_print "- Welcome to Vector!" diff --git a/zygisk/module/daemon b/zygisk/module/daemon index ec39cffde..514eac7c0 100644 --- a/zygisk/module/daemon +++ b/zygisk/module/daemon @@ -1,17 +1,20 @@ #!/system/bin/sh -dir=${0%/*} -tmpLspdApk="/data/local/tmp/daemon.apk" -debug=@DEBUG@ +dir="${0%/*}" +tmpDaemonApk="/data/local/tmp/daemon.apk" +debug="@DEBUG@" -if [ -r $tmpLspdApk ]; then - java_options="-Djava.class.path=$tmpLspdApk" +# Safely check for debug APK and set classpath +if [ -r "$tmpDaemonApk" ]; then + java_options="-Djava.class.path=$tmpDaemonApk" debug="true" else java_options="-Djava.class.path=$dir/daemon.apk" fi -if [ $debug = "true" ]; then +# Apply debug options based on Android SDK version. +# Reference: https://developer.android.com/ndk/guides/wrap-script#debugging_when_using_wrapsh +if [ "$debug" = "true" ]; then os_version=$(getprop ro.build.version.sdk) if [ "$os_version" -eq "27" ]; then java_options="$java_options -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable" @@ -22,19 +25,22 @@ if [ $debug = "true" ]; then fi fi +# Mount an empty temporary file system over /data/resource-cache to avoid possible overlay conflicts. +# Note that this script runs inside an isolated mount namespace (via unshare). mount tmpfs -t tmpfs /data/resource-cache +# Wait for Zygote socket using a polling loop if [ ! -S "/dev/socket/zygote" ]; then - timeout 0.5 inotifyd - /dev/socket:near | while read -r line; do - $debug && log -p v -t "LSPosed" "inotify: $line" - if [ -S "/dev/socket/zygote" ]; then - $debug && log -p v -t "LSPosed" "zygote started" - touch /dev/socket& - exit - fi + # Wait up to ~10 seconds to avoid an infinite hang if something breaks + wait_count=0 + while [ ! -S "/dev/socket/zygote" ] &&[ "$wait_count" -lt 100 ]; do + sleep 0.1 + wait_count=$((wait_count + 1)) done + [ "$debug" = "true" ] && log -p v -t "Vector" "zygote started" fi -$debug && log -p d -t "LSPosed" "start daemon $*" -# shellcheck disable=SC2086 +[ "$debug" = "true" ] && log -p d -t "Vector" "Starting daemon $*" + +# Launch the daemon exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.lsposed.lspd.Main "$@" >/dev/null 2>&1 diff --git a/zygisk/module/service.sh b/zygisk/module/service.sh index 87e6e9ed6..88300abab 100644 --- a/zygisk/module/service.sh +++ b/zygisk/module/service.sh @@ -1,25 +1,5 @@ -# -# This file is part of LSPosed. -# -# LSPosed is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# LSPosed is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with LSPosed. If not, see . -# -# Copyright (C) 2021 LSPosed Contributors -# +# Extract the directory path +MODDIR="${0%/*}" -MODDIR=${0%/*} - -cd "$MODDIR" - -# To avoid delaying the normal mount timing of zygote, we start LSPosed service daemon in late_start service mode instead of post-fs-data mode -unshare --propagation slave -m sh -c "$MODDIR/daemon --system-server-max-retry=3 $@&" +# Start the daemon directly in the background within a private mount namespace +unshare --propagation slave -m "$MODDIR/daemon" --system-server-max-retry=3 "$@" & diff --git a/zygisk/module/util_functions.sh b/zygisk/module/util_functions.sh deleted file mode 100644 index 4bb9ea87f..000000000 --- a/zygisk/module/util_functions.sh +++ /dev/null @@ -1,63 +0,0 @@ -# -# This file is part of LSPosed. -# -# LSPosed is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# LSPosed is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with LSPosed. If not, see . -# -# Copyright (C) 2020 EdXposed Contributors -# Copyright (C) 2021 LSPosed Contributors -# - -check_magisk_version() { - ui_print "- Magisk version: $MAGISK_VER_CODE" - if [ "$FLAVOR" == "riru" ] || [ "$FLAVOR" == "zygisk" ]; then - if [ "$MAGISK_VER_CODE" -lt 26000 ]; then - ui_print "*********************************************************" - ui_print "! Please install Magisk v26+" - abort "*********************************************************" - fi - else - ui_print "*********************************************************" - ui_print "! Unsupported flavor $FLAVOR" - abort "*********************************************************" - fi -} - -require_new_android() { - ui_print "*********************************************************" - ui_print "! Unsupported Android version ${1} (below Oreo MR1)" - ui_print "! Learn more from our GitHub" - [ "$BOOTMODE" == "true" ] && am start -a android.intent.action.VIEW -d https://github.com/JingMatrix/LSPosed/#supported-versions - abort "*********************************************************" -} - -check_android_version() { - if [ "$API" -ge 27 ]; then - ui_print "- Android SDK version: $API" - else - require_new_android "$API" - fi -} - -check_incompatible_module() { - MODULEDIR="$(magisk --path)/.magisk/modules" - for id in "riru_dreamland" "riru_edxposed" "riru_edxposed_sandhook" "taichi"; do - if [ -d "$MODULEDIR/$id" ] && [ ! -f "$MODULEDIR/$id/disable" ] && [ ! -f "$MODULEDIR/$id/remove" ]; then - ui_print "*********************************************************" - ui_print "! Please disable or uninstall incompatible frameworks:" - ui_print "! $id" - abort "*********************************************************" - break - fi - done -} diff --git a/zygisk/module/verify.sh b/zygisk/module/verify.sh deleted file mode 100644 index 2e1f4efb8..000000000 --- a/zygisk/module/verify.sh +++ /dev/null @@ -1,71 +0,0 @@ -# -# This file is part of LSPosed. -# -# LSPosed is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# LSPosed is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with LSPosed. If not, see . -# -# Copyright (C) 2020 EdXposed Contributors -# Copyright (C) 2021 LSPosed Contributors -# - -TMPDIR_FOR_VERIFY="$TMPDIR/.vunzip" -mkdir "$TMPDIR_FOR_VERIFY" - -abort_verify() { - ui_print "*********************************************************" - ui_print "! $1" - ui_print "! This zip may be corrupted, please try downloading again" - abort "*********************************************************" -} - -# extract -extract() { - zip=$1 - file=$2 - dir=$3 - junk_paths=$4 - [ -z "$junk_paths" ] && junk_paths=false - opts="-o" - [ $junk_paths = true ] && opts="-oj" - - file_path="" - hash_path="" - if [ $junk_paths = true ]; then - file_path="$dir/$(basename "$file")" - hash_path="$TMPDIR_FOR_VERIFY/$(basename "$file").sha256" - else - file_path="$dir/$file" - hash_path="$TMPDIR_FOR_VERIFY/$file.sha256" - fi - - unzip $opts "$zip" "$file" -d "$dir" >&2 - [ -f "$file_path" ] || abort_verify "$file not exists" - - unzip $opts "$zip" "$file.sha256" -d "$TMPDIR_FOR_VERIFY" >&2 - [ -f "$hash_path" ] || abort_verify "$file.sha256 not exists" - - (echo "$(cat "$hash_path") $file_path" | sha256sum -c -s -) || abort_verify "Failed to verify $file" - ui_print "- Verified $file" >&1 -} - -file="META-INF/com/google/android/update-binary" -file_path="$TMPDIR_FOR_VERIFY/$file" -hash_path="$file_path.sha256" -unzip -o "$ZIPFILE" "META-INF/com/google/android/*" -d "$TMPDIR_FOR_VERIFY" >&2 -[ -f "$file_path" ] || abort_verify "$file not exists" -if [ -f "$hash_path" ]; then - (echo "$(cat "$hash_path") $file_path" | sha256sum -c -s -) || abort_verify "Failed to verify $file" - ui_print "- Verified $file" >&1 -else - ui_print "- Download from Magisk app" -fi From fb43b4e110f47db686f0c9bfe1b290817c617610 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 4 Mar 2026 23:16:17 +0100 Subject: [PATCH 12/24] Update transaction codes 1. Avoid hard-coding transaction codes for maintance convience. 2. No need to enforce descriptor, which only introduce detection point. --- .../java/org/lsposed/lspd/service/BridgeService.java | 4 +--- .../lsposed/lspd/service/LSPApplicationService.java | 4 ++-- zygisk/src/main/cpp/ipc_bridge.cpp | 12 +++--------- .../src/main/kotlin/org/matrix/vector/core/Main.kt | 4 +--- .../org/matrix/vector/service/BridgeService.kt | 6 ++---- .../kotlin/org/matrix/vector/service/ParcelUtils.kt | 1 - 6 files changed, 9 insertions(+), 22 deletions(-) diff --git a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java b/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java index 2773939c1..850cc4d08 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java @@ -19,8 +19,7 @@ public class BridgeService { - static final int TRANSACTION_CODE = ('_' << 24) | ('L' << 16) | ('S' << 8) | 'P'; // 1598837584 - private static final String DESCRIPTOR = "Vector"; + static final int TRANSACTION_CODE = ('_' << 24) | ('V' << 16) | ('E' << 8) | 'C'; private static final String SERVICE_NAME = "activity"; enum ACTION { @@ -132,7 +131,6 @@ private static synchronized void sendToBridge(IBinder binder, boolean isRestart) Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { - data.writeInterfaceToken(DESCRIPTOR); data.writeInt(ACTION.ACTION_SEND_BINDER.ordinal()); Log.v(TAG, "binder " + binder.toString()); data.writeStrongBinder(binder); diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java index 0ff6c301c..f5cd46555 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java @@ -40,8 +40,8 @@ import java.util.stream.Collectors; public class LSPApplicationService extends ILSPApplicationService.Stub { - final static int DEX_TRANSACTION_CODE = 1310096052; - final static int OBFUSCATION_MAP_TRANSACTION_CODE = 724533732; + final static int DEX_TRANSACTION_CODE = ('_' << 24) | ('D' << 16) | ('E' << 8) | 'X'; + final static int OBFUSCATION_MAP_TRANSACTION_CODE = ('_' << 24) | ('O' << 16) | ('B' << 8) | 'F'; // key: private final static Map, ProcessInfo> processes = new ConcurrentHashMap<>(); diff --git a/zygisk/src/main/cpp/ipc_bridge.cpp b/zygisk/src/main/cpp/ipc_bridge.cpp index ec55caf09..839a15445 100644 --- a/zygisk/src/main/cpp/ipc_bridge.cpp +++ b/zygisk/src/main/cpp/ipc_bridge.cpp @@ -81,8 +81,6 @@ class BinderCaller { // --- Binder IPC Protocol Constants --- // These are the "secret handshakes" used to communicate with the Vector manager service. -// The service descriptor that the remote Binder service expects. -constexpr auto kBridgeServiceDescriptor = "Vector"sv; // The name of the system service we use as a rendezvous point to find our manager service. // Using "activity" is a common technique as it's always available. constexpr auto kBridgeServiceName = "activity"sv; @@ -90,9 +88,9 @@ constexpr auto kBridgeServiceName = "activity"sv; constexpr auto kSystemServerBridgeServiceName = "serial"sv; // Transaction codes for specific actions. -constexpr jint kBridgeTransactionCode = 1598837584; -constexpr jint kDexTransactionCode = 1310096052; -constexpr jint kObfuscationMapTransactionCode = 724533732; +constexpr jint kBridgeTransactionCode = ('_' << 24) | ('V' << 16) | ('E' << 8) | 'C'; +constexpr jint kDexTransactionCode = ('_' << 24) | ('D' << 16) | ('E' << 8) | 'X'; +constexpr jint kObfuscationMapTransactionCode = ('_' << 24) | ('O' << 16) | ('B' << 8) | 'F'; // Action codes sent within a kBridgeTransactionCode transaction. constexpr jint kActionGetBinder = 2; @@ -267,10 +265,6 @@ lsplant::ScopedLocalRef IPCBridge::RequestAppBinder(JNIEnv *env, jstrin } // Write the request data to the 'data' parcel. - auto descriptor = - lsplant::ScopedLocalRef(env, env->NewStringUTF(kBridgeServiceDescriptor.data())); - lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_interface_token_method_, - descriptor.get()); lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, kActionGetBinder); lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_string_method_, nice_name); lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, diff --git a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt index a2cf9ede3..267a7cf32 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt @@ -9,9 +9,7 @@ import org.lsposed.lspd.util.Utils import org.matrix.vector.ParasiticManagerHooker import org.matrix.vector.ParasiticManagerSystemHooker -/** - * Main entry point for the Java-side loader, invoked via JNI from the Vector Zygisk module. - */ +/** Main entry point for the Java-side loader, invoked via JNI from the Vector Zygisk module. */ object Main { /** diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt index 0fb0360bf..5aeb86a20 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt @@ -19,9 +19,8 @@ import org.lsposed.lspd.util.Utils.Log */ object BridgeService { private const val TRANSACTION_CODE = - ('_'.code shl 24) or ('L'.code shl 16) or ('S'.code shl 8) or 'P'.code - private const val DESCRIPTOR = "Vector" - private const val TAG = "Vector-Bridge" + ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code + private const val TAG = "VectorBridge" /** Actions supported by the manual IPC bridge. */ private enum class Action { @@ -94,7 +93,6 @@ object BridgeService { @JvmStatic fun onTransact(data: Parcel, reply: Parcel?, flags: Int): Boolean { return try { - data.enforceInterface(DESCRIPTOR) val actionIdx = data.readInt() val action = Action.values().getOrElse(actionIdx) { Action.UNKNOWN } diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt index 0c96b3917..7df7b2be0 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt @@ -1,6 +1,5 @@ package org.matrix.vector.service -import android.annotation.SuppressLint import android.os.Parcel import java.lang.reflect.Method From aeb9f61bba9ca84df97a1947c7e75cd32869d425 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 4 Mar 2026 23:25:11 +0100 Subject: [PATCH 13/24] Delete original folder magisk-loader However, we should keep the file `zygisk.json` for smooth updating experience. --- magisk-loader/.gitignore | 3 - magisk-loader/build.gradle.kts | 272 ------------ .../META-INF/com/google/android/update-binary | 29 -- .../com/google/android/updater-script | 1 - magisk-loader/magisk_module/action.sh | 4 - magisk-loader/magisk_module/customize.sh | 157 ------- magisk-loader/magisk_module/daemon | 41 -- magisk-loader/magisk_module/module.prop | 7 - magisk-loader/magisk_module/service.sh | 25 -- magisk-loader/magisk_module/system.prop | 1 - magisk-loader/magisk_module/uninstall.sh | 30 -- magisk-loader/magisk_module/util_functions.sh | 63 --- magisk-loader/magisk_module/verify.sh | 71 ---- magisk-loader/proguard-rules.pro | 18 - magisk-loader/src/main/AndroidManifest.xml | 20 - .../main/java/org/lsposed/lspd/core/Main.java | 56 --- .../lsposed/lspd/service/BridgeService.java | 184 -------- .../org/lsposed/lspd/service/ParcelUtils.java | 59 --- .../lspd/util/ParasiticManagerHooker.java | 333 --------------- .../util/ParasiticManagerSystemHooker.java | 83 ---- magisk-loader/src/main/jni/CMakeLists.txt | 30 -- magisk-loader/src/main/jni/api/zygisk.hpp | 392 ----------------- .../src/main/jni/api/zygisk_main.cpp | 75 ---- magisk-loader/src/main/jni/include/loader.h | 31 -- magisk-loader/src/main/jni/src/config_impl.h | 40 -- .../src/main/jni/src/magisk_loader.cpp | 208 --------- .../src/main/jni/src/magisk_loader.h | 70 ---- magisk-loader/src/main/jni/src/service.cpp | 395 ------------------ magisk-loader/src/main/jni/src/service.h | 145 ------- .../src/main/jni/template/loader.cpp | 6 - magisk-loader/update/riru.json | 6 - zygisk/zygisk.json | 1 + 32 files changed, 1 insertion(+), 2855 deletions(-) delete mode 100644 magisk-loader/.gitignore delete mode 100644 magisk-loader/build.gradle.kts delete mode 100644 magisk-loader/magisk_module/META-INF/com/google/android/update-binary delete mode 100644 magisk-loader/magisk_module/META-INF/com/google/android/updater-script delete mode 100644 magisk-loader/magisk_module/action.sh delete mode 100644 magisk-loader/magisk_module/customize.sh delete mode 100644 magisk-loader/magisk_module/daemon delete mode 100644 magisk-loader/magisk_module/module.prop delete mode 100644 magisk-loader/magisk_module/service.sh delete mode 100644 magisk-loader/magisk_module/system.prop delete mode 100644 magisk-loader/magisk_module/uninstall.sh delete mode 100644 magisk-loader/magisk_module/util_functions.sh delete mode 100644 magisk-loader/magisk_module/verify.sh delete mode 100644 magisk-loader/proguard-rules.pro delete mode 100644 magisk-loader/src/main/AndroidManifest.xml delete mode 100644 magisk-loader/src/main/java/org/lsposed/lspd/core/Main.java delete mode 100644 magisk-loader/src/main/java/org/lsposed/lspd/service/BridgeService.java delete mode 100644 magisk-loader/src/main/java/org/lsposed/lspd/service/ParcelUtils.java delete mode 100644 magisk-loader/src/main/java/org/lsposed/lspd/util/ParasiticManagerHooker.java delete mode 100644 magisk-loader/src/main/java/org/lsposed/lspd/util/ParasiticManagerSystemHooker.java delete mode 100644 magisk-loader/src/main/jni/CMakeLists.txt delete mode 100644 magisk-loader/src/main/jni/api/zygisk.hpp delete mode 100644 magisk-loader/src/main/jni/api/zygisk_main.cpp delete mode 100644 magisk-loader/src/main/jni/include/loader.h delete mode 100644 magisk-loader/src/main/jni/src/config_impl.h delete mode 100644 magisk-loader/src/main/jni/src/magisk_loader.cpp delete mode 100644 magisk-loader/src/main/jni/src/magisk_loader.h delete mode 100644 magisk-loader/src/main/jni/src/service.cpp delete mode 100644 magisk-loader/src/main/jni/src/service.h delete mode 100644 magisk-loader/src/main/jni/template/loader.cpp delete mode 100644 magisk-loader/update/riru.json create mode 100644 zygisk/zygisk.json diff --git a/magisk-loader/.gitignore b/magisk-loader/.gitignore deleted file mode 100644 index b5e4b9f39..000000000 --- a/magisk-loader/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/build -/release -/.cxx diff --git a/magisk-loader/build.gradle.kts b/magisk-loader/build.gradle.kts deleted file mode 100644 index 239f0d30a..000000000 --- a/magisk-loader/build.gradle.kts +++ /dev/null @@ -1,272 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -import org.apache.commons.codec.binary.Hex -import org.apache.tools.ant.filters.FixCrLfFilter -import org.apache.tools.ant.filters.ReplaceTokens -import java.io.ByteArrayOutputStream -import java.security.MessageDigest - -plugins { - alias(libs.plugins.agp.app) - alias(libs.plugins.lsplugin.resopt) -} - -val moduleName = "LSPosed" -val moduleBaseId = "lsposed" -val authors = "JingMatrix & LSPosed Developers" - -val injectedPackageName: String by rootProject.extra -val injectedPackageUid: Int by rootProject.extra - -val defaultManagerPackageName: String by rootProject.extra -val verCode: Int by rootProject.extra -val verName: String by rootProject.extra - -android { - flavorDimensions += "api" - - buildFeatures { - prefab = true - buildConfig = true - } - - defaultConfig { - applicationId = "org.lsposed.lspd" - multiDexEnabled = false - - buildConfigField( - "String", - "DEFAULT_MANAGER_PACKAGE_NAME", - """"$defaultManagerPackageName"""" - ) - buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") - buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") - } - - buildTypes { - release { - isMinifyEnabled = true - proguardFiles("proguard-rules.pro") - } - } - - externalNativeBuild { - cmake { - path("src/main/jni/CMakeLists.txt") - } - } - - productFlavors { - all { - externalNativeBuild { - cmake { - arguments += "-DMODULE_NAME=${name.lowercase()}_$moduleBaseId" - arguments += "-DAPI=${name.lowercase()}" - } - } - } - - create("Zygisk") { - dimension = "api" - externalNativeBuild { - cmake { - arguments += "-DAPI_VERSION=1" - } - } - } - } - namespace = "org.lsposed.lspd" -} -abstract class Injected @Inject constructor(val magiskDir: String) { - @get:Inject - abstract val factory: ObjectFactory -} - -dependencies { - implementation(projects.core) - implementation(projects.hiddenapi.bridge) - implementation(projects.services.managerService) - implementation(projects.services.daemonService) - compileOnly(libs.androidx.annotation) - compileOnly(projects.hiddenapi.stubs) -} - -val zipAll = tasks.register("zipAll") { - group = "LSPosed" -} - -fun afterEval() = android.applicationVariants.forEach { variant -> - val variantCapped = variant.name.replaceFirstChar { it.uppercase() } - val variantLowered = variant.name.lowercase() - val buildTypeCapped = variant.buildType.name.replaceFirstChar { it.uppercase() } - val buildTypeLowered = variant.buildType.name.lowercase() - val flavorCapped = variant.flavorName!!.replaceFirstChar { it.uppercase() } - val flavorLowered = variant.flavorName!!.lowercase() - - val magiskDir = layout.buildDirectory.dir("magisk/$variantLowered") - - val moduleId = "${flavorLowered}_$moduleBaseId" - val zipFileName = "$moduleName-v$verName-$verCode-${flavorLowered}-$buildTypeLowered.zip" - - val prepareMagiskFilesTask = tasks.register("prepareMagiskFiles$variantCapped") { - group = "LSPosed" - dependsOn( - "assemble$variantCapped", - ":app:package$buildTypeCapped", - ":daemon:package$buildTypeCapped", - ":dex2oat:externalNativeBuild${buildTypeCapped}" - ) - into(magiskDir) - from("${rootProject.projectDir}/README.md") - from("$projectDir/magisk_module") { - exclude("module.prop", "customize.sh", "daemon") - } - from("$projectDir/magisk_module") { - include("module.prop") - expand( - "moduleId" to moduleId, - "versionName" to "v$verName", - "versionCode" to verCode, - "authorList" to authors, - "updateJson" to "https://raw.githubusercontent.com/JingMatrix/LSPosed/master/magisk-loader/update/${flavorLowered}.json", - "requirement" to when (flavorLowered) { - "zygisk" -> "Requires Magisk 26.0+ and Zygisk enabled" - else -> "No further requirements" - }, - "api" to flavorCapped, - ) - filter("eol" to FixCrLfFilter.CrLf.newInstance("lf")) - } - from("$projectDir/magisk_module") { - include("customize.sh", "daemon") - val tokens = mapOf( - "FLAVOR" to flavorLowered, - "DEBUG" to if (buildTypeLowered == "debug") "true" else "false" - ) - filter("tokens" to tokens) - filter("eol" to FixCrLfFilter.CrLf.newInstance("lf")) - } - from(project(":app").tasks.getByName("package$buildTypeCapped").outputs) { - include("*.apk") - rename(".*\\.apk", "manager.apk") - } - from(project(":daemon").tasks.getByName("package$buildTypeCapped").outputs) { - include("*.apk") - rename(".*\\.apk", "daemon.apk") - } - into("lib") { - val libDir = variantCapped + "/strip${variantCapped}DebugSymbols" - from(layout.buildDirectory.dir("intermediates/stripped_native_libs/$libDir/out/lib")) { - include("**/liblspd.so") - } - } - into("bin") { - from(project(":dex2oat").layout.buildDirectory.dir("intermediates/cmake/$buildTypeLowered/obj")) { - include("**/dex2oat") - include("**/liboat_hook.so") - } - } - val dexOutPath = if (buildTypeLowered == "release") - layout.buildDirectory.dir("intermediates/dex/$variantCapped/minify${variantCapped}WithR8") - else - layout.buildDirectory.dir("intermediates/dex/$variantCapped/mergeDex$variantCapped") - into("framework") { - from(dexOutPath) - rename("classes.dex", "lspd.dex") - } - val injected = objects.newInstance(magiskDir.get().asFile.path) - doLast { - injected.factory.fileTree().from(injected.magiskDir).visit { - if (isDirectory) return@visit - val md = MessageDigest.getInstance("SHA-256") - file.forEachBlock(4096) { bytes, size -> - md.update(bytes, 0, size) - } - File(file.path + ".sha256").writeText(Hex.encodeHexString(md.digest())) - } - } - } - - val zipTask = tasks.register("zip${variantCapped}") { - group = "LSPosed" - dependsOn(prepareMagiskFilesTask) - archiveFileName = zipFileName - destinationDirectory = file("$projectDir/release") - from(magiskDir) - } - - zipAll.configure { - dependsOn(zipTask) - } - - val adb: String = androidComponents.sdkComponents.adb.get().asFile.absolutePath - val pushTask = tasks.register("push${variantCapped}") { - group = "LSPosed" - dependsOn(zipTask) - workingDir("${projectDir}/release") - commandLine(adb, "push", zipFileName, "/data/local/tmp/") - } - val installMagiskTask = tasks.register("installMagisk${variantCapped}") { - group = "LSPosed" - dependsOn(pushTask) - commandLine( - adb, "shell", "su", "-c", - "magisk --install-module /data/local/tmp/${zipFileName}" - ) - } - tasks.register("installMagiskAndReboot${variantCapped}") { - group = "LSPosed" - dependsOn(installMagiskTask) - commandLine(adb, "shell", "su", "-c", "/system/bin/svc", "power", "reboot") - } - val installKsuTask = tasks.register("installKsu${variantCapped}") { - group = "LSPosed" - dependsOn(pushTask) - commandLine( - adb, "shell", "su", "-c", - "ksud module install /data/local/tmp/${zipFileName}" - ) - } - tasks.register("installKsuAndReboot${variantCapped}") { - group = "LSPosed" - dependsOn(installKsuTask) - commandLine(adb, "shell", "su", "-c", "/system/bin/svc", "power", "reboot") - } - val installAPatchTask = tasks.register("installAPatch${variantCapped}") { - group = "LSPosed" - dependsOn(pushTask) - commandLine( - adb, "shell", "su", "-c", - "apd module install /data/local/tmp/${zipFileName}" - ) - } - tasks.register("installAPatchAndReboot${variantCapped}") { - group = "LSPosed" - dependsOn(installAPatchTask) - commandLine(adb, "shell", "su", "-c", "/system/bin/svc", "power", "reboot") - } -} - -afterEvaluate { - afterEval() -} - -evaluationDependsOn(":app") -evaluationDependsOn(":daemon") diff --git a/magisk-loader/magisk_module/META-INF/com/google/android/update-binary b/magisk-loader/magisk_module/META-INF/com/google/android/update-binary deleted file mode 100644 index 26911f4be..000000000 --- a/magisk-loader/magisk_module/META-INF/com/google/android/update-binary +++ /dev/null @@ -1,29 +0,0 @@ -################# -# Initialization -################# - -umask 022 - -# echo before loading util_functions -ui_print() { echo "$1"; } - -require_new_magisk() { - ui_print "*******************************" - ui_print " Please install Magisk v20.4+! " - ui_print "*******************************" - exit 1 -} - -######################### -# Load util_functions.sh -######################### - -OUTFD=$2 -ZIPFILE=$3 - -[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk -. /data/adb/magisk/util_functions.sh -[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk - -install_module -exit 0 diff --git a/magisk-loader/magisk_module/META-INF/com/google/android/updater-script b/magisk-loader/magisk_module/META-INF/com/google/android/updater-script deleted file mode 100644 index 11d5c96e0..000000000 --- a/magisk-loader/magisk_module/META-INF/com/google/android/updater-script +++ /dev/null @@ -1 +0,0 @@ -#MAGISK diff --git a/magisk-loader/magisk_module/action.sh b/magisk-loader/magisk_module/action.sh deleted file mode 100644 index 42359ce19..000000000 --- a/magisk-loader/magisk_module/action.sh +++ /dev/null @@ -1,4 +0,0 @@ -DEFAULT_MANAGER_PACKAGE_NAME=org.lsposed.manager -INJECTED_PACKAGE_NAME=com.android.shell - -am start -c "${DEFAULT_MANAGER_PACKAGE_NAME}.LAUNCH_MANAGER" "${INJECTED_PACKAGE_NAME}/.BugreportWarningActivity" diff --git a/magisk-loader/magisk_module/customize.sh b/magisk-loader/magisk_module/customize.sh deleted file mode 100644 index 5fc4161cf..000000000 --- a/magisk-loader/magisk_module/customize.sh +++ /dev/null @@ -1,157 +0,0 @@ -# -# This file is part of LSPosed. -# -# LSPosed is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# LSPosed is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with LSPosed. If not, see . -# -# Copyright (C) 2020 EdXposed Contributors -# Copyright (C) 2021 LSPosed Contributors -# - -# shellcheck disable=SC2034 -SKIPUNZIP=1 - -FLAVOR=@FLAVOR@ - -enforce_install_from_magisk_app() { - if $BOOTMODE; then - ui_print "- Installing from Magisk app" - else - ui_print "*********************************************************" - ui_print "! Install from recovery is NOT supported" - ui_print "! Some recovery has broken implementations, please thus install from Magisk app" - abort "*********************************************************" - fi -} - -VERSION=$(grep_prop version "${TMPDIR}/module.prop") -ui_print "- LSPosed version ${VERSION}" - -# Extract verify.sh -ui_print "- Extracting verify.sh" -unzip -o "$ZIPFILE" 'verify.sh' -d "$TMPDIR" >&2 -if [ ! -f "$TMPDIR/verify.sh" ]; then - ui_print "*********************************************************" - ui_print "! Unable to extract verify.sh!" - ui_print "! This zip may be corrupted, please try downloading again" - abort "*********************************************************" -fi -. "$TMPDIR/verify.sh" - -# Base check -extract "$ZIPFILE" 'customize.sh' "$TMPDIR" -extract "$ZIPFILE" 'verify.sh' "$TMPDIR" -extract "$ZIPFILE" 'util_functions.sh' "$TMPDIR" -. "$TMPDIR/util_functions.sh" -check_android_version -if [ -z "$KSU" ] && [ -z "$APATCH" ]; then - check_magisk_version -fi -check_incompatible_module - -enforce_install_from_magisk_app - -# Check architecture -if [ "$ARCH" != "arm" ] && [ "$ARCH" != "arm64" ] && [ "$ARCH" != "x86" ] && [ "$ARCH" != "x64" ]; then - abort "! Unsupported platform: $ARCH" -else - ui_print "- Device platform: $ARCH" -fi - -# Extract libs -ui_print "- Extracting module files" - -extract "$ZIPFILE" 'module.prop' "$MODPATH" -extract "$ZIPFILE" 'action.sh' "$MODPATH" -extract "$ZIPFILE" 'service.sh' "$MODPATH" -extract "$ZIPFILE" 'uninstall.sh' "$MODPATH" -extract "$ZIPFILE" 'sepolicy.rule' "$MODPATH" -extract "$ZIPFILE" 'framework/lspd.dex' "$MODPATH" -extract "$ZIPFILE" 'daemon.apk' "$MODPATH" -extract "$ZIPFILE" 'daemon' "$MODPATH" -rm -f /data/adb/lspd/manager.apk -extract "$ZIPFILE" 'manager.apk' "$MODPATH" - -if [ "$FLAVOR" == "zygisk" ]; then - mkdir -p "$MODPATH/zygisk" - - if [ "$ARCH" = "arm" ] || [ "$ARCH" = "arm64" ]; then - extract "$ZIPFILE" "lib/armeabi-v7a/liblspd.so" "$MODPATH/zygisk" true - mv "$MODPATH/zygisk/liblspd.so" "$MODPATH/zygisk/armeabi-v7a.so" - - if [ "$IS64BIT" = true ]; then - extract "$ZIPFILE" "lib/arm64-v8a/liblspd.so" "$MODPATH/zygisk" true - mv "$MODPATH/zygisk/liblspd.so" "$MODPATH/zygisk/arm64-v8a.so" - fi - fi - - if [ "$ARCH" = "x86" ] || [ "$ARCH" = "x64" ]; then - extract "$ZIPFILE" "lib/x86/liblspd.so" "$MODPATH/zygisk" true - mv "$MODPATH/zygisk/liblspd.so" "$MODPATH/zygisk/x86.so" - - if [ "$IS64BIT" = true ]; then - extract "$ZIPFILE" "lib/x86_64/liblspd.so" "$MODPATH/zygisk" true - mv "$MODPATH/zygisk/liblspd.so" "$MODPATH/zygisk/x86_64.so" - fi - fi -fi - -if [ "$API" -ge 29 ]; then - ui_print "- Extracting dex2oat binaries" - mkdir "$MODPATH/bin" - - if [ "$ARCH" = "arm" ] || [ "$ARCH" = "arm64" ]; then - extract "$ZIPFILE" "bin/armeabi-v7a/dex2oat" "$MODPATH/bin" true - extract "$ZIPFILE" "bin/armeabi-v7a/liboat_hook.so" "$MODPATH/bin" true - mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat32" - mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook32.so" - - if [ "$IS64BIT" = true ]; then - extract "$ZIPFILE" "bin/arm64-v8a/dex2oat" "$MODPATH/bin" true - extract "$ZIPFILE" "bin/arm64-v8a/liboat_hook.so" "$MODPATH/bin" true - mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat64" - mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook64.so" - fi - elif [ "$ARCH" == "x86" ] || [ "$ARCH" == "x64" ]; then - extract "$ZIPFILE" "bin/x86/dex2oat" "$MODPATH/bin" true - extract "$ZIPFILE" "bin/x86/liboat_hook.so" "$MODPATH/bin" true - mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat32" - mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook32.so" - - if [ "$IS64BIT" = true ]; then - extract "$ZIPFILE" "bin/x86_64/dex2oat" "$MODPATH/bin" true - extract "$ZIPFILE" "bin/x86_64/liboat_hook.so" "$MODPATH/bin" true - mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat64" - mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook64.so" - fi - fi - - ui_print "- Patching binaries" - DEV_PATH=$(tr -dc 'a-z0-9' < /dev/urandom | head -c 32) - sed -i "s/5291374ceda0aef7c5d86cd2a4f6a3ac/$DEV_PATH/g" "$MODPATH/daemon.apk" - sed -i "s/5291374ceda0aef7c5d86cd2a4f6a3ac/$DEV_PATH/" "$MODPATH/bin/dex2oat32" - sed -i "s/5291374ceda0aef7c5d86cd2a4f6a3ac/$DEV_PATH/" "$MODPATH/bin/dex2oat64" -else - extract "$ZIPFILE" 'system.prop' "$MODPATH" -fi - -set_perm_recursive "$MODPATH" 0 0 0755 0644 -set_perm_recursive "$MODPATH/bin" 0 2000 0755 0755 u:object_r:xposed_file:s0 -chmod 0744 "$MODPATH/daemon" - -if [ "$(grep_prop ro.maple.enable)" == "1" ] && [ "$FLAVOR" == "zygisk" ]; then - ui_print "- Add ro.maple.enable=0" - echo "ro.maple.enable=0" >> "$MODPATH/system.prop" -fi - -ui_print "- Welcome to LSPosed!" diff --git a/magisk-loader/magisk_module/daemon b/magisk-loader/magisk_module/daemon deleted file mode 100644 index fb08a5103..000000000 --- a/magisk-loader/magisk_module/daemon +++ /dev/null @@ -1,41 +0,0 @@ -#!/system/bin/sh - -dir=${0%/*} -tmpLspdApk="/data/local/tmp/daemon.apk" -debug=@DEBUG@ -flavor=@FLAVOR@ - -if [ -r $tmpLspdApk ]; then - java_options="-Djava.class.path=$tmpLspdApk" - debug="true" -else - java_options="-Djava.class.path=$dir/daemon.apk" -fi - -if [ $debug = "true" ]; then - os_version=$(getprop ro.build.version.sdk) - if [ "$os_version" -eq "27" ]; then - java_options="$java_options -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable" - elif [ "$os_version" -eq "28" ]; then - java_options="$java_options -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable" - else - java_options="$java_options -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y" - fi -fi - -mount tmpfs -t tmpfs /data/resource-cache - -if [ ! -S "/dev/socket/zygote" ]; then - timeout 0.5 inotifyd - /dev/socket:near | while read -r line; do - $debug && log -p v -t "LSPosed" "inotify: $line" - if [ -S "/dev/socket/zygote" ]; then - $debug && log -p v -t "LSPosed" "zygote started" - touch /dev/socket& - exit - fi - done -fi -$debug && log -p d -t "LSPosed" "start $flavor daemon $*" - -# shellcheck disable=SC2086 -exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.lsposed.lspd.Main "$@" >/dev/null 2>&1 diff --git a/magisk-loader/magisk_module/module.prop b/magisk-loader/magisk_module/module.prop deleted file mode 100644 index 68f62aaea..000000000 --- a/magisk-loader/magisk_module/module.prop +++ /dev/null @@ -1,7 +0,0 @@ -id=${moduleId} -name=${api} - LSPosed -version=${versionName} (${versionCode}) -versionCode=${versionCode} -author=${authorList} -description=Another enhanced implementation of Xposed Framework. Supports Android 8.1 ~ 16. ${requirement}. -updateJson=${updateJson} diff --git a/magisk-loader/magisk_module/service.sh b/magisk-loader/magisk_module/service.sh deleted file mode 100644 index 87e6e9ed6..000000000 --- a/magisk-loader/magisk_module/service.sh +++ /dev/null @@ -1,25 +0,0 @@ -# -# This file is part of LSPosed. -# -# LSPosed is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# LSPosed is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with LSPosed. If not, see . -# -# Copyright (C) 2021 LSPosed Contributors -# - -MODDIR=${0%/*} - -cd "$MODDIR" - -# To avoid delaying the normal mount timing of zygote, we start LSPosed service daemon in late_start service mode instead of post-fs-data mode -unshare --propagation slave -m sh -c "$MODDIR/daemon --system-server-max-retry=3 $@&" diff --git a/magisk-loader/magisk_module/system.prop b/magisk-loader/magisk_module/system.prop deleted file mode 100644 index 64184152a..000000000 --- a/magisk-loader/magisk_module/system.prop +++ /dev/null @@ -1 +0,0 @@ -dalvik.vm.dex2oat-flags=--inline-max-code-units=0 diff --git a/magisk-loader/magisk_module/uninstall.sh b/magisk-loader/magisk_module/uninstall.sh deleted file mode 100644 index 35f36e196..000000000 --- a/magisk-loader/magisk_module/uninstall.sh +++ /dev/null @@ -1,30 +0,0 @@ -# -# This file is part of LSPosed. -# -# LSPosed is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# LSPosed is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with LSPosed. If not, see . -# -# Copyright (C) 2021 LSPosed Contributors -# - -MODDIR=${0%/*} -MODSDIR=$(dirname "$MODDIR") -if [ -d "$MODSDIR/riru_lsposed" ] && [ -d "$MODSDIR/zygisk_lsposed" ]; then - if [ -f "$MODSDIR/riru_lsposed/remove" ] && [ -f "$MODSDIR/zygisk_lsposed/remove" ]; then - rm -rf /data/adb/lspd - fi -else - rm -rf /data/adb/lspd -fi - -rm -rf /data/adb/riru/modules/lspd diff --git a/magisk-loader/magisk_module/util_functions.sh b/magisk-loader/magisk_module/util_functions.sh deleted file mode 100644 index 4bb9ea87f..000000000 --- a/magisk-loader/magisk_module/util_functions.sh +++ /dev/null @@ -1,63 +0,0 @@ -# -# This file is part of LSPosed. -# -# LSPosed is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# LSPosed is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with LSPosed. If not, see . -# -# Copyright (C) 2020 EdXposed Contributors -# Copyright (C) 2021 LSPosed Contributors -# - -check_magisk_version() { - ui_print "- Magisk version: $MAGISK_VER_CODE" - if [ "$FLAVOR" == "riru" ] || [ "$FLAVOR" == "zygisk" ]; then - if [ "$MAGISK_VER_CODE" -lt 26000 ]; then - ui_print "*********************************************************" - ui_print "! Please install Magisk v26+" - abort "*********************************************************" - fi - else - ui_print "*********************************************************" - ui_print "! Unsupported flavor $FLAVOR" - abort "*********************************************************" - fi -} - -require_new_android() { - ui_print "*********************************************************" - ui_print "! Unsupported Android version ${1} (below Oreo MR1)" - ui_print "! Learn more from our GitHub" - [ "$BOOTMODE" == "true" ] && am start -a android.intent.action.VIEW -d https://github.com/JingMatrix/LSPosed/#supported-versions - abort "*********************************************************" -} - -check_android_version() { - if [ "$API" -ge 27 ]; then - ui_print "- Android SDK version: $API" - else - require_new_android "$API" - fi -} - -check_incompatible_module() { - MODULEDIR="$(magisk --path)/.magisk/modules" - for id in "riru_dreamland" "riru_edxposed" "riru_edxposed_sandhook" "taichi"; do - if [ -d "$MODULEDIR/$id" ] && [ ! -f "$MODULEDIR/$id/disable" ] && [ ! -f "$MODULEDIR/$id/remove" ]; then - ui_print "*********************************************************" - ui_print "! Please disable or uninstall incompatible frameworks:" - ui_print "! $id" - abort "*********************************************************" - break - fi - done -} diff --git a/magisk-loader/magisk_module/verify.sh b/magisk-loader/magisk_module/verify.sh deleted file mode 100644 index 2e1f4efb8..000000000 --- a/magisk-loader/magisk_module/verify.sh +++ /dev/null @@ -1,71 +0,0 @@ -# -# This file is part of LSPosed. -# -# LSPosed is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# LSPosed is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with LSPosed. If not, see . -# -# Copyright (C) 2020 EdXposed Contributors -# Copyright (C) 2021 LSPosed Contributors -# - -TMPDIR_FOR_VERIFY="$TMPDIR/.vunzip" -mkdir "$TMPDIR_FOR_VERIFY" - -abort_verify() { - ui_print "*********************************************************" - ui_print "! $1" - ui_print "! This zip may be corrupted, please try downloading again" - abort "*********************************************************" -} - -# extract -extract() { - zip=$1 - file=$2 - dir=$3 - junk_paths=$4 - [ -z "$junk_paths" ] && junk_paths=false - opts="-o" - [ $junk_paths = true ] && opts="-oj" - - file_path="" - hash_path="" - if [ $junk_paths = true ]; then - file_path="$dir/$(basename "$file")" - hash_path="$TMPDIR_FOR_VERIFY/$(basename "$file").sha256" - else - file_path="$dir/$file" - hash_path="$TMPDIR_FOR_VERIFY/$file.sha256" - fi - - unzip $opts "$zip" "$file" -d "$dir" >&2 - [ -f "$file_path" ] || abort_verify "$file not exists" - - unzip $opts "$zip" "$file.sha256" -d "$TMPDIR_FOR_VERIFY" >&2 - [ -f "$hash_path" ] || abort_verify "$file.sha256 not exists" - - (echo "$(cat "$hash_path") $file_path" | sha256sum -c -s -) || abort_verify "Failed to verify $file" - ui_print "- Verified $file" >&1 -} - -file="META-INF/com/google/android/update-binary" -file_path="$TMPDIR_FOR_VERIFY/$file" -hash_path="$file_path.sha256" -unzip -o "$ZIPFILE" "META-INF/com/google/android/*" -d "$TMPDIR_FOR_VERIFY" >&2 -[ -f "$file_path" ] || abort_verify "$file not exists" -if [ -f "$hash_path" ]; then - (echo "$(cat "$hash_path") $file_path" | sha256sum -c -s -) || abort_verify "Failed to verify $file" - ui_print "- Verified $file" >&1 -else - ui_print "- Download from Magisk app" -fi diff --git a/magisk-loader/proguard-rules.pro b/magisk-loader/proguard-rules.pro deleted file mode 100644 index 36e55739f..000000000 --- a/magisk-loader/proguard-rules.pro +++ /dev/null @@ -1,18 +0,0 @@ --keepclasseswithmembers class org.lsposed.lspd.core.Main { - public static void forkCommon(boolean, java.lang.String, java.lang.String, android.os.IBinder); -} --keepclasseswithmembers,includedescriptorclasses class * { - native ; -} --keepclasseswithmembers class org.lsposed.lspd.service.BridgeService { - public static boolean *(android.os.IBinder, int, long, long, int); -} - --assumenosideeffects class android.util.Log { - public static *** v(...); - public static *** d(...); -} --repackageclasses --allowaccessmodification --dontwarn org.lsposed.lspd.core.* --dontwarn org.lsposed.lspd.util.Hookers diff --git a/magisk-loader/src/main/AndroidManifest.xml b/magisk-loader/src/main/AndroidManifest.xml deleted file mode 100644 index adcc8f429..000000000 --- a/magisk-loader/src/main/AndroidManifest.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/magisk-loader/src/main/java/org/lsposed/lspd/core/Main.java b/magisk-loader/src/main/java/org/lsposed/lspd/core/Main.java deleted file mode 100644 index d908f12c9..000000000 --- a/magisk-loader/src/main/java/org/lsposed/lspd/core/Main.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.core; - -import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; - -import android.os.IBinder; -import android.os.Process; - -import org.lsposed.lspd.service.ILSPApplicationService; -import org.lsposed.lspd.util.ParasiticManagerHooker; -import org.lsposed.lspd.util.ParasiticManagerSystemHooker; -import org.lsposed.lspd.util.Utils; -import org.lsposed.lspd.BuildConfig; - -public class Main { - - public static void forkCommon(boolean isSystem, String niceName, String appDir, IBinder binder) { - if (isSystem) { - ParasiticManagerSystemHooker.start(); - } - - Startup.initXposed(isSystem, niceName, appDir, ILSPApplicationService.Stub.asInterface(binder)); - - try { - Utils.Log.muted = serviceClient.isLogMuted(); - } catch (Throwable t) { - Utils.logE("failed to configure logs", t); - } - - if (niceName.equals(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME) && ParasiticManagerHooker.start()) { - Utils.logI("Loaded manager, skipping next steps"); - return; - } - - Utils.logI("Loading xposed for " + niceName + "/" + Process.myUid()); - Startup.bootstrapXposed(); - } -} diff --git a/magisk-loader/src/main/java/org/lsposed/lspd/service/BridgeService.java b/magisk-loader/src/main/java/org/lsposed/lspd/service/BridgeService.java deleted file mode 100644 index 873196a53..000000000 --- a/magisk-loader/src/main/java/org/lsposed/lspd/service/BridgeService.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static hidden.HiddenApiBridge.Binder_allowBlocking; -import static hidden.HiddenApiBridge.Context_getActivityToken; - -import android.app.ActivityThread; -import android.app.IApplicationThread; -import android.content.Context; -import android.os.Binder; -import android.os.IBinder; -import android.os.Parcel; -import android.os.RemoteException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.lsposed.lspd.BuildConfig; -import org.lsposed.lspd.util.Utils.Log; - -public class BridgeService { - private static final int TRANSACTION_CODE = ('_' << 24) | ('L' << 16) | ('S' << 8) | 'P'; - private static final String DESCRIPTOR = "LSPosed"; - protected static final String TAG = "LSPosed-Bridge"; - - enum ACTION { - ACTION_UNKNOWN, - ACTION_SEND_BINDER, - ACTION_GET_BINDER, - ACTION_ENABLE_MANAGER, - } - - // for client - private static IBinder serviceBinder = null; - private static ILSPosedService service = null; - - // for client - private static final IBinder.DeathRecipient serviceRecipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - serviceBinder.unlinkToDeath(this, 0); - serviceBinder = null; - service = null; - Log.e(TAG, "service is dead"); - } - }; - - // For client - private static void receiveFromBridge(IBinder binder) { - if (binder == null) { - Log.e(TAG, "received empty binder"); - return; - } - - var token = Binder.clearCallingIdentity(); - if (serviceBinder != null) { - serviceBinder.unlinkToDeath(serviceRecipient, 0); - } - Binder.restoreCallingIdentity(token); - - serviceBinder = Binder_allowBlocking(binder); - service = ILSPosedService.Stub.asInterface(serviceBinder); - try { - serviceBinder.linkToDeath(serviceRecipient, 0); - } catch (Throwable e) { - Log.e(TAG, "service link to death: ", e); - } - try { - IApplicationThread at = ActivityThread.currentActivityThread().getApplicationThread(); - Context ctx = ActivityThread.currentActivityThread().getSystemContext(); - service.dispatchSystemServerContext(at.asBinder(), Context_getActivityToken(ctx), BuildConfig.FLAVOR); - } catch (Throwable e) { - Log.e(TAG, "dispatch context: ", e); - } - Log.i(TAG, "binder received"); - } - - public static ILSPosedService getService() { - return service; - } - - @SuppressWarnings({"unused", "RedundantSuppression"}) - public static boolean onTransact(@NonNull Parcel data, @Nullable Parcel reply, int flags) { - if (!ParcelUtils.safeEnforceInterface(data, DESCRIPTOR)) return false; - - try { - ACTION action = ACTION.values()[data.readInt()]; - - Log.d(TAG, "onTransact: action=" + action + ", callingUid=" + Binder.getCallingUid() + ", callingPid=" + Binder.getCallingPid()); - - switch (action) { - case ACTION_SEND_BINDER: { - if (Binder.getCallingUid() == 0) { - receiveFromBridge(data.readStrongBinder()); - if (reply != null) { - reply.writeNoException(); - } - return true; - } - break; - } - case ACTION_GET_BINDER: { - IBinder binder = null; - try { - String processName = data.readString(); - IBinder heartBeat = data.readStrongBinder(); - var applicationService = service == null ? null : service.requestApplicationService(Binder.getCallingUid(), Binder.getCallingPid(), processName, heartBeat); - if (applicationService != null) binder = applicationService.asBinder(); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - if (binder != null && reply != null) { - reply.writeNoException(); - Log.d(TAG, "got binder is " + binder); - reply.writeStrongBinder(binder); - return true; - } - return false; - } - case ACTION_ENABLE_MANAGER: { - var uid = Binder.getCallingUid(); - if ((uid == 0 || uid == 2000 || uid == 1000) && service != null) { - var result = service.setManagerEnabled(data.readInt() == 1); - if (reply != null) reply.writeInt(result ? 1 : 0); - return true; - } - return false; - } - } - } catch (Throwable e) { - Log.e(TAG, "onTransact", e); - } - return false; - } - - @SuppressWarnings("unused") - public static boolean execTransact(IBinder obj, int code, long dataObj, long replyObj, int flags) { - if (code != TRANSACTION_CODE) return false; - - Parcel data = ParcelUtils.fromNativePointer(dataObj); - Parcel reply = ParcelUtils.fromNativePointer(replyObj); - - if (data == null || reply == null) { - Log.w(TAG, "Got transaction with null data or reply"); - return false; - } - - try { - try { - return onTransact(data, reply, flags); - } catch (Exception e) { - if ((flags & IBinder.FLAG_ONEWAY) != 0) { - Log.w(TAG, "Caught a Exception from the binder stub implementation. ", e); - } else { - reply.setDataPosition(0); - reply.writeException(e); - } - Log.w(TAG, "on transact", e); - return true; - } - } finally { - data.recycle(); - reply.recycle(); - } - } -} diff --git a/magisk-loader/src/main/java/org/lsposed/lspd/service/ParcelUtils.java b/magisk-loader/src/main/java/org/lsposed/lspd/service/ParcelUtils.java deleted file mode 100644 index 560c8becc..000000000 --- a/magisk-loader/src/main/java/org/lsposed/lspd/service/ParcelUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import android.annotation.SuppressLint; -import android.os.Parcel; - -import java.lang.reflect.Method; - -public class ParcelUtils { - public static boolean safeEnforceInterface(Parcel parcel, String descriptor) { - try { - parcel.enforceInterface(descriptor); - return true; - } catch (Throwable e) { - return false; - } - } - - private static Method obtainMethod; - - @SuppressLint("SoonBlockedPrivateApi") - public static Parcel fromNativePointer(long ptr) { - if (ptr == 0) return null; - - if (obtainMethod == null) { - try { - //noinspection JavaReflectionMemberAccess - obtainMethod = Parcel.class.getDeclaredMethod("obtain", long.class); - obtainMethod.setAccessible(true); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - - try { - return (Parcel) obtainMethod.invoke(null, ptr); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } -} diff --git a/magisk-loader/src/main/java/org/lsposed/lspd/util/ParasiticManagerHooker.java b/magisk-loader/src/main/java/org/lsposed/lspd/util/ParasiticManagerHooker.java deleted file mode 100644 index 061c820e1..000000000 --- a/magisk-loader/src/main/java/org/lsposed/lspd/util/ParasiticManagerHooker.java +++ /dev/null @@ -1,333 +0,0 @@ -package org.lsposed.lspd.util; - -import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; - -import android.app.ActivityThread; -import android.app.LoadedApk; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.PersistableBundle; -import android.util.AndroidRuntimeException; -import android.util.ArrayMap; -import android.webkit.WebViewDelegate; -import android.webkit.WebViewFactory; -import android.webkit.WebViewFactoryProvider; - -import org.lsposed.lspd.ILSPManagerService; - -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.lang.reflect.Method; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import de.robv.android.xposed.XC_MethodHook; -import de.robv.android.xposed.XC_MethodReplacement; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedHelpers; -import hidden.HiddenApiBridge; - -public class ParasiticManagerHooker { - private static final String CHROMIUM_WEBVIEW_FACTORY_METHOD = "create"; - - private static PackageInfo managerPkgInfo = null; - private static int managerFd = -1; - private final static Map states = new ConcurrentHashMap<>(); - private final static Map persistentStates = new ConcurrentHashMap<>(); - - private synchronized static PackageInfo getManagerPkgInfo(ApplicationInfo appInfo) { - if (managerPkgInfo == null && appInfo != null) { - try { - Context ctx = ActivityThread.currentActivityThread().getSystemContext(); - var sourceDir = "/proc/self/fd/" + managerFd; - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { - var dstDir = appInfo.dataDir + "/cache/lsposed.apk"; - try (var inStream = new FileInputStream(sourceDir); var outStream = new FileOutputStream(dstDir)) { - FileChannel inChannel = inStream.getChannel(); - FileChannel outChannel = outStream.getChannel(); - inChannel.transferTo(0, inChannel.size(), outChannel); - sourceDir = dstDir; - } catch (Throwable e) { - Hookers.logE("copy apk", e); - } - } - managerPkgInfo = ctx.getPackageManager().getPackageArchiveInfo(sourceDir, PackageManager.GET_ACTIVITIES); - var newAppInfo = managerPkgInfo.applicationInfo; - newAppInfo.sourceDir = sourceDir; - newAppInfo.publicSourceDir = sourceDir; - newAppInfo.nativeLibraryDir = appInfo.nativeLibraryDir; - newAppInfo.packageName = appInfo.packageName; - newAppInfo.dataDir = HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(appInfo); - newAppInfo.deviceProtectedDataDir = appInfo.deviceProtectedDataDir; - newAppInfo.processName = appInfo.processName; - HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(newAppInfo, HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(appInfo)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - HiddenApiBridge.ApplicationInfo_overlayPaths(newAppInfo, HiddenApiBridge.ApplicationInfo_overlayPaths(appInfo)); - } - HiddenApiBridge.ApplicationInfo_resourceDirs(newAppInfo, HiddenApiBridge.ApplicationInfo_resourceDirs(appInfo)); - newAppInfo.uid = appInfo.uid; - // FIXME: It seems the parsed flags is incorrect (0) on A14 QPR3 - newAppInfo.flags = newAppInfo.flags | ApplicationInfo.FLAG_HAS_CODE; - } catch (Throwable e) { - Utils.logE("get manager pkginfo", e); - } - } - return managerPkgInfo; - } - - private static void sendBinderToManager(final ClassLoader classLoader, IBinder binder) { - try { - var clazz = XposedHelpers.findClass("org.lsposed.manager.Constants", classLoader); - var ok = (boolean) XposedHelpers.callStaticMethod(clazz, "setBinder", - new Class[]{IBinder.class}, binder); - if (ok) return; - throw new RuntimeException("setBinder: " + false); - } catch (Throwable t) { - Utils.logW("Could not send binder to LSPosed Manager", t); - } - } - - private static void hookForManager(ILSPManagerService managerService) { - var managerApkHooker = new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) { - Hookers.logD("ActivityThread#handleBindApplication() starts"); - Object bindData = param.args[0]; - ApplicationInfo appInfo = (ApplicationInfo) XposedHelpers.getObjectField(bindData, "appInfo"); - XposedHelpers.setObjectField(bindData, "appInfo", getManagerPkgInfo(appInfo).applicationInfo); - } - }; - XposedHelpers.findAndHookMethod(ActivityThread.class, - "handleBindApplication", - "android.app.ActivityThread$AppBindData", - managerApkHooker); - - var unhooks = new XC_MethodHook.Unhook[]{null}; - unhooks[0] = XposedHelpers.findAndHookMethod( - LoadedApk.class, "getClassLoader", new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - var pkgInfo = getManagerPkgInfo(null); - if (pkgInfo != null && XposedHelpers.getObjectField(param.thisObject, "mApplicationInfo") == pkgInfo.applicationInfo) { - var sSourceDir = pkgInfo.applicationInfo.sourceDir; - var pathClassLoader = param.getResult(); - - Hookers.logD("LoadedApk getClassLoader " + pathClassLoader); - var pathList = XposedHelpers.getObjectField(pathClassLoader, "pathList"); - List lstDexPath = (List)XposedHelpers.callMethod(pathList, "getDexPaths"); - if (!lstDexPath.contains(sSourceDir)) { - Utils.logW("Could not find manager apk injected in classloader"); - XposedHelpers.callMethod(pathClassLoader, "addDexPath", sSourceDir); - } - sendBinderToManager((ClassLoader) pathClassLoader, managerService.asBinder()); - unhooks[0].unhook(); - } - } - }); - - var activityClientRecordClass = XposedHelpers.findClass("android.app.ActivityThread$ActivityClientRecord", ActivityThread.class.getClassLoader()); - var activityHooker = new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) { - for (var i = 0; i < param.args.length; ++i) { - if (param.args[i] instanceof ActivityInfo) { - var aInfo = (ActivityInfo) param.args[i]; - var pkgInfo = getManagerPkgInfo(aInfo.applicationInfo); - if (pkgInfo == null) return; - for (var activity : pkgInfo.activities) { - if ("org.lsposed.manager.ui.activity.MainActivity".equals(activity.name)) { - activity.applicationInfo = pkgInfo.applicationInfo; - param.args[i] = activity; - } - } - } - if (param.args[i] instanceof Intent) { - var intent = (Intent) param.args[i]; - intent.setComponent(new ComponentName(intent.getComponent().getPackageName(), "org.lsposed.manager.ui.activity.MainActivity")); - } - } - if (param.method.getName().equals("scheduleLaunchActivity")) { - ActivityInfo aInfo = null; - var parameters = ((Method) param.method).getParameterTypes(); - for (var i = 0; i < parameters.length; ++i) { - if (parameters[i] == ActivityInfo.class) { - aInfo = (ActivityInfo) param.args[i]; - Hookers.logD("loading state of " + aInfo.name); - } else if (parameters[i] == Bundle.class && aInfo != null) { - final int idx = i; - states.computeIfPresent(aInfo.name, (k, v) -> { - param.args[idx] = v; - return v; - }); - } else if (parameters[i] == PersistableBundle.class && aInfo != null) { - final int idx = i; - persistentStates.computeIfPresent(aInfo.name, (k, v) -> { - param.args[idx] = v; - return v; - }); - } - } - - } - } - - @Override - protected void afterHookedMethod(MethodHookParam param) { - for (var i = 0; i < param.args.length && activityClientRecordClass.isInstance(param.thisObject); ++i) { - if (param.args[i] instanceof ActivityInfo) { - var aInfo = (ActivityInfo) param.args[i]; - Hookers.logD("loading state of " + aInfo.name); - states.computeIfPresent(aInfo.name, (k, v) -> { - XposedHelpers.setObjectField(param.thisObject, "state", v); - return v; - }); - persistentStates.computeIfPresent(aInfo.name, (k, v) -> { - XposedHelpers.setObjectField(param.thisObject, "persistentState", v); - return v; - }); - } - } - } - }; - XposedBridge.hookAllConstructors(activityClientRecordClass, activityHooker); - - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { - XposedBridge.hookAllMethods(XposedHelpers.findClass("android.app.ActivityThread$ApplicationThread", ActivityThread.class.getClassLoader()), "scheduleLaunchActivity", activityHooker); - } - - XposedBridge.hookAllMethods(ActivityThread.class, "handleReceiver", new XC_MethodReplacement() { - @Override - protected Object replaceHookedMethod(MethodHookParam param) { - for (var arg : param.args) { - if (arg instanceof BroadcastReceiver.PendingResult) { - ((BroadcastReceiver.PendingResult) arg).finish(); - } - } - return null; - } - }); - - XposedBridge.hookAllMethods(ActivityThread.class, "installProvider", new XC_MethodHook() { - private Context originalContext = null; - - @Override - protected void beforeHookedMethod(MethodHookParam param) { - Hookers.logD("before install provider"); - Context ctx = null; - ProviderInfo info = null; - int ctxIdx = -1; - for (var i = 0; i < param.args.length; ++i) { - var arg = param.args[i]; - if (arg instanceof Context) { - ctx = (Context) arg; - ctxIdx = i; - } else if (arg instanceof ProviderInfo) info = (ProviderInfo) arg; - } - var pkgInfo = getManagerPkgInfo(null); - if (ctx != null && info != null && pkgInfo != null) { - var packageName = pkgInfo.applicationInfo.packageName; - if (!info.applicationInfo.packageName.equals(packageName)) return; - if (originalContext == null) { - info.applicationInfo.packageName = packageName + ".origin"; - var originalPkgInfo = ActivityThread.currentActivityThread().getPackageInfoNoCheck(info.applicationInfo, HiddenApiBridge.Resources_getCompatibilityInfo(ctx.getResources())); - XposedHelpers.setObjectField(originalPkgInfo, "mPackageName", packageName); - originalContext = (Context) XposedHelpers.callStaticMethod(XposedHelpers.findClass("android.app.ContextImpl", null), - "createAppContext", ActivityThread.currentActivityThread(), originalPkgInfo); - info.applicationInfo.packageName = packageName; - } - param.args[ctxIdx] = originalContext; - } else { - Hookers.logE("Failed to reload provider", new RuntimeException()); - } - } - }); - - XposedHelpers.findAndHookMethod(WebViewFactory.class, "getProvider", new XC_MethodReplacement() { - @Override - protected Object replaceHookedMethod(MethodHookParam param) { - var sProviderInstance = XposedHelpers.getStaticObjectField(WebViewFactory.class, "sProviderInstance"); - if (sProviderInstance != null) return sProviderInstance; - //noinspection unchecked - var providerClass = (Class) XposedHelpers.callStaticMethod(WebViewFactory.class, "getProviderClass"); - Method staticFactory = null; - try { - staticFactory = providerClass.getMethod( - CHROMIUM_WEBVIEW_FACTORY_METHOD, WebViewDelegate.class); - } catch (Exception e) { - Hookers.logE("error instantiating provider with static factory method", e); - } - - try { - var webViewDelegateConstructor = WebViewDelegate.class.getDeclaredConstructor(); - webViewDelegateConstructor.setAccessible(true); - if (staticFactory != null) { - sProviderInstance = staticFactory.invoke(null, webViewDelegateConstructor.newInstance()); - } - XposedHelpers.setStaticObjectField(WebViewFactory.class, "sProviderInstance", sProviderInstance); - Hookers.logD("Loaded provider: " + sProviderInstance); - return sProviderInstance; - } catch (Exception e) { - Hookers.logE("error instantiating provider", e); - throw new AndroidRuntimeException(e); - } - } - }); - var stateHooker = new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) { - try { - var record = param.args[0]; - if (record instanceof IBinder) { - record = ((ArrayMap) XposedHelpers.getObjectField(param.thisObject, "mActivities")).get(record); - if (record == null) return; - } - XposedHelpers.callMethod(param.thisObject, Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? "callActivityOnSaveInstanceState" : "callCallActivityOnSaveInstanceState", record); - var state = (Bundle) XposedHelpers.getObjectField(record, "state"); - var persistentState = (PersistableBundle) XposedHelpers.getObjectField(record, "persistentState"); - var aInfo = (ActivityInfo) XposedHelpers.getObjectField(record, "activityInfo"); - states.compute(aInfo.name, (k, v) -> state); - persistentStates.compute(aInfo.name, (k, v) -> persistentState); - Hookers.logD("saving state of " + aInfo.name); - } catch (Throwable e) { - Hookers.logE("save state", e); - } - } - }; - XposedBridge.hookAllMethods(ActivityThread.class, "performStopActivityInner", stateHooker); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) - XposedHelpers.findAndHookMethod(ActivityThread.class, "performDestroyActivity", IBinder.class, boolean.class, int.class, boolean.class, stateHooker); - } - - - static public boolean start() { - List binder = new ArrayList<>(1); - try (var managerParcelFd = serviceClient.requestInjectedManagerBinder(binder)) { - if (binder.size() > 0 && binder.get(0) != null && managerParcelFd != null) { - managerFd = managerParcelFd.detachFd(); - var managerService = ILSPManagerService.Stub.asInterface(binder.get(0)); - hookForManager(managerService); - Utils.logD("injected manager"); - return true; - } else { - // Not manager - return false; - } - } catch (Throwable e) { - Utils.logE("failed to inject manager", e); - return false; - } - } -} diff --git a/magisk-loader/src/main/java/org/lsposed/lspd/util/ParasiticManagerSystemHooker.java b/magisk-loader/src/main/java/org/lsposed/lspd/util/ParasiticManagerSystemHooker.java deleted file mode 100644 index 1a203916e..000000000 --- a/magisk-loader/src/main/java/org/lsposed/lspd/util/ParasiticManagerSystemHooker.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.lsposed.lspd.util; - -import android.annotation.SuppressLint; -import android.app.ProfilerInfo; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ResolveInfo; - -import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker; -import org.lsposed.lspd.impl.LSPosedHelper; -import org.lsposed.lspd.service.BridgeService; -import org.lsposed.lspd.util.Utils; - -import io.github.libxposed.api.XposedInterface; - - -public class ParasiticManagerSystemHooker implements HandleSystemServerProcessHooker.Callback { - public static void start() { - HandleSystemServerProcessHooker.callback = new ParasiticManagerSystemHooker(); - } - - /*@XposedHooker - private static class Hooker2 implements XposedInterface.Hooker { - @BeforeInvocation - public static void beforeHookedMethod(XposedInterface.BeforeHookCallback callback) throws Throwable { - Log.d("LSPosed", "checking new activity"); - var self = callback.getThisObject(); - if (self == null) return; - var request = XposedHelpers.getObjectField(self, "mRequest"); - Log.d("LSPosed", "start activity intent=" + XposedHelpers.getObjectField(request, "intent") + " ai=" + XposedHelpers.getObjectField(request, "activityInfo"), new Throwable()); - } - }*/ - - private static class Hooker implements XposedInterface.Hooker { - public static void after(XposedInterface.AfterHookCallback callback) throws Throwable { - var intent = (Intent) callback.getArgs()[0]; - if (intent == null) return; - if (!intent.hasCategory("org.lsposed.manager.LAUNCH_MANAGER")) return; - var aInfo = (ActivityInfo) callback.getResult(); - if (aInfo == null || !"com.android.shell".equals(aInfo.packageName)) return; - // We shouldn't pollute system's object - aInfo = new ActivityInfo(aInfo); - // use a different process name - aInfo.processName = "org.lsposed.manager"; - // choose a theme that has transition animation - aInfo.theme = android.R.style.Theme_DeviceDefault_Settings; - // remove some annoying flags - aInfo.flags = aInfo.flags & ~(ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS | ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS); - BridgeService.getService().preStartManager(); - callback.setResult(aInfo); - } - } - - @SuppressLint("PrivateApi") - @Override - public void onSystemServerLoaded(ClassLoader classLoader) { - try { - Class supervisorClass; - try { - // 14-12.0 - supervisorClass = Class.forName("com.android.server.wm.ActivityTaskSupervisor", false, classLoader); - } catch (ClassNotFoundException ignore) { - try { - // 11-10 - supervisorClass = Class.forName("com.android.server.wm.ActivityStackSupervisor", false, classLoader); - } catch (ClassNotFoundException ignore2) { - // 9-8.1 - supervisorClass = Class.forName("com.android.server.am.ActivityStackSupervisor", false, classLoader); - } - } - LSPosedHelper.hookMethod(Hooker.class, supervisorClass, "resolveActivity", Intent.class, ResolveInfo.class, int.class, ProfilerInfo.class); - /* - for (var method: Class.forName("com.android.server.wm.ActivityStarter", false, classLoader).getDeclaredMethods()) { - if ("execute".equals(method.getName())) - HookBridge.deoptimizeMethod(method); - } - LSPosedHelper.hookAllMethods(Hooker2.class, Class.forName("com.android.server.wm.ActivityStarter", false, classLoader), "execute");*/ - Utils.logD("hooked activity starter"); - } catch (Throwable e) { - Utils.logE("onSystemServerLoaded: ", e); - } - } -} diff --git a/magisk-loader/src/main/jni/CMakeLists.txt b/magisk-loader/src/main/jni/CMakeLists.txt deleted file mode 100644 index 43cad4be5..000000000 --- a/magisk-loader/src/main/jni/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(lspd) - -add_subdirectory(${VECTOR_ROOT}/core/src/main/jni core) - -configure_file(template/loader.cpp src/loader.cpp) - -aux_source_directory(src SRC_LIST) -if (${API} STREQUAL "zygisk") - set(SRC_LIST ${SRC_LIST} api/zygisk_main.cpp) -endif() - -add_library(${PROJECT_NAME} SHARED ${SRC_LIST} ${CMAKE_CURRENT_BINARY_DIR}/src/loader.cpp) - -target_include_directories(${PROJECT_NAME} PUBLIC include) -target_include_directories(${PROJECT_NAME} PRIVATE src) - -target_link_libraries(${PROJECT_NAME} core log) - -if (DEFINED DEBUG_SYMBOLS_PATH) - set(DEBUG_SYMBOLS_PATH ${DEBUG_SYMBOLS_PATH}/${API}) - message(STATUS "Debug symbols will be placed at ${DEBUG_SYMBOLS_PATH}") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} - COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ - ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug - COMMAND ${CMAKE_STRIP} --strip-all $ - COMMAND ${CMAKE_OBJCOPY} --add-gnu-debuglink ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug - $) -endif() diff --git a/magisk-loader/src/main/jni/api/zygisk.hpp b/magisk-loader/src/main/jni/api/zygisk.hpp deleted file mode 100644 index 6bd6c6f6f..000000000 --- a/magisk-loader/src/main/jni/api/zygisk.hpp +++ /dev/null @@ -1,392 +0,0 @@ -/* Copyright 2022-2023 John "topjohnwu" Wu - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ - -// This is the public API for Zygisk modules. -// DO NOT MODIFY ANY CODE IN THIS HEADER. - -#pragma once - -#include -#include - -#define ZYGISK_API_VERSION 4 - -/* - -*************** -* Introduction -*************** - -On Android, all app processes are forked from a special daemon called "Zygote". -For each new app process, zygote will fork a new process and perform "specialization". -This specialization operation enforces the Android security sandbox on the newly forked -process to make sure that 3rd party application code is only loaded after it is being -restricted within a sandbox. - -On Android, there is also this special process called "system_server". This single -process hosts a significant portion of system services, which controls how the -Android operating system and apps interact with each other. - -The Zygisk framework provides a way to allow developers to build modules and run custom -code before and after system_server and any app processes' specialization. -This enable developers to inject code and alter the behavior of system_server and app processes. - -Please note that modules will only be loaded after zygote has forked the child process. -THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON! - -********************* -* Development Guide -********************* - -Define a class and inherit zygisk::ModuleBase to implement the functionality of your module. -Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk. - -Example code: - -static jint (*orig_logger_entry_max)(JNIEnv *env); -static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); } - -class ExampleModule : public zygisk::ModuleBase { -public: - void onLoad(zygisk::Api *api, JNIEnv *env) override { - this->api = api; - this->env = env; - } - void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { - JNINativeMethod methods[] = { - { "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max }, - }; - api->hookJniNativeMethods(env, "android/util/Log", methods, 1); - *(void **) &orig_logger_entry_max = methods[0].fnPtr; - } -private: - zygisk::Api *api; - JNIEnv *env; -}; - -REGISTER_ZYGISK_MODULE(ExampleModule) - ------------------------------------------------------------------------------------------ - -Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize, -or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class -never runs in a true superuser environment. - -If your module require access to superuser permissions, you can create and register -a root companion handler function. This function runs in a separate root companion -daemon process, and an Unix domain socket is provided to allow you to perform IPC between -your target process and the root companion process. - -Example code: - -static void example_handler(int socket) { ... } - -REGISTER_ZYGISK_COMPANION(example_handler) - -*/ - -namespace zygisk { - -struct Api; -struct AppSpecializeArgs; -struct ServerSpecializeArgs; - -class ModuleBase { -public: - - // This method is called as soon as the module is loaded into the target process. - // A Zygisk API handle will be passed as an argument. - virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {} - - // This method is called before the app process is specialized. - // At this point, the process just got forked from zygote, but no app specific specialization - // is applied. This means that the process does not have any sandbox restrictions and - // still runs with the same privilege of zygote. - // - // All the arguments that will be sent and used for app specialization is passed as a single - // AppSpecializeArgs object. You can read and overwrite these arguments to change how the app - // process will be specialized. - // - // If you need to run some operations as superuser, you can call Api::connectCompanion() to - // get a socket to do IPC calls with a root companion process. - // See Api::connectCompanion() for more info. - virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {} - - // This method is called after the app process is specialized. - // At this point, the process has all sandbox restrictions enabled for this application. - // This means that this method runs with the same privilege of the app's own code. - virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {} - - // This method is called before the system server process is specialized. - // See preAppSpecialize(args) for more info. - virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {} - - // This method is called after the system server process is specialized. - // At this point, the process runs with the privilege of system_server. - virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {} -}; - -struct AppSpecializeArgs { - // Required arguments. These arguments are guaranteed to exist on all Android versions. - jint &uid; - jint &gid; - jintArray &gids; - jint &runtime_flags; - jobjectArray &rlimits; - jint &mount_external; - jstring &se_info; - jstring &nice_name; - jstring &instruction_set; - jstring &app_data_dir; - - // Optional arguments. Please check whether the pointer is null before de-referencing - jintArray *const fds_to_ignore; - jboolean *const is_child_zygote; - jboolean *const is_top_app; - jobjectArray *const pkg_data_info_list; - jobjectArray *const whitelisted_data_info_list; - jboolean *const mount_data_dirs; - jboolean *const mount_storage_dirs; - - AppSpecializeArgs() = delete; -}; - -struct ServerSpecializeArgs { - jint &uid; - jint &gid; - jintArray &gids; - jint &runtime_flags; - jlong &permitted_capabilities; - jlong &effective_capabilities; - - ServerSpecializeArgs() = delete; -}; - -namespace internal { -struct api_table; -template void entry_impl(api_table *, JNIEnv *); -} - -// These values are used in Api::setOption(Option) -enum Option : int { - // Force Magisk's denylist unmount routines to run on this process. - // - // Setting this option only makes sense in preAppSpecialize. - // The actual unmounting happens during app process specialization. - // - // Set this option to force all Magisk and modules' files to be unmounted from the - // mount namespace of the process, regardless of the denylist enforcement status. - FORCE_DENYLIST_UNMOUNT = 0, - - // When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize. - // Be aware that after dlclose-ing your module, all of your code will be unmapped from memory. - // YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS. - DLCLOSE_MODULE_LIBRARY = 1, -}; - -// Bit masks of the return value of Api::getFlags() -enum StateFlag : uint32_t { - // The user has granted root access to the current process - PROCESS_GRANTED_ROOT = (1u << 0), - - // The current process was added on the denylist - PROCESS_ON_DENYLIST = (1u << 1), -}; - -// All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded -// from the specialized process afterwards. -struct Api { - - // Connect to a root companion process and get a Unix domain socket for IPC. - // - // This API only works in the pre[XXX]Specialize methods due to SELinux restrictions. - // - // The pre[XXX]Specialize methods run with the same privilege of zygote. - // If you would like to do some operations with superuser permissions, register a handler - // function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func). - // Another good use case for a companion process is that if you want to share some resources - // across multiple processes, hold the resources in the companion process and pass it over. - // - // The root companion process is ABI aware; that is, when calling this method from a 32-bit - // process, you will be connected to a 32-bit companion process, and vice versa for 64-bit. - // - // Returns a file descriptor to a socket that is connected to the socket passed to your - // module's companion request handler. Returns -1 if the connection attempt failed. - int connectCompanion(); - - // Get the file descriptor of the root folder of the current module. - // - // This API only works in the pre[XXX]Specialize methods. - // Accessing the directory returned is only possible in the pre[XXX]Specialize methods - // or in the root companion process (assuming that you sent the fd over the socket). - // Both restrictions are due to SELinux and UID. - // - // Returns -1 if errors occurred. - int getModuleDir(); - - // Set various options for your module. - // Please note that this method accepts one single option at a time. - // Check zygisk::Option for the full list of options available. - void setOption(Option opt); - - // Get information about the current process. - // Returns bitwise-or'd zygisk::StateFlag values. - uint32_t getFlags(); - - // Exempt the provided file descriptor from being automatically closed. - // - // This API only make sense in preAppSpecialize; calling this method in any other situation - // is either a no-op (returns true) or an error (returns false). - // - // When false is returned, the provided file descriptor will eventually be closed by zygote. - bool exemptFd(int fd); - - // Hook JNI native methods for a class - // - // Lookup all registered JNI native methods and replace it with your own methods. - // The original function pointer will be saved in each JNINativeMethod's fnPtr. - // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr - // will be set to nullptr. - void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); - - // Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory. - // - // Parsing /proc/[PID]/maps will give you the memory map of a process. As an example: - // - //
- // 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64 - // (More details: https://man7.org/linux/man-pages/man5/proc.5.html) - // - // The `dev` and `inode` pair uniquely identifies a file being mapped into memory. - // For matching ELFs loaded in memory, replace function `symbol` with `newFunc`. - // If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`. - void pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc); - - // Commit all the hooks that was previously registered. - // Returns false if an error occurred. - bool pltHookCommit(); - -private: - internal::api_table *tbl; - template friend void internal::entry_impl(internal::api_table *, JNIEnv *); -}; - -// Register a class as a Zygisk module - -#define REGISTER_ZYGISK_MODULE(clazz) \ -void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ - zygisk::internal::entry_impl(table, env); \ -} - -// Register a root companion request handler function for your module -// -// The function runs in a superuser daemon process and handles a root companion request from -// your module running in a target process. The function has to accept an integer value, -// which is a Unix domain socket that is connected to the target process. -// See Api::connectCompanion() for more info. -// -// NOTE: the function can run concurrently on multiple threads. -// Be aware of race conditions if you have globally shared resources. - -#define REGISTER_ZYGISK_COMPANION(func) \ -void zygisk_companion_entry(int client) { func(client); } - -/********************************************************* - * The following is internal ABI implementation detail. - * You do not have to understand what it is doing. - *********************************************************/ - -namespace internal { - -struct module_abi { - long api_version; - ModuleBase *impl; - - void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *); - void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *); - void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *); - void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *); - - module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) { - preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); }; - postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); }; - preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); }; - postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); }; - } -}; - -struct api_table { - // Base - void *impl; - bool (*registerModule)(api_table *, module_abi *); - - void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int); - void (*pltHookRegister)(dev_t, ino_t, const char *, void *, void **); - bool (*exemptFd)(int); - bool (*pltHookCommit)(); - int (*connectCompanion)(void * /* impl */); - void (*setOption)(void * /* impl */, Option); - int (*getModuleDir)(void * /* impl */); - uint32_t (*getFlags)(void * /* impl */); -}; - -template -void entry_impl(api_table *table, JNIEnv *env) { - static Api api; - api.tbl = table; - static T module; - ModuleBase *m = &module; - static module_abi abi(m); - if (!table->registerModule(table, &abi)) return; - m->onLoad(&api, env); -} - -} // namespace internal - -inline int Api::connectCompanion() { - return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1; -} -inline int Api::getModuleDir() { - return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1; -} -inline void Api::setOption(Option opt) { - if (tbl->setOption) tbl->setOption(tbl->impl, opt); -} -inline uint32_t Api::getFlags() { - return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0; -} -inline bool Api::exemptFd(int fd) { - return tbl->exemptFd != nullptr && tbl->exemptFd(fd); -} -inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { - if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods); -} -inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc) { - if (tbl->pltHookRegister) tbl->pltHookRegister(dev, inode, symbol, newFunc, oldFunc); -} -inline bool Api::pltHookCommit() { - return tbl->pltHookCommit != nullptr && tbl->pltHookCommit(); -} - -} // namespace zygisk - -extern "C" { - -[[gnu::visibility("default"), maybe_unused]] -void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); - -[[gnu::visibility("default"), maybe_unused]] -void zygisk_companion_entry(int); - -} // extern "C" diff --git a/magisk-loader/src/main/jni/api/zygisk_main.cpp b/magisk-loader/src/main/jni/api/zygisk_main.cpp deleted file mode 100644 index 1eca2b356..000000000 --- a/magisk-loader/src/main/jni/api/zygisk_main.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -#include -#include -#include -#include - -#include "config_impl.h" -#include "magisk_loader.h" -#include "zygisk.hpp" - -namespace lspd { -int allow_unload = 0; -int *allowUnload = &allow_unload; - -class ZygiskModule : public zygisk::ModuleBase { - JNIEnv *env_; - zygisk::Api *api_; - - void onLoad(zygisk::Api *api, JNIEnv *env) override { - env_ = env; - api_ = api; - MagiskLoader::Init(); - ConfigImpl::Init(); - } - - void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { - MagiskLoader::GetInstance()->OnNativeForkAndSpecializePre( - env_, args->uid, args->gids, args->nice_name, - args->is_child_zygote ? *args->is_child_zygote : false, args->app_data_dir); - } - - void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override { - MagiskLoader::GetInstance()->OnNativeForkAndSpecializePost(env_, args->nice_name, - args->app_data_dir); - if (*allowUnload) api_->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); - } - - void preServerSpecialize([[maybe_unused]] zygisk::ServerSpecializeArgs *args) override { - MagiskLoader::GetInstance()->OnNativeForkSystemServerPre(env_); - } - - void postServerSpecialize([[maybe_unused]] const zygisk::ServerSpecializeArgs *args) override { - if (__system_property_find("ro.vendor.product.ztename")) { - auto *process = env_->FindClass("android/os/Process"); - auto *set_argv0 = env_->GetStaticMethodID(process, "setArgV0", "(Ljava/lang/String;)V"); - auto *name = env_->NewStringUTF("system_server"); - env_->CallStaticVoidMethod(process, set_argv0, name); - env_->DeleteLocalRef(name); - env_->DeleteLocalRef(process); - } - MagiskLoader::GetInstance()->OnNativeForkSystemServerPost(env_); - if (*allowUnload) api_->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); - } -}; -} // namespace lspd - -REGISTER_ZYGISK_MODULE(lspd::ZygiskModule); diff --git a/magisk-loader/src/main/jni/include/loader.h b/magisk-loader/src/main/jni/include/loader.h deleted file mode 100644 index e6a824e82..000000000 --- a/magisk-loader/src/main/jni/include/loader.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -// -// Created by Nullptr on 2022/3/16. -// - -#pragma once - -#include "config.h" - -namespace lspd { - extern const int apiVersion; - extern const char* const moduleName; -} diff --git a/magisk-loader/src/main/jni/src/config_impl.h b/magisk-loader/src/main/jni/src/config_impl.h deleted file mode 100644 index 08109c52f..000000000 --- a/magisk-loader/src/main/jni/src/config_impl.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -#pragma once - -#include "config_bridge.h" -#include "service.h" - -namespace lspd { - class ConfigImpl : public ConfigBridge { - public: - inline static void Init() { - instance_ = std::make_unique(); - } - - virtual obfuscation_map_t &obfuscation_map() override { return obfuscation_map_; } - - virtual void - obfuscation_map(obfuscation_map_t m) override { obfuscation_map_ = std::move(m); } - - private: - inline static std::map obfuscation_map_; - }; -} diff --git a/magisk-loader/src/main/jni/src/magisk_loader.cpp b/magisk-loader/src/main/jni/src/magisk_loader.cpp deleted file mode 100644 index dd0a3fe71..000000000 --- a/magisk-loader/src/main/jni/src/magisk_loader.cpp +++ /dev/null @@ -1,208 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -#include "magisk_loader.h" - -#include -#include -#include -#include - -#include - -#include "config_impl.h" -#include "service.h" -#include "symbol_cache.h" -#include "utils/jni_helper.hpp" - -using namespace lsplant; - -static_assert(FS_IOC_SETFLAGS == LP_SELECT(0x40046602, 0x40086602)); - -namespace lspd { -extern int *allowUnload; -jboolean is_parasitic_manager = JNI_FALSE; - -constexpr int FIRST_ISOLATED_UID = 99000; -constexpr int LAST_ISOLATED_UID = 99999; -constexpr int FIRST_APP_ZYGOTE_ISOLATED_UID = 90000; -constexpr int LAST_APP_ZYGOTE_ISOLATED_UID = 98999; -constexpr int SHARED_RELRO_UID = 1037; -constexpr int PER_USER_RANGE = 100000; - -static constexpr uid_t kAidInjected = INJECTED_UID; -static constexpr uid_t kAidInet = 3003; - -void MagiskLoader::LoadDex(JNIEnv *env, PreloadedDex &&dex) { - auto classloader = JNI_FindClass(env, "java/lang/ClassLoader"); - auto getsyscl_mid = JNI_GetStaticMethodID(env, classloader, "getSystemClassLoader", - "()Ljava/lang/ClassLoader;"); - auto sys_classloader = JNI_CallStaticObjectMethod(env, classloader, getsyscl_mid); - if (!sys_classloader) [[unlikely]] { - LOGE("getSystemClassLoader failed!!!"); - return; - } - auto in_memory_classloader = JNI_FindClass(env, "dalvik/system/InMemoryDexClassLoader"); - auto initMid = JNI_GetMethodID(env, in_memory_classloader, "", - "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); - auto byte_buffer_class = JNI_FindClass(env, "java/nio/ByteBuffer"); - auto dex_buffer = env->NewDirectByteBuffer(dex.data(), dex.size()); - if (auto my_cl = - JNI_NewObject(env, in_memory_classloader, initMid, dex_buffer, sys_classloader)) { - inject_class_loader_ = JNI_NewGlobalRef(env, my_cl); - } else { - LOGE("InMemoryDexClassLoader creation failed!!!"); - return; - } - - env->DeleteLocalRef(dex_buffer); -} - -std::string GetEntryClassName() { - const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); - static auto signature = obfs_map.at("org.lsposed.lspd.core.") + "Main"; - return signature; -} - -void MagiskLoader::SetupEntryClass(JNIEnv *env) { - if (auto entry_class = FindClassFromLoader(env, GetCurrentClassLoader(), GetEntryClassName())) { - entry_class_ = JNI_NewGlobalRef(env, entry_class); - } -} - -void MagiskLoader::OnNativeForkSystemServerPre(JNIEnv *env) { - Service::instance()->InitService(env); - setAllowUnload(skip_); -} - -void MagiskLoader::OnNativeForkSystemServerPost(JNIEnv *env) { - if (!skip_) { - auto *instance = Service::instance(); - auto system_server_binder = instance->RequestSystemServerBinder(env); - if (!system_server_binder) { - LOGF("Failed to get system server binder, system server initialization failed."); - return; - } - - auto application_binder = - instance->RequestApplicationBinderFromSystemServer(env, system_server_binder); - - // Call application_binder directly if application binder is available, - // or we proxy the request from system server binder - auto &&next_binder = application_binder ? application_binder : system_server_binder; - const auto [dex_fd, size] = instance->RequestLSPDex(env, next_binder); - auto obfs_map = instance->RequestObfuscationMap(env, next_binder); - ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); - LoadDex(env, PreloadedDex(dex_fd, size)); - close(dex_fd); - instance->HookBridge(*this, env); - - // always inject into system server - InitArtHooker(env, initInfo); - InitHooks(env); - SetupEntryClass(env); - FindAndCall(env, "forkCommon", - "(ZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_TRUE, - JNI_NewStringUTF(env, "system"), nullptr, application_binder, - is_parasitic_manager); - GetArt(true); - } -} - -void MagiskLoader::OnNativeForkAndSpecializePre(JNIEnv *env, jint uid, jintArray &gids, - jstring &nice_name, jboolean is_child_zygote, - jstring app_data_dir) { - jboolean is_manager = JNI_FALSE; - if (uid == kAidInjected) { - const JUTFString name(env, nice_name); - if (name.get() == "org.lsposed.manager"sv) { - int array_size = gids ? env->GetArrayLength(gids) : 0; - auto region = std::make_unique(array_size + 1); - auto *new_gids = env->NewIntArray(array_size + 1); - if (gids) env->GetIntArrayRegion(gids, 0, array_size, region.get()); - region.get()[array_size] = kAidInet; - env->SetIntArrayRegion(new_gids, 0, array_size + 1, region.get()); - if (gids) env->SetIntArrayRegion(gids, 0, 1, region.get() + array_size); - gids = new_gids; - nice_name = JNI_NewStringUTF(env, "com.android.shell").release(); - is_manager = JNI_TRUE; - } - } - is_parasitic_manager = is_manager; - Service::instance()->InitService(env); - const auto app_id = uid % PER_USER_RANGE; - JUTFString process_name(env, nice_name); - skip_ = false; - if (!skip_ && !app_data_dir) { - LOGD("skip injecting into {} because it has no data dir", process_name.get()); - skip_ = true; - } - if (!skip_ && is_child_zygote) { - skip_ = true; - LOGD("skip injecting into {} because it's a child zygote", process_name.get()); - } - - if (!skip_ && - ((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)) { - skip_ = true; - LOGI("skip injecting into {} because it's isolated", process_name.get()); - } - setAllowUnload(skip_); -} - -void MagiskLoader::OnNativeForkAndSpecializePost(JNIEnv *env, jstring nice_name, jstring app_dir) { - const JUTFString process_name(env, nice_name); - auto *instance = Service::instance(); - if (is_parasitic_manager) nice_name = JNI_NewStringUTF(env, "org.lsposed.manager").release(); - auto binder = - skip_ ? ScopedLocalRef{env, nullptr} : instance->RequestBinder(env, nice_name); - if (binder) { - auto [dex_fd, size] = instance->RequestLSPDex(env, binder); - auto obfs_map = instance->RequestObfuscationMap(env, binder); - ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); - LoadDex(env, PreloadedDex(dex_fd, size)); - close(dex_fd); - InitArtHooker(env, initInfo); - InitHooks(env); - SetupEntryClass(env); - LOGD("Done prepare"); - FindAndCall(env, "forkCommon", - "(ZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_FALSE, - nice_name, app_dir, binder); - LOGD("injected xposed into {}", process_name.get()); - setAllowUnload(false); - GetArt(true); - } else { - auto context = Context::ReleaseInstance(); - auto service = Service::ReleaseInstance(); - GetArt(true); - LOGD("skipped {}", process_name.get()); - setAllowUnload(true); - } -} - -void MagiskLoader::setAllowUnload(bool unload) { - if (allowUnload) { - *allowUnload = unload ? 1 : 0; - } -} -} // namespace lspd diff --git a/magisk-loader/src/main/jni/src/magisk_loader.h b/magisk-loader/src/main/jni/src/magisk_loader.h deleted file mode 100644 index 57da14ff4..000000000 --- a/magisk-loader/src/main/jni/src/magisk_loader.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -// -// Created by Nullptr on 2022/3/16. -// - -#pragma once - -#include "../src/native_api.h" -#include "context.h" -#include "elf_util.h" -#include "symbol_cache.h" - -namespace lspd { -class MagiskLoader : public Context { -public: - inline static void Init() { instance_ = std::make_unique(); } - - inline static MagiskLoader *GetInstance() { - return static_cast(instance_.get()); - } - - void OnNativeForkAndSpecializePre(JNIEnv *env, jint uid, jintArray &gids, jstring &nice_name, - jboolean is_child_zygote, jstring app_data_dir); - - void OnNativeForkAndSpecializePost(JNIEnv *env, jstring nice_name, jstring app_dir); - - void OnNativeForkSystemServerPre(JNIEnv *env); - - void OnNativeForkSystemServerPost(JNIEnv *env); - -protected: - void LoadDex(JNIEnv *env, PreloadedDex &&dex) override; - - void SetupEntryClass(JNIEnv *env) override; - -private: - bool skip_ = false; - const lsplant::InitInfo initInfo = lsplant::InitInfo{ - .inline_hooker = - [](auto t, auto r) { - void *bk = nullptr; - return HookInline(t, r, &bk) == 0 ? bk : nullptr; - }, - .inline_unhooker = [](auto t) { return UnhookInline(t) == 0; }, - .art_symbol_resolver = [](auto symbol) { return GetArt()->getSymbAddress(symbol); }, - .art_symbol_prefix_resolver = - [](auto symbol) { return GetArt()->getSymbPrefixFirstAddress(symbol); }, - }; - - static void setAllowUnload(bool unload); -}; -} // namespace lspd diff --git a/magisk-loader/src/main/jni/src/service.cpp b/magisk-loader/src/main/jni/src/service.cpp deleted file mode 100644 index e7b7e3f11..000000000 --- a/magisk-loader/src/main/jni/src/service.cpp +++ /dev/null @@ -1,395 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -// -// Created by loves on 2/7/2021. -// - -#include -#include -#include "loader.h" -#include "service.h" -#include "context.h" -#include "utils/jni_helper.hpp" -#include "symbol_cache.h" -#include "config_bridge.h" -#include "elf_util.h" - -using namespace lsplant; - -namespace lspd { - std::unique_ptr Service::instance_ = std::make_unique(); - - std::atomic last_failed_id = ~0; - - class IPCThreadState { - static IPCThreadState* (*selfOrNullFn)(); - static pid_t (*getCallingPidFn)(IPCThreadState*); - static uid_t (*getCallingUidFn)(IPCThreadState*); - - public: - - uint64_t getCallingId() { - if (getCallingUidFn != nullptr && getCallingPidFn != nullptr) [[likely]] { - auto pid = getCallingUidFn(this); - auto uid = getCallingPidFn(this); - return (static_cast(uid) << 32) | pid; - } - return ~0; - } - - static IPCThreadState* selfOrNull() { - if (selfOrNullFn != nullptr) [[likely]] { - return selfOrNullFn(); - } - return nullptr; - } - - static void Init(const SandHook::ElfImg *binder) { - if (binder == nullptr) { - LOGE("libbinder not found"); - return; - } - selfOrNullFn = reinterpret_cast( - binder->getSymbAddress("_ZN7android14IPCThreadState10selfOrNullEv")); - getCallingPidFn = reinterpret_cast( - binder->getSymbAddress("_ZNK7android14IPCThreadState13getCallingPidEv")); - getCallingUidFn = reinterpret_cast( - binder->getSymbAddress("_ZNK7android14IPCThreadState13getCallingUidEv")); - LOGI("libbinder selfOrNull {} getCallingPid {} getCallingUid {}", (void*) selfOrNullFn, (void*) getCallingPidFn, (void*) getCallingUidFn); - } - }; - - IPCThreadState* (*IPCThreadState::selfOrNullFn)() = nullptr; - uid_t (*IPCThreadState::getCallingUidFn)(IPCThreadState*) = nullptr; - pid_t (*IPCThreadState::getCallingPidFn)(IPCThreadState*) = nullptr; - - jboolean - Service::exec_transact_replace(jboolean *res, JNIEnv *env, [[maybe_unused]] jobject obj, - va_list args) { - va_list copy; - va_copy(copy, args); - auto code = va_arg(copy, jint); - auto data_obj = va_arg(copy, jlong); - auto reply_obj = va_arg(copy, jlong); - auto flags = va_arg(copy, jint); - va_end(copy); - - if (code == BRIDGE_TRANSACTION_CODE) [[unlikely]] { - *res = JNI_CallStaticBooleanMethod(env, instance()->bridge_service_class_, - instance()->exec_transact_replace_methodID_, - obj, code, data_obj, reply_obj, flags); - if (!*res) { - auto self = IPCThreadState::selfOrNull(); - if (self != nullptr) { - auto id = self->getCallingId(); - last_failed_id.store(id, std::memory_order_relaxed); - } - } - return true; - } - return false; - } - - jboolean - Service::call_boolean_method_va_replace(JNIEnv *env, jobject obj, jmethodID methodId, - va_list args) { - bool need_skip = false; - if (auto self = IPCThreadState::selfOrNull(); self != nullptr) { - auto last = last_failed_id.load(std::memory_order_relaxed); - auto current = self->getCallingId(); - need_skip = last == current; - } - if (!need_skip && methodId == instance()->exec_transact_backup_methodID_) [[unlikely]] { - jboolean res = false; - if (exec_transact_replace(&res, env, obj, args)) [[unlikely]] return res; - // else fallback to backup - } - return instance()->call_boolean_method_va_backup_(env, obj, methodId, args); - } - - void Service::InitService(JNIEnv *env) { - if (initialized_) [[unlikely]] return; - - // ServiceManager - if (auto service_manager_class = JNI_FindClass(env, "android/os/ServiceManager")) { - service_manager_class_ = JNI_NewGlobalRef(env, service_manager_class); - } else return; - get_service_method_ = JNI_GetStaticMethodID(env, service_manager_class_, "getService", - "(Ljava/lang/String;)Landroid/os/IBinder;"); - if (!get_service_method_) return; - - // IBinder - if (auto ibinder_class = JNI_FindClass(env, "android/os/IBinder")) { - transact_method_ = JNI_GetMethodID(env, ibinder_class, "transact", - "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z"); - } else return; - - if (auto binder_class = JNI_FindClass(env, "android/os/Binder")) { - binder_class_ = JNI_NewGlobalRef(env, binder_class); - } else return; - binder_ctor_ = JNI_GetMethodID(env, binder_class_, "", "()V"); - - // Parcel - if (auto parcel_class = JNI_FindClass(env, "android/os/Parcel")) { - parcel_class_ = JNI_NewGlobalRef(env, parcel_class); - } else return; - data_size_method_ = JNI_GetMethodID(env, parcel_class_, "dataSize","()I"); - obtain_method_ = JNI_GetStaticMethodID(env, parcel_class_, "obtain", - "()Landroid/os/Parcel;"); - recycleMethod_ = JNI_GetMethodID(env, parcel_class_, "recycle", "()V"); - write_interface_token_method_ = JNI_GetMethodID(env, parcel_class_, "writeInterfaceToken", - "(Ljava/lang/String;)V"); - write_int_method_ = JNI_GetMethodID(env, parcel_class_, "writeInt", "(I)V"); - write_string_method_ = JNI_GetMethodID(env, parcel_class_, "writeString", - "(Ljava/lang/String;)V"); - write_strong_binder_method_ = JNI_GetMethodID(env, parcel_class_, "writeStrongBinder", - "(Landroid/os/IBinder;)V"); - read_exception_method_ = JNI_GetMethodID(env, parcel_class_, "readException", "()V"); - read_int_method_ = JNI_GetMethodID(env, parcel_class_, "readInt", "()I"); - read_long_method_ = JNI_GetMethodID(env, parcel_class_, "readLong", "()J"); - read_strong_binder_method_ = JNI_GetMethodID(env, parcel_class_, "readStrongBinder", - "()Landroid/os/IBinder;"); - read_string_method_ = JNI_GetMethodID(env, parcel_class_, "readString", - "()Ljava/lang/String;"); - read_file_descriptor_method_ = JNI_GetMethodID(env, parcel_class_, "readFileDescriptor", - "()Landroid/os/ParcelFileDescriptor;"); -// createStringArray_ = env->GetMethodID(parcel_class_, "createStringArray", -// "()[Ljava/lang/String;"); - - if (auto parcel_file_descriptor_class = JNI_FindClass(env, "android/os/ParcelFileDescriptor")) { - parcel_file_descriptor_class_ = JNI_NewGlobalRef(env, parcel_file_descriptor_class); - } else { - LOGE("ParcelFileDescriptor not found"); - return; - } - detach_fd_method_ = JNI_GetMethodID(env, parcel_file_descriptor_class_, "detachFd", "()I"); - - if (auto dead_object_exception_class = JNI_FindClass(env, - "android/os/DeadObjectException")) { - deadObjectExceptionClass_ = JNI_NewGlobalRef(env, dead_object_exception_class); - } - initialized_ = true; - } - - std::string GetBridgeServiceName() { - const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); - static auto signature = obfs_map.at("org.lsposed.lspd.service.") + "BridgeService"; - return signature; - } - - void Service::HookBridge(const Context &context, JNIEnv *env) { - static bool kHooked = false; - // This should only be ran once, so unlikely - if (kHooked) [[unlikely]] return; - if (!initialized_) [[unlikely]] return; - kHooked = true; - if (auto bridge_service_class = context.FindClassFromCurrentLoader(env, - GetBridgeServiceName())) - bridge_service_class_ = JNI_NewGlobalRef(env, bridge_service_class); - else { - LOGE("server class not found"); - return; - } - - constexpr const auto *hooker_sig = "(Landroid/os/IBinder;IJJI)Z"; - - exec_transact_replace_methodID_ = JNI_GetStaticMethodID(env, bridge_service_class_, - "execTransact", - hooker_sig); - if (!exec_transact_replace_methodID_) { - LOGE("execTransact class not found"); - return; - } - - auto &art = lspd::GetArt(); - auto binder_class = JNI_FindClass(env, "android/os/Binder"); - exec_transact_backup_methodID_ = JNI_GetMethodID(env, binder_class, "execTransact", - "(IJJI)Z"); - auto *setTableOverride = art->getSymbAddress( - "_ZN3art9JNIEnvExt16SetTableOverrideEPK18JNINativeInterface"); - if (!setTableOverride) { - LOGE("set table override not found"); - } - memcpy(&native_interface_replace_, env->functions, sizeof(JNINativeInterface)); - - call_boolean_method_va_backup_ = env->functions->CallBooleanMethodV; - native_interface_replace_.CallBooleanMethodV = &call_boolean_method_va_replace; - - if (setTableOverride != nullptr) { - setTableOverride(&native_interface_replace_); - } - if (auto activity_thread_class = JNI_FindClass(env, "android/app/IActivityManager$Stub")) { - if (auto *set_activity_controller_field = JNI_GetStaticFieldID(env, - activity_thread_class, - "TRANSACTION_setActivityController", - "I")) { - SET_ACTIVITY_CONTROLLER_CODE = JNI_GetStaticIntField(env, activity_thread_class, - set_activity_controller_field); - } - } - - auto &binder = lspd::GetLibBinder(false); - IPCThreadState::Init(binder.get()); - lspd::GetLibBinder(true); - - LOGD("Done InitService"); - } - - ScopedLocalRef Service::RequestBinder(JNIEnv *env, jstring nice_name) { - if (!initialized_) [[unlikely]] { - LOGE("Service not initialized"); - return {env, nullptr}; - } - - auto bridge_service_name = JNI_NewStringUTF(env, BRIDGE_SERVICE_NAME.data()); - auto bridge_service = JNI_CallStaticObjectMethod(env, service_manager_class_, - get_service_method_, bridge_service_name); - if (!bridge_service) { - LOGD("can't get {}", BRIDGE_SERVICE_NAME); - return {env, nullptr}; - } - - auto heart_beat_binder = JNI_NewObject(env, binder_class_, binder_ctor_); - - auto data = JNI_CallStaticObjectMethod(env, parcel_class_, obtain_method_); - auto reply = JNI_CallStaticObjectMethod(env, parcel_class_, obtain_method_); - - auto descriptor = JNI_NewStringUTF(env, BRIDGE_SERVICE_DESCRIPTOR.data()); - JNI_CallVoidMethod(env, data, write_interface_token_method_, descriptor); - JNI_CallVoidMethod(env, data, write_int_method_, BRIDGE_ACTION_GET_BINDER); - JNI_CallVoidMethod(env, data, write_string_method_, nice_name); - JNI_CallVoidMethod(env, data, write_strong_binder_method_, heart_beat_binder); - - auto res = JNI_CallBooleanMethod(env, bridge_service, transact_method_, - BRIDGE_TRANSACTION_CODE, - data, - reply, 0); - - ScopedLocalRef service = {env, nullptr}; - if (res) { - JNI_CallVoidMethod(env, reply, read_exception_method_); - service = JNI_CallObjectMethod(env, reply, read_strong_binder_method_); - } - JNI_CallVoidMethod(env, data, recycleMethod_); - JNI_CallVoidMethod(env, reply, recycleMethod_); - if (service) { - JNI_NewGlobalRef(env, heart_beat_binder); - } - - return service; - } - - ScopedLocalRef Service::RequestSystemServerBinder(JNIEnv *env) { - if (!initialized_) [[unlikely]] { - LOGE("Service not initialized"); - return {env, nullptr}; - } - // Get Binder for LSPSystemServerService. - // The binder itself was inject into system service "serial" - auto bridge_service_name = JNI_NewStringUTF(env, SYSTEM_SERVER_BRIDGE_SERVICE_NAME); - ScopedLocalRef binder{env, nullptr}; - for (int i = 0; i < 3; ++i) { - binder = JNI_CallStaticObjectMethod(env, service_manager_class_, - get_service_method_, bridge_service_name); - if (binder) { - LOGD("Got binder for system server"); - break; - } - LOGI("Fail to get binder for system server, try again in 1s"); - using namespace std::chrono_literals; - std::this_thread::sleep_for(1s); - } - if (!binder) { - LOGW("Fail to get binder for system server"); - return {env, nullptr}; - } - return binder; - } - - ScopedLocalRef Service::RequestApplicationBinderFromSystemServer(JNIEnv *env, const ScopedLocalRef &system_server_binder) { - auto heart_beat_binder = JNI_NewObject(env, binder_class_, binder_ctor_); - Wrapper wrapper{env, this}; - - JNI_CallVoidMethod(env, wrapper.data, write_int_method_, getuid()); - JNI_CallVoidMethod(env, wrapper.data, write_int_method_, getpid()); - JNI_CallVoidMethod(env, wrapper.data, write_string_method_, JNI_NewStringUTF(env, "system")); - JNI_CallVoidMethod(env, wrapper.data, write_strong_binder_method_, heart_beat_binder); - - auto res = wrapper.transact(system_server_binder, BRIDGE_TRANSACTION_CODE); - - ScopedLocalRef app_binder = {env, nullptr}; - if (res) { - JNI_CallVoidMethod(env, wrapper.reply, read_exception_method_); - app_binder = JNI_CallObjectMethod(env, wrapper.reply, read_strong_binder_method_); - } - if (app_binder) { - JNI_NewGlobalRef(env, heart_beat_binder); - } - LOGD("app_binder: {}", static_cast(app_binder.get())); - return app_binder; - } - - std::tuple Service::RequestLSPDex(JNIEnv *env, const ScopedLocalRef &binder) { - Wrapper wrapper{env, this}; - bool res = wrapper.transact(binder, DEX_TRANSACTION_CODE); - if (!res) { - LOGE("Service::RequestLSPDex: transaction failed?"); - return {-1, 0}; - } - auto parcel_fd = JNI_CallObjectMethod(env, wrapper.reply, read_file_descriptor_method_); - int fd = JNI_CallIntMethod(env, parcel_fd, detach_fd_method_); - auto size = static_cast(JNI_CallLongMethod(env, wrapper.reply, read_long_method_)); - LOGD("fd={}, size={}", fd, size); - return {fd, size}; - } - - std::map - Service::RequestObfuscationMap(JNIEnv *env, const ScopedLocalRef &binder) { - std::map ret; - Wrapper wrapper{env, this}; - bool res = wrapper.transact(binder, OBFUSCATION_MAP_TRANSACTION_CODE); - - if (!res) { - LOGE("Service::RequestObfuscationMap: transaction failed?"); - return ret; - } - auto size = JNI_CallIntMethod(env, wrapper.reply, read_int_method_); - if (!size || (size & 1) == 1) { - LOGW("Service::RequestObfuscationMap: invalid parcel size"); - } - - auto get_string = [this, &wrapper, &env]() -> std::string { - auto s = JNI_Cast(JNI_CallObjectMethod(env, wrapper.reply, read_string_method_)); - return JUTFString(s); - }; - for (auto i = 0; i < size / 2; i++) { - // DO NOT TOUCH, or value evaluates before key. - auto &&key = get_string(); - ret[key] = get_string(); - } -#ifndef NDEBUG - for (const auto &i: ret) { - LOGD("{} => {}", i.first, i.second); - } -#endif - - return ret; - } -} // namespace lspd diff --git a/magisk-loader/src/main/jni/src/service.h b/magisk-loader/src/main/jni/src/service.h deleted file mode 100644 index 670e71124..000000000 --- a/magisk-loader/src/main/jni/src/service.h +++ /dev/null @@ -1,145 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -// -// Created by loves on 2/7/2021. -// - -#ifndef LSPOSED_SERVICE_H -#define LSPOSED_SERVICE_H - -#include -#include -#include "context.h" - -using namespace std::literals::string_view_literals; - -namespace lspd { - class Service { - constexpr static jint DEX_TRANSACTION_CODE = 1310096052; - constexpr static jint OBFUSCATION_MAP_TRANSACTION_CODE = 724533732; - constexpr static jint BRIDGE_TRANSACTION_CODE = 1598837584; - constexpr static auto BRIDGE_SERVICE_DESCRIPTOR = "LSPosed"sv; - constexpr static auto BRIDGE_SERVICE_NAME = "activity"sv; - constexpr static auto SYSTEM_SERVER_BRIDGE_SERVICE_NAME = "serial"sv; - constexpr static jint BRIDGE_ACTION_GET_BINDER = 2; - inline static jint SET_ACTIVITY_CONTROLLER_CODE = -1; - - class Wrapper { - public: - Service* service_; - JNIEnv *env_; - lsplant::ScopedLocalRef data; - lsplant::ScopedLocalRef reply; - - Wrapper(JNIEnv *env, Service* service) : - service_(service), - env_(env), - data(lsplant::JNI_CallStaticObjectMethod(env, service->parcel_class_, service->obtain_method_)), - reply(lsplant::JNI_CallStaticObjectMethod(env, service->parcel_class_, service->obtain_method_)) - {} - - inline bool transact(const lsplant::ScopedLocalRef &binder, jint transaction_code) { - return JNI_CallBooleanMethod(env_, binder, service_->transact_method_,transaction_code, - data, reply, 0); - } - - inline ~Wrapper() { - JNI_CallVoidMethod(env_, data, service_->recycleMethod_); - JNI_CallVoidMethod(env_, reply, service_->recycleMethod_); - } - }; - - public: - inline static Service* instance() { - return instance_.get(); - } - - inline static std::unique_ptr ReleaseInstance() { - return std::move(instance_); - } - - void InitService(JNIEnv *env); - - void HookBridge(const Context& context, JNIEnv *env); - lsplant::ScopedLocalRef RequestBinder(JNIEnv *env, jstring nice_name); - - lsplant::ScopedLocalRef RequestSystemServerBinder(JNIEnv *env); - - lsplant::ScopedLocalRef RequestApplicationBinderFromSystemServer(JNIEnv *env, const lsplant::ScopedLocalRef &system_server_binder); - - std::tuple RequestLSPDex(JNIEnv *env, const lsplant::ScopedLocalRef &binder); - - std::map RequestObfuscationMap(JNIEnv *env, const lsplant::ScopedLocalRef &binder); - - private: - static std::unique_ptr instance_; - bool initialized_ = false; - - Service() = default; - - static jboolean - call_boolean_method_va_replace(JNIEnv *env, jobject obj, jmethodID methodId, va_list args); - - static jboolean exec_transact_replace(jboolean *res, JNIEnv *env, jobject obj, va_list args); - - JNINativeInterface native_interface_replace_{}; - jmethodID exec_transact_backup_methodID_ = nullptr; - - jboolean (*call_boolean_method_va_backup_)(JNIEnv *env, jobject obj, jmethodID methodId, - va_list args) = nullptr; - - jclass bridge_service_class_ = nullptr; - jmethodID exec_transact_replace_methodID_ = nullptr; - - jclass binder_class_ = nullptr; - jmethodID binder_ctor_ = nullptr; - - jclass service_manager_class_ = nullptr; - jmethodID get_service_method_ = nullptr; - - jmethodID transact_method_ = nullptr; - - jclass parcel_class_ = nullptr; - jmethodID data_size_method_ = nullptr; - jmethodID obtain_method_ = nullptr; - jmethodID recycleMethod_ = nullptr; - jmethodID write_interface_token_method_ = nullptr; - jmethodID write_int_method_ = nullptr; - jmethodID write_string_method_ = nullptr; - jmethodID read_exception_method_ = nullptr; - jmethodID read_strong_binder_method_ = nullptr; - jmethodID write_strong_binder_method_ = nullptr; - jmethodID read_file_descriptor_method_ = nullptr; - jmethodID read_int_method_ = nullptr; - jmethodID read_long_method_ = nullptr; - jmethodID read_string_method_ = nullptr; - - jclass parcel_file_descriptor_class_ = nullptr; - jmethodID detach_fd_method_ = nullptr; - - jclass deadObjectExceptionClass_ = nullptr; - - friend std::unique_ptr std::make_unique(); - - }; -} - - -#endif //LSPOSED_SERVICE_H diff --git a/magisk-loader/src/main/jni/template/loader.cpp b/magisk-loader/src/main/jni/template/loader.cpp deleted file mode 100644 index f4394c436..000000000 --- a/magisk-loader/src/main/jni/template/loader.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "loader.h" - -namespace lspd { -const int apiVersion = ${API_VERSION}; -const char* const moduleName = "${MODULE_NAME}"; -} diff --git a/magisk-loader/update/riru.json b/magisk-loader/update/riru.json deleted file mode 100644 index f79c99e57..000000000 --- a/magisk-loader/update/riru.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "v1.10.0", - "versionCode": 7068, - "zipUrl": "https://github.com/JingMatrix/LSPosed/releases/download/v1.10.0/LSPosed-v1.10.0-7068-riru-release.zip", - "changelog": "https://raw.githubusercontent.com/JingMatrix/LSPosed/master/magisk-loader/update/changelog.md" -} diff --git a/zygisk/zygisk.json b/zygisk/zygisk.json new file mode 100644 index 000000000..ffcd4415b --- /dev/null +++ b/zygisk/zygisk.json @@ -0,0 +1 @@ +{ } From c073d20e99038d3ac2dc00d3cf7c2038cb173d2c Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 4 Mar 2026 23:34:10 +0100 Subject: [PATCH 14/24] Remove the original jni library This part is replaced by the native module --- core/build.gradle.kts | 7 - core/src/main/jni/CMakeLists.txt | 26 - core/src/main/jni/include/config.h | 72 -- core/src/main/jni/include/config_bridge.h | 45 - core/src/main/jni/include/context.h | 130 --- core/src/main/jni/include/elf_util.h | 171 ---- .../framework/androidfw/resource_types.h | 352 -------- core/src/main/jni/include/logging.h | 60 -- core/src/main/jni/include/macros.h | 13 - core/src/main/jni/include/native_util.h | 86 -- core/src/main/jni/include/symbol_cache.h | 40 - core/src/main/jni/include/utils.h | 53 -- core/src/main/jni/src/context.cpp | 122 --- core/src/main/jni/src/elf_util.cpp | 482 ---------- core/src/main/jni/src/jni/dex_parser.cpp | 823 ------------------ core/src/main/jni/src/jni/dex_parser.h | 25 - core/src/main/jni/src/jni/hook_bridge.cpp | 361 -------- core/src/main/jni/src/jni/hook_bridge.h | 25 - core/src/main/jni/src/jni/native_api.cpp | 44 - core/src/main/jni/src/jni/native_api.h | 27 - core/src/main/jni/src/jni/resources_hook.cpp | 230 ----- core/src/main/jni/src/jni/resources_hook.h | 28 - core/src/main/jni/src/native_api.cpp | 140 --- core/src/main/jni/src/native_api.h | 80 -- core/src/main/jni/src/symbol_cache.cpp | 62 -- core/src/main/jni/template/config.cpp | 6 - 26 files changed, 3510 deletions(-) delete mode 100644 core/src/main/jni/CMakeLists.txt delete mode 100644 core/src/main/jni/include/config.h delete mode 100644 core/src/main/jni/include/config_bridge.h delete mode 100644 core/src/main/jni/include/context.h delete mode 100644 core/src/main/jni/include/elf_util.h delete mode 100644 core/src/main/jni/include/framework/androidfw/resource_types.h delete mode 100644 core/src/main/jni/include/logging.h delete mode 100644 core/src/main/jni/include/macros.h delete mode 100644 core/src/main/jni/include/native_util.h delete mode 100644 core/src/main/jni/include/symbol_cache.h delete mode 100644 core/src/main/jni/include/utils.h delete mode 100644 core/src/main/jni/src/context.cpp delete mode 100644 core/src/main/jni/src/elf_util.cpp delete mode 100644 core/src/main/jni/src/jni/dex_parser.cpp delete mode 100644 core/src/main/jni/src/jni/dex_parser.h delete mode 100644 core/src/main/jni/src/jni/hook_bridge.cpp delete mode 100644 core/src/main/jni/src/jni/hook_bridge.h delete mode 100644 core/src/main/jni/src/jni/native_api.cpp delete mode 100644 core/src/main/jni/src/jni/native_api.h delete mode 100644 core/src/main/jni/src/jni/resources_hook.cpp delete mode 100644 core/src/main/jni/src/jni/resources_hook.h delete mode 100644 core/src/main/jni/src/native_api.cpp delete mode 100644 core/src/main/jni/src/native_api.h delete mode 100644 core/src/main/jni/src/symbol_cache.cpp delete mode 100644 core/src/main/jni/template/config.cpp diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 213108021..fee240a03 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -48,13 +48,6 @@ android { } } -copy { - from("src/main/jni/template/") { - expand("VERSION_CODE" to "$verCode", "VERSION_NAME" to verName) - } - into("src/main/jni/src/") -} - dependencies { api(projects.xposed) implementation(projects.external.apache) diff --git a/core/src/main/jni/CMakeLists.txt b/core/src/main/jni/CMakeLists.txt deleted file mode 100644 index 8a51dc703..000000000 --- a/core/src/main/jni/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(core) - -set(CMAKE_CXX_STANDARD 23) - -add_subdirectory(${VECTOR_ROOT}/external external) - -aux_source_directory(src SRC_LIST) -aux_source_directory(src/jni SRC_LIST) -aux_source_directory(src/xz SRC_LIST) - -add_library(${PROJECT_NAME} STATIC ${SRC_LIST}) - -set(IGNORED_WARNINGS - -Wno-c99-extensions - -Wno-gnu-zero-variadic-macro-arguments - -Wno-gnu-flexible-array-initializer - -Wno-variadic-macros - -Wno-zero-length-array) - -target_include_directories(${PROJECT_NAME} PUBLIC include) -target_include_directories(${PROJECT_NAME} PRIVATE src ${VECTOR_ROOT}/external/xz-embedded/linux/include) -target_compile_options(${PROJECT_NAME} PRIVATE -Wpedantic ${IGNORED_WARNINGS}) - -target_link_libraries(${PROJECT_NAME} PUBLIC dobby_static lsplant_static xz_static log fmt-header-only) -target_link_libraries(${PROJECT_NAME} PRIVATE dex_builder_static) diff --git a/core/src/main/jni/include/config.h b/core/src/main/jni/include/config.h deleted file mode 100644 index c3f649f58..000000000 --- a/core/src/main/jni/include/config.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -#pragma once - -#include -#include -#include -#include "macros.h" -#include "utils.h" -#include "utils/hook_helper.hpp" - -namespace lspd { - -//#define LOG_DISABLED -//#define DEBUG - - inline bool constexpr Is64() { -#if defined(__LP64__) - return true; -#else - return false; -#endif - } - - inline constexpr bool is64 = Is64(); - - inline bool constexpr IsDebug() { -#ifdef NDEBUG - return false; -#else - return true; -#endif - } - - inline constexpr bool isDebug = IsDebug(); - -#if defined(__LP64__) -# define LP_SELECT(lp32, lp64) lp64 -#else -# define LP_SELECT(lp32, lp64) lp32 -#endif - - inline static constexpr auto kLibArtName = "libart.so"; - inline static constexpr auto kLibBinderName = "libbinder.so"; - inline static constexpr auto kLibFwName = "libandroidfw.so"; - inline static constexpr auto kLinkerName = "/linker"; - - inline constexpr const char *BoolToString(bool b) { - return b ? "true" : "false"; - } - - extern const int versionCode; - extern const char* const versionName; -} diff --git a/core/src/main/jni/include/config_bridge.h b/core/src/main/jni/include/config_bridge.h deleted file mode 100644 index b24d4fec2..000000000 --- a/core/src/main/jni/include/config_bridge.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ -#pragma once - -#include - -namespace lspd { - using obfuscation_map_t = std::map; - - class ConfigBridge { - public: - inline static ConfigBridge *GetInstance() { - return instance_.get(); - } - - inline static std::unique_ptr ReleaseInstance() { - return std::move(instance_); - } - - virtual obfuscation_map_t &obfuscation_map() = 0; - - virtual void obfuscation_map(obfuscation_map_t) = 0; - - virtual ~ConfigBridge() = default; - - protected: - static std::unique_ptr instance_; - }; -} diff --git a/core/src/main/jni/include/context.h b/core/src/main/jni/include/context.h deleted file mode 100644 index ca56bd02d..000000000 --- a/core/src/main/jni/include/context.h +++ /dev/null @@ -1,130 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include "utils.h" -#include "utils/jni_helper.hpp" - -namespace lspd { - class Context { - - public: - inline static Context *GetInstance() { - return instance_.get(); - } - - inline static std::unique_ptr ReleaseInstance() { - return std::move(instance_); - } - - inline jobject GetCurrentClassLoader() const { return inject_class_loader_; } - - inline lsplant::ScopedLocalRef - FindClassFromCurrentLoader(JNIEnv *env, std::string_view className) const { - return FindClassFromLoader(env, GetCurrentClassLoader(), className); - }; - - virtual ~Context() = default; - - protected: - static std::unique_ptr instance_; - jobject inject_class_loader_ = nullptr; - jclass entry_class_ = nullptr; - - struct PreloadedDex { - - PreloadedDex() : addr_(nullptr), size_(0) {} - - PreloadedDex(const PreloadedDex &) = delete; - - PreloadedDex &operator=(const PreloadedDex &) = delete; - - PreloadedDex(int fd, std::size_t size); - - PreloadedDex &operator=(PreloadedDex &&other) { - addr_ = other.addr_; - size_ = other.size_; - other.addr_ = nullptr; - other.size_ = 0; - return *this; - } - - PreloadedDex(PreloadedDex &&other) : addr_(other.addr_), size_(other.size_) { - other.addr_ = nullptr; - other.size_ = 0; - }; - - // Use with caution! - PreloadedDex(void *addr, size_t size) : addr_(addr), size_(size) {}; - - operator bool() const { return addr_ && size_; } - - auto size() const { return size_; } - - auto data() const { return addr_; } - - ~PreloadedDex(); - - private: - void *addr_; - std::size_t size_; - }; - - Context() {} - - static lsplant::ScopedLocalRef FindClassFromLoader(JNIEnv *env, jobject class_loader, - std::string_view class_name); - - template - inline void FindAndCall(JNIEnv *env, std::string_view method_name, std::string_view method_sig, - Args &&... args) const { - if (!entry_class_) [[unlikely]] { - LOGE("cannot call method {}, entry class is null", method_name); - return; - } - jmethodID mid = lsplant::JNI_GetStaticMethodID(env, entry_class_, method_name, method_sig); - if (mid) [[likely]] { - env->CallStaticVoidMethod(entry_class_, mid, lsplant::UnwrapScope(std::forward(args))...); - } else { - LOGE("method {} id is null", method_name); - } - } - - virtual void InitArtHooker(JNIEnv *env, const lsplant::InitInfo &initInfo); - - virtual void InitHooks(JNIEnv *env); - - virtual void LoadDex(JNIEnv *env, PreloadedDex &&dex) = 0; - - virtual void SetupEntryClass(JNIEnv *env) = 0; - - private: - friend std::unique_ptr std::make_unique(); - }; - -} diff --git a/core/src/main/jni/include/elf_util.h b/core/src/main/jni/include/elf_util.h deleted file mode 100644 index f3faa9363..000000000 --- a/core/src/main/jni/include/elf_util.h +++ /dev/null @@ -1,171 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2019 Swift Gan - * Copyright (C) 2021 LSPosed Contributors - */ -#ifndef SANDHOOK_ELF_UTIL_H -#define SANDHOOK_ELF_UTIL_H - -#include -#include -#include -#include -#include -#include -#include "config.h" - -#define SHT_GNU_HASH 0x6ffffff6 - -namespace SandHook { - class ElfImg { - public: - - ElfImg(std::string_view elf); - - template - requires(std::is_pointer_v) - constexpr const T getSymbAddress(std::string_view name) const { - auto offset = getSymbOffset(name, GnuHash(name), ElfHash(name)); - if (offset > 0 && base != nullptr) { - return reinterpret_cast(static_cast((uintptr_t) base + offset - bias)); - } else { - return nullptr; - } - } - - template - requires(std::is_pointer_v) - constexpr const T getSymbPrefixFirstAddress(std::string_view prefix) const { - auto offset = PrefixLookupFirst(prefix); - if (offset > 0 && base != nullptr) { - return reinterpret_cast(static_cast((uintptr_t) base + offset - bias)); - } else { - return nullptr; - } - } - - template - requires(std::is_pointer_v) - const std::vector getAllSymbAddress(std::string_view name) const { - auto offsets = LinearRangeLookup(name); - std::vector res; - res.reserve(offsets.size()); - for (const auto &offset : offsets) { - res.emplace_back(reinterpret_cast(static_cast((uintptr_t) base + offset - bias))); - } - return res; - } - - bool isValid() const { - return base != nullptr; - } - - bool isStripped() const { - return debugdata_offset != 0 && debugdata_size != 0; - } - - const std::string name() const { - return elf; - } - - ~ElfImg(); - - private: - ElfW(Addr) getSymbOffset(std::string_view name, uint32_t gnu_hash, uint32_t elf_hash) const; - - ElfW(Addr) ElfLookup(std::string_view name, uint32_t hash) const; - - ElfW(Addr) GnuLookup(std::string_view name, uint32_t hash) const; - - ElfW(Addr) LinearLookup(std::string_view name) const; - - std::vector LinearRangeLookup(std::string_view name) const; - - ElfW(Addr) PrefixLookupFirst(std::string_view prefix) const; - - constexpr static uint32_t ElfHash(std::string_view name); - - constexpr static uint32_t GnuHash(std::string_view name); - - bool findModuleBase(); - - void MayInitLinearMap() const; - - void parse(ElfW(Ehdr) *header); - - bool xzdecompress(); - - std::string elf; - void *base = nullptr; - char *buffer = nullptr; - off_t size = 0; - off_t bias = -4396; - ElfW(Ehdr) *header = nullptr; - ElfW(Ehdr) *header_debugdata = nullptr; - ElfW(Shdr) *section_header = nullptr; - ElfW(Shdr) *symtab = nullptr; - ElfW(Shdr) *strtab = nullptr; - ElfW(Shdr) *dynsym = nullptr; - ElfW(Sym) *symtab_start = nullptr; - ElfW(Sym) *dynsym_start = nullptr; - ElfW(Sym) *strtab_start = nullptr; - ElfW(Off) symtab_count = 0; - ElfW(Off) symstr_offset = 0; - ElfW(Off) symstr_offset_for_symtab = 0; - ElfW(Off) symtab_offset = 0; - ElfW(Off) dynsym_offset = 0; - ElfW(Off) symtab_size = 0; - ElfW(Off) debugdata_offset = 0; - ElfW(Off) debugdata_size = 0; - std::string elf_debugdata; - - uint32_t nbucket_{}; - uint32_t *bucket_ = nullptr; - uint32_t *chain_ = nullptr; - - uint32_t gnu_nbucket_{}; - uint32_t gnu_symndx_{}; - uint32_t gnu_bloom_size_; - uint32_t gnu_shift2_; - uintptr_t *gnu_bloom_filter_; - uint32_t *gnu_bucket_; - uint32_t *gnu_chain_; - - mutable std::map symtabs_; - }; - - constexpr uint32_t ElfImg::ElfHash(std::string_view name) { - uint32_t h = 0, g; - for (unsigned char p: name) { - h = (h << 4) + p; - g = h & 0xf0000000; - h ^= g; - h ^= g >> 24; - } - return h; - } - - constexpr uint32_t ElfImg::GnuHash(std::string_view name) { - uint32_t h = 5381; - for (unsigned char p: name) { - h += (h << 5) + p; - } - return h; - } -} - -#endif //SANDHOOK_ELF_UTIL_H diff --git a/core/src/main/jni/include/framework/androidfw/resource_types.h b/core/src/main/jni/include/framework/androidfw/resource_types.h deleted file mode 100644 index 8f9fd6ab3..000000000 --- a/core/src/main/jni/include/framework/androidfw/resource_types.h +++ /dev/null @@ -1,352 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -#pragma once - -#include -#include -#include "utils/hook_helper.hpp" - -using lsplant::operator""_sym; - -// @ApiSensitive(Level.MIDDLE) -namespace android { - - typedef int32_t status_t; - - - template - struct unexpected { - E val_; - }; - - template - struct expected { - using value_type = T; - using error_type = E; - using unexpected_type = unexpected; - std::variant var_; - - constexpr bool has_value() const noexcept { return var_.index() == 0; } - - constexpr const T &value() const &{ return std::get(var_); } - - constexpr T &value() &{ return std::get(var_); } - - constexpr const T *operator->() const { return std::addressof(value()); } - - constexpr T *operator->() { return std::addressof(value()); } - }; - - enum class IOError { - // Used when reading a file residing on an IncFs file-system times out. - PAGES_MISSING = -1, - }; - - template - struct BasicStringPiece { - const TChar *data_; - size_t length_; - }; - - using NullOrIOError = std::variant; - - using StringPiece16 = BasicStringPiece; - - enum { - RES_NULL_TYPE = 0x0000, - RES_STRING_POOL_TYPE = 0x0001, - RES_TABLE_TYPE = 0x0002, - RES_XML_TYPE = 0x0003, - // Chunk types in RES_XML_TYPE - RES_XML_FIRST_CHUNK_TYPE = 0x0100, - RES_XML_START_NAMESPACE_TYPE = 0x0100, - RES_XML_END_NAMESPACE_TYPE = 0x0101, - RES_XML_START_ELEMENT_TYPE = 0x0102, - RES_XML_END_ELEMENT_TYPE = 0x0103, - RES_XML_CDATA_TYPE = 0x0104, - RES_XML_LAST_CHUNK_TYPE = 0x017f, - // This contains a uint32_t array mapping strings in the string - // pool back to resource identifiers. It is optional. - RES_XML_RESOURCE_MAP_TYPE = 0x0180, - // Chunk types in RES_TABLE_TYPE - RES_TABLE_PACKAGE_TYPE = 0x0200, - RES_TABLE_TYPE_TYPE = 0x0201, - RES_TABLE_TYPE_SPEC_TYPE = 0x0202, - RES_TABLE_LIBRARY_TYPE = 0x0203 - }; - - struct ResXMLTree_node { - void *header; - // Line number in original source file at which this element appeared. - uint32_t lineNumber; - // Optional XML comment that was associated with this element; -1 if none. - void *comment; - }; - - class ResXMLTree; - - class ResXMLParser { - - public: - enum event_code_t { - BAD_DOCUMENT = -1, - START_DOCUMENT = 0, - END_DOCUMENT = 1, - - FIRST_CHUNK_CODE = RES_XML_FIRST_CHUNK_TYPE, - - START_NAMESPACE = RES_XML_START_NAMESPACE_TYPE, - END_NAMESPACE = RES_XML_END_NAMESPACE_TYPE, - START_TAG = RES_XML_START_ELEMENT_TYPE, - END_TAG = RES_XML_END_ELEMENT_TYPE, - TEXT = RES_XML_CDATA_TYPE - }; - - const ResXMLTree &mTree; - event_code_t mEventCode; - const ResXMLTree_node *mCurNode; - const void *mCurExt; - }; - - class ResStringPool { - - public: - status_t mError; - void *mOwnedData; - const void *mHeader; - size_t mSize; - mutable pthread_mutex_t mDecodeLock; - const uint32_t *mEntries; - const uint32_t *mEntryStyles; - const void *mStrings; - char16_t mutable **mCache; - uint32_t mStringPoolSize; // number of uint16_t - const uint32_t *mStyles; - uint32_t mStylePoolSize; // number of uint32_t - - using stringAtRet = expected; - - inline static auto stringAtS_ = ("_ZNK7android13ResStringPool8stringAtEjPj"_sym | - "_ZNK7android13ResStringPool8stringAtEmPm"_sym).as; - - inline static auto stringAt_ = ("_ZNK7android13ResStringPool8stringAtEj"_sym | - "_ZNK7android13ResStringPool8stringAtEm"_sym).as; - - StringPiece16 stringAt(size_t idx) const { - if (stringAt_) { - size_t len; - const char16_t *str = stringAt_(const_cast(this), idx, &len); - return {str, len}; - } else if (stringAtS_) { - auto str = stringAtS_(const_cast(this), idx); - if (str.has_value()) { - return {str->data_, str->length_}; - } - } - return {nullptr, 0u}; - } - - static bool setup(const lsplant::HookHandler &handler) { - return handler(stringAt_) || handler(stringAtS_); - } - }; - - - class ResXMLTree : public ResXMLParser { - - public: - void *mDynamicRefTable; - status_t mError; - void *mOwnedData; - const void *mHeader; - size_t mSize; - const uint8_t *mDataEnd; - ResStringPool mStrings; - const uint32_t *mResIds; - size_t mNumResIds; - const ResXMLTree_node *mRootNode; - const void *mRootExt; - event_code_t mRootCode; - }; - - struct ResStringPool_ref { - - // Index into the string pool table (uint32_t-offset from the indices - // immediately after ResStringPool_header) at which to find the location - // of the string data in the pool. - uint32_t index; - }; - - struct ResXMLTree_attrExt { - - // String of the full namespace of this element. - struct ResStringPool_ref ns; - - // String name of this node if it is an ELEMENT; the raw - // character data if this is a CDATA node. - struct ResStringPool_ref name; - - // Byte offset from the start of this structure where the attributes start. - uint16_t attributeStart; - - // Size of the ResXMLTree_attribute structures that follow. - uint16_t attributeSize; - - // Number of attributes associated with an ELEMENT. These are - // available as an array of ResXMLTree_attribute structures - // immediately following this node. - uint16_t attributeCount; - - // Index (1-based) of the "id" attribute. 0 if none. - uint16_t idIndex; - - // Index (1-based) of the "class" attribute. 0 if none. - uint16_t classIndex; - - // Index (1-based) of the "style" attribute. 0 if none. - uint16_t styleIndex; - }; - - struct Res_value { - - // Number of bytes in this structure. - uint16_t size; - // Always set to 0. - uint8_t res0; - - // Type of the data value. - enum : uint8_t { - // The 'data' is either 0 or 1, specifying this resource is either - // undefined or empty, respectively. - TYPE_NULL = 0x00, - // The 'data' holds a ResTable_ref, a reference to another resource - // table entry. - TYPE_REFERENCE = 0x01, - // The 'data' holds an attribute resource identifier. - TYPE_ATTRIBUTE = 0x02, - // The 'data' holds an index into the containing resource table's - // global value string pool. - TYPE_STRING = 0x03, - // The 'data' holds a single-precision floating point number. - TYPE_FLOAT = 0x04, - // The 'data' holds a complex number encoding a dimension value, - // such as "100in". - TYPE_DIMENSION = 0x05, - // The 'data' holds a complex number encoding a fraction of a - // container. - TYPE_FRACTION = 0x06, - // The 'data' holds a dynamic ResTable_ref, which needs to be - // resolved before it can be used like a TYPE_REFERENCE. - TYPE_DYNAMIC_REFERENCE = 0x07, - // The 'data' holds an attribute resource identifier, which needs to be resolved - // before it can be used like a TYPE_ATTRIBUTE. - TYPE_DYNAMIC_ATTRIBUTE = 0x08, - // Beginning of integer flavors... - TYPE_FIRST_INT = 0x10, - // The 'data' is a raw integer value of the form n..n. - TYPE_INT_DEC = 0x10, - // The 'data' is a raw integer value of the form 0xn..n. - TYPE_INT_HEX = 0x11, - // The 'data' is either 0 or 1, for input "false" or "true" respectively. - TYPE_INT_BOOLEAN = 0x12, - // Beginning of color integer flavors... - TYPE_FIRST_COLOR_INT = 0x1c, - // The 'data' is a raw integer value of the form #aarrggbb. - TYPE_INT_COLOR_ARGB8 = 0x1c, - // The 'data' is a raw integer value of the form #rrggbb. - TYPE_INT_COLOR_RGB8 = 0x1d, - // The 'data' is a raw integer value of the form #argb. - TYPE_INT_COLOR_ARGB4 = 0x1e, - // The 'data' is a raw integer value of the form #rgb. - TYPE_INT_COLOR_RGB4 = 0x1f, - // ...end of integer flavors. - TYPE_LAST_COLOR_INT = 0x1f, - // ...end of integer flavors. - TYPE_LAST_INT = 0x1f - }; - uint8_t dataType; - // Structure of complex data values (TYPE_UNIT and TYPE_FRACTION) - enum { - // Where the unit type information is. This gives us 16 possible - // types, as defined below. - COMPLEX_UNIT_SHIFT = 0, - COMPLEX_UNIT_MASK = 0xf, - // TYPE_DIMENSION: Value is raw pixels. - COMPLEX_UNIT_PX = 0, - // TYPE_DIMENSION: Value is Device Independent Pixels. - COMPLEX_UNIT_DIP = 1, - // TYPE_DIMENSION: Value is a Scaled device independent Pixels. - COMPLEX_UNIT_SP = 2, - // TYPE_DIMENSION: Value is in points. - COMPLEX_UNIT_PT = 3, - // TYPE_DIMENSION: Value is in inches. - COMPLEX_UNIT_IN = 4, - // TYPE_DIMENSION: Value is in millimeters. - COMPLEX_UNIT_MM = 5, - // TYPE_FRACTION: A basic fraction of the overall size. - COMPLEX_UNIT_FRACTION = 0, - // TYPE_FRACTION: A fraction of the parent size. - COMPLEX_UNIT_FRACTION_PARENT = 1, - // Where the radix information is, telling where the decimal place - // appears in the mantissa. This give us 4 possible fixed point - // representations as defined below. - COMPLEX_RADIX_SHIFT = 4, - COMPLEX_RADIX_MASK = 0x3, - // The mantissa is an integral number -- i.e., 0xnnnnnn.0 - COMPLEX_RADIX_23p0 = 0, - // The mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn - COMPLEX_RADIX_16p7 = 1, - // The mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn - COMPLEX_RADIX_8p15 = 2, - // The mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn - COMPLEX_RADIX_0p23 = 3, - // Where the actual value is. This gives us 23 bits of - // precision. The top bit is the sign. - COMPLEX_MANTISSA_SHIFT = 8, - COMPLEX_MANTISSA_MASK = 0xffffff - }; - // Possible data values for TYPE_NULL. - enum { - // The value is not defined. - DATA_NULL_UNDEFINED = 0, - // The value is explicitly defined as empty. - DATA_NULL_EMPTY = 1 - }; - // The data for this item, as interpreted according to dataType. - typedef uint32_t data_type; - data_type data; - }; - - struct ResXMLTree_attribute { - // Namespace of this attribute. - struct ResStringPool_ref ns; - - // Name of this attribute. - struct ResStringPool_ref name; - - // The original raw string value of this attribute. - struct ResStringPool_ref rawValue; - - // Processesd typed value of this attribute. - struct Res_value typedValue; - }; - -} diff --git a/core/src/main/jni/include/logging.h b/core/src/main/jni/include/logging.h deleted file mode 100644 index 76b96b095..000000000 --- a/core/src/main/jni/include/logging.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -#ifndef _LOGGING_H -#define _LOGGING_H - -#include -#include -#include - -#ifndef LOG_TAG -#define LOG_TAG "LSPosed" -#endif - -#ifdef LOG_DISABLED -#define LOGD(...) 0 -#define LOGV(...) 0 -#define LOGI(...) 0 -#define LOGW(...) 0 -#define LOGE(...) 0 -#else -template -constexpr inline void LOG(int prio, const char* tag, fmt::format_string fmt, T&&... args) { - std::array buf{}; - auto s = fmt::format_to_n(buf.data(), buf.size(), fmt, std::forward(args)...).size; - buf[s] = '\0'; - __android_log_write(prio, tag, buf.data()); -} -#ifndef NDEBUG -#define LOGD(fmt, ...) LOG(ANDROID_LOG_DEBUG, LOG_TAG, "{}:{}#{}" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) -#define LOGV(fmt, ...) LOG(ANDROID_LOG_VERBOSE, LOG_TAG, "{}:{}#{}" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) -#else -#define LOGD(...) 0 -#define LOGV(...) 0 -#endif -#define LOGI(...) LOG(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) -#define LOGW(...) LOG(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) -#define LOGE(...) LOG(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) -#define LOGF(...) LOG(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__) -#define PLOGE(fmt, args...) LOGE(fmt " failed with {}: {}", ##args, errno, strerror(errno)) -#endif - -#endif // _LOGGING_H diff --git a/core/src/main/jni/include/macros.h b/core/src/main/jni/include/macros.h deleted file mode 100644 index e8b96ded2..000000000 --- a/core/src/main/jni/include/macros.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include // for size_t -#include // for TEMP_FAILURE_RETRY -#include -// The arraysize(arr) macro returns the # of elements in an array arr. -// The expression is a compile-time constant, and therefore can be -// used in defining new arrays, for example. If you use arraysize on -// a pointer by mistake, you will get a compile-time error. -template -[[gnu::always_inline]] constexpr inline size_t arraysize(T(&)[N]) { - return N; -} diff --git a/core/src/main/jni/include/native_util.h b/core/src/main/jni/include/native_util.h deleted file mode 100644 index dbd28dcac..000000000 --- a/core/src/main/jni/include/native_util.h +++ /dev/null @@ -1,86 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -#include -#include - -#include "elf_util.h" -#include "symbol_cache.h" -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-value" -#pragma once - -#include - -#include - -#include "../src/native_api.h" -#include "config.h" -#include "config_bridge.h" -#include "logging.h" -#include "utils/jni_helper.hpp" - -namespace lspd { - -[[gnu::always_inline]] -inline bool RegisterNativeMethodsInternal(JNIEnv *env, std::string_view class_name, - const JNINativeMethod *methods, jint method_count) { - auto clazz = Context::GetInstance()->FindClassFromCurrentLoader(env, class_name.data()); - if (clazz.get() == nullptr) { - LOGF("Couldn't find class: {}", class_name.data()); - return false; - } - return JNI_RegisterNatives(env, clazz, methods, method_count); -} - -#if defined(__cplusplus) -#define _NATIVEHELPER_JNI_MACRO_CAST(to) reinterpret_cast -#else -#define _NATIVEHELPER_JNI_MACRO_CAST(to) (to) -#endif - -#ifndef LSP_NATIVE_METHOD -#define LSP_NATIVE_METHOD(className, functionName, signature) \ - {#functionName, signature, \ - _NATIVEHELPER_JNI_MACRO_CAST(void *)( \ - Java_org_lsposed_lspd_nativebridge_##className##_##functionName)} -#endif - -#define JNI_START [[maybe_unused]] JNIEnv *env, [[maybe_unused]] jclass clazz - -#ifndef LSP_DEF_NATIVE_METHOD -#define LSP_DEF_NATIVE_METHOD(ret, className, functionName, ...) \ - extern "C" ret Java_org_lsposed_lspd_nativebridge_##className##_##functionName(JNI_START, \ - ##__VA_ARGS__) -#endif - -#define REGISTER_LSP_NATIVE_METHODS(class_name) \ - RegisterNativeMethodsInternal(env, GetNativeBridgeSignature() + #class_name, gMethods, \ - arraysize(gMethods)) - -inline std::string GetNativeBridgeSignature() { - const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); - static auto signature = obfs_map.at("org.lsposed.lspd.nativebridge."); - return signature; -} - -} // namespace lspd - -#pragma clang diagnostic pop diff --git a/core/src/main/jni/include/symbol_cache.h b/core/src/main/jni/include/symbol_cache.h deleted file mode 100644 index fae57f885..000000000 --- a/core/src/main/jni/include/symbol_cache.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -// -// Created by kotori on 2/7/21. -// - -#ifndef LSPOSED_SYMBOL_CACHE_H -#define LSPOSED_SYMBOL_CACHE_H - -#include - -namespace SandHook { - class ElfImg; -} - -namespace lspd { - std::unique_ptr &GetArt(bool release=false); - std::unique_ptr &GetLibBinder(bool release=false); - std::unique_ptr &GetLinker(bool release=false); -} - -#endif //LSPOSED_SYMBOL_CACHE_H diff --git a/core/src/main/jni/include/utils.h b/core/src/main/jni/include/utils.h deleted file mode 100644 index 37ffe2e9a..000000000 --- a/core/src/main/jni/include/utils.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -#pragma once - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wgnu-string-literal-operator-template" - -#include -#include -#include -#include -#include -#include "logging.h" - -namespace lspd { - using namespace std::literals::string_literals; - - inline int32_t GetAndroidApiLevel() { - static int32_t api_level = []() { - char prop_value[PROP_VALUE_MAX]; - __system_property_get("ro.build.version.sdk", prop_value); - int base = atoi(prop_value); - __system_property_get("ro.build.version.preview_sdk", prop_value); - return base + atoi(prop_value); - }(); - return api_level; - } - - inline std::string JavaNameToSignature(std::string s) { - std::replace(s.begin(), s.end(), '.', '/'); - return "L" + s; - } -} - -#pragma clang diagnostic pop diff --git a/core/src/main/jni/src/context.cpp b/core/src/main/jni/src/context.cpp deleted file mode 100644 index 1efb436b7..000000000 --- a/core/src/main/jni/src/context.cpp +++ /dev/null @@ -1,122 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -#include - -#include "config.h" -#include "context.h" -#include "native_util.h" -#include "jni/hook_bridge.h" -#include "jni/native_api.h" -#include "jni/resources_hook.h" -#include "jni/dex_parser.h" -#include "symbol_cache.h" - -using namespace lsplant; - - -namespace lspd { - std::unique_ptr Context::instance_; - std::unique_ptr ConfigBridge::instance_; - - Context::PreloadedDex::PreloadedDex(int fd, std::size_t size) { - LOGD("Context::PreloadedDex::PreloadedDex: fd={}, size={}", fd, size); - auto *addr = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0); - - if (addr != MAP_FAILED) { - addr_ = addr; - size_ = size; - } else { - PLOGE("Read dex"); - } - } - - Context::PreloadedDex::~PreloadedDex() { - if (*this) munmap(addr_, size_); - } - - void Context::InitArtHooker(JNIEnv *env, const lsplant::InitInfo &initInfo) { - if (!lsplant::Init(env, initInfo)) { - LOGE("Failed to init lsplant"); - return; - } - } - - void Context::InitHooks(JNIEnv *env) { - auto path_list = JNI_GetObjectFieldOf(env, inject_class_loader_, "pathList", - "Ldalvik/system/DexPathList;"); - if (!path_list) { - LOGE("Failed to get path list"); - return; - } - const auto elements = JNI_Cast( - JNI_GetObjectFieldOf(env, path_list, "dexElements", - "[Ldalvik/system/DexPathList$Element;")); - if (!elements) { - LOGE("Failed to get elements"); - return; - } - for (const auto &element: elements) { - if (!element) - continue; - auto java_dex_file = JNI_GetObjectFieldOf(env, element, "dexFile", - "Ldalvik/system/DexFile;"); - if (!java_dex_file) { - LOGE("Failed to get java dex file"); - return; - } - auto cookie = JNI_GetObjectFieldOf(env, java_dex_file, "mCookie", "Ljava/lang/Object;"); - if (!cookie) { - LOGE("Failed to get cookie"); - return; - } - lsplant::MakeDexFileTrusted(env, cookie.get()); - } - RegisterResourcesHook(env); - RegisterHookBridge(env); - RegisterNativeAPI(env); - RegisterDexParserBridge(env); - } - - ScopedLocalRef - Context::FindClassFromLoader(JNIEnv *env, jobject class_loader, - std::string_view class_name) { - if (class_loader == nullptr) return {env, nullptr}; - static auto clz = JNI_NewGlobalRef(env, - JNI_FindClass(env, "dalvik/system/DexClassLoader")); - static jmethodID mid = JNI_GetMethodID(env, clz, "loadClass", - "(Ljava/lang/String;)Ljava/lang/Class;"); - if (!mid) { - mid = JNI_GetMethodID(env, clz, "findClass", - "(Ljava/lang/String;)Ljava/lang/Class;"); - } - if (mid) [[likely]] { - auto target = JNI_CallObjectMethod(env, class_loader, mid, - JNI_NewStringUTF(env, class_name.data())); - if (target) { - return target; - } - } else { - LOGE("No loadClass/findClass method found"); - } - LOGE("Class {} not found", class_name); - return {env, nullptr}; - } -} // namespace lspd diff --git a/core/src/main/jni/src/elf_util.cpp b/core/src/main/jni/src/elf_util.cpp deleted file mode 100644 index ed0cf13ee..000000000 --- a/core/src/main/jni/src/elf_util.cpp +++ /dev/null @@ -1,482 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2019 Swift Gan - * Copyright (C) 2021 LSPosed Contributors - */ -#include "elf_util.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "linux/xz.h" -#include "logging.h" - -using namespace SandHook; - -template -inline constexpr auto offsetOf(ElfW(Ehdr) * head, ElfW(Off) off) { - return reinterpret_cast, T, T *>>( - reinterpret_cast(head) + off); -} - -ElfImg::ElfImg(std::string_view base_name) : elf(base_name) { - if (!findModuleBase()) { - base = nullptr; - return; - } - - // load elf - int fd = open(elf.data(), O_RDONLY); - if (fd < 0) { - LOGE("failed to open {}", elf); - return; - } - - size = lseek(fd, 0, SEEK_END); - if (size <= 0) { - LOGE("lseek() failed for {}", elf); - } - - header = reinterpret_cast(mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0)); - - close(fd); - parse(header); - if (isStripped()) { - if (xzdecompress()) { - header_debugdata = reinterpret_cast(elf_debugdata.data()); - parse(header_debugdata); - } - } -} - -void ElfImg::parse(ElfW(Ehdr) * hdr) { - section_header = offsetOf(hdr, hdr->e_shoff); - - auto shoff = reinterpret_cast(section_header); - char *section_str = offsetOf(hdr, section_header[hdr->e_shstrndx].sh_offset); - - for (int i = 0; i < hdr->e_shnum; i++, shoff += hdr->e_shentsize) { - auto *section_h = (ElfW(Shdr) *)shoff; - char *sname = section_h->sh_name + section_str; - auto entsize = section_h->sh_entsize; - switch (section_h->sh_type) { - case SHT_DYNSYM: { - if (bias == -4396) { - dynsym = section_h; - dynsym_offset = section_h->sh_offset; - dynsym_start = offsetOf(hdr, dynsym_offset); - LOGD("dynsym header {:#x} size {}", section_h->sh_offset, section_h->sh_size); - } - break; - } - case SHT_SYMTAB: { - if (strcmp(sname, ".symtab") == 0) { - symtab = section_h; - symtab_offset = section_h->sh_offset; - symtab_size = section_h->sh_size; - symtab_count = symtab_size / entsize; - symtab_start = offsetOf(hdr, symtab_offset); - LOGD("symtab header {:#x} size {} found in {}", section_h->sh_offset, - section_h->sh_size, debugdata_offset != 0 ? "gnu_debugdata" : "orgin elf"); - } - break; - } - case SHT_STRTAB: { - if (bias == -4396) { - strtab = section_h; - symstr_offset = section_h->sh_offset; - strtab_start = offsetOf(hdr, symstr_offset); - LOGD("strtab header {:#x} size {}", section_h->sh_offset, section_h->sh_size); - } - if (strcmp(sname, ".strtab") == 0) { - symstr_offset_for_symtab = section_h->sh_offset; - } - break; - } - case SHT_PROGBITS: { - if (strcmp(sname, ".gnu_debugdata") == 0) { - debugdata_offset = section_h->sh_offset; - debugdata_size = section_h->sh_size; - LOGD("gnu_debugdata header {:#x} size {}", section_h->sh_offset, - section_h->sh_size); - } - if (strtab == nullptr || dynsym == nullptr) break; - if (bias == -4396) { - bias = (off_t)section_h->sh_addr - (off_t)section_h->sh_offset; - } - break; - } - case SHT_HASH: { - auto *d_un = offsetOf(hdr, section_h->sh_offset); - nbucket_ = d_un[0]; - bucket_ = d_un + 2; - chain_ = bucket_ + nbucket_; - break; - } - case SHT_GNU_HASH: { - auto *d_buf = reinterpret_cast(((size_t)hdr) + section_h->sh_offset); - gnu_nbucket_ = d_buf[0]; - gnu_symndx_ = d_buf[1]; - gnu_bloom_size_ = d_buf[2]; - gnu_shift2_ = d_buf[3]; - gnu_bloom_filter_ = reinterpret_cast(d_buf + 4); - gnu_bucket_ = - reinterpret_cast(gnu_bloom_filter_ + gnu_bloom_size_); - gnu_chain_ = gnu_bucket_ + gnu_nbucket_ - gnu_symndx_; - break; - } - } - } -} - -bool ElfImg::xzdecompress() { - struct xz_buf str_xz_buf; - struct xz_dec *str_xz_dec; - enum xz_ret ret = XZ_OK; - bool bError = true; - -#define BUFSIZE 1024 * 1024 - - xz_crc32_init(); -#ifdef XZ_USE_CRC64 - xz_crc64_init(); -#endif - str_xz_dec = xz_dec_init(XZ_DYNALLOC, 1 << 26); - if (str_xz_dec == NULL) { - LOGE("xz_dec_init memory allocation failed"); - return false; - } - - uint8_t *sBuffOut = (uint8_t *)malloc(BUFSIZE); - if (sBuffOut == NULL) { - LOGE("allocation for debugdata_header failed"); - return false; - } - - int iSzOut = BUFSIZE; - - str_xz_buf.in = ((uint8_t *)header) + debugdata_offset; - str_xz_buf.in_pos = 0; - str_xz_buf.in_size = debugdata_size; - str_xz_buf.out = sBuffOut; - str_xz_buf.out_pos = 0; - str_xz_buf.out_size = BUFSIZE; - - uint8_t iSkip = 0; - - while (true) { - ret = xz_dec_run(str_xz_dec, &str_xz_buf); - - if (str_xz_buf.out_pos == BUFSIZE) { - str_xz_buf.out_pos = 0; - iSkip++; - } else { - iSzOut -= (BUFSIZE - str_xz_buf.out_pos); - } - - if (ret == XZ_OK) { - iSzOut += BUFSIZE; - sBuffOut = (uint8_t *)realloc(sBuffOut, iSzOut); - str_xz_buf.out = sBuffOut + (iSkip * BUFSIZE); - continue; - } - -#ifdef XZ_DEC_ANY_CHECK - if (ret == XZ_UNSUPPORTED_CHECK) { - LOGW("Unsupported check; not verifying file integrity"); - continue; - } -#endif - break; - } // end while true - - switch (ret) { - case XZ_STREAM_END: - bError = false; - break; - - case XZ_MEM_ERROR: - LOGE("Memory allocation failed"); - break; - - case XZ_MEMLIMIT_ERROR: - LOGE("Memory usage limit reached"); - break; - - case XZ_FORMAT_ERROR: - LOGE("Not a .xz file"); - break; - - case XZ_OPTIONS_ERROR: - LOGE("Unsupported options in the .xz headers"); - break; - - case XZ_DATA_ERROR: - LOGE("Compressed data is corrupt"); - break; - - case XZ_BUF_ERROR: - LOGE("xz_dec_run failed with XZ_BUF_ERROR"); - break; - - default: - LOGE("xz_dec_run return a wrong value!"); - break; - } - xz_dec_end(str_xz_dec); - if (bError) { - return false; - } - if (sBuffOut[0] != 0x7F && sBuffOut[1] != 0x45 && sBuffOut[2] != 0x4C && sBuffOut[3] != 0x46) { - LOGE("not ELF header in gnu_debugdata"); - return false; - } - elf_debugdata = std::string((char *)sBuffOut, iSzOut); - free(sBuffOut); - return true; -} - -ElfW(Addr) ElfImg::ElfLookup(std::string_view name, uint32_t hash) const { - if (nbucket_ == 0) return 0; - - char *strings = (char *)strtab_start; - - for (auto n = bucket_[hash % nbucket_]; n != 0; n = chain_[n]) { - auto *sym = dynsym_start + n; - if (name == strings + sym->st_name) { - return sym->st_value; - } - } - return 0; -} - -ElfW(Addr) ElfImg::GnuLookup(std::string_view name, uint32_t hash) const { - static constexpr auto bloom_mask_bits = sizeof(ElfW(Addr)) * 8; - - if (gnu_nbucket_ == 0 || gnu_bloom_size_ == 0) return 0; - - auto bloom_word = gnu_bloom_filter_[(hash / bloom_mask_bits) % gnu_bloom_size_]; - uintptr_t mask = 0 | (uintptr_t)1 << (hash % bloom_mask_bits) | - (uintptr_t)1 << ((hash >> gnu_shift2_) % bloom_mask_bits); - if ((mask & bloom_word) == mask) { - auto sym_index = gnu_bucket_[hash % gnu_nbucket_]; - if (sym_index >= gnu_symndx_) { - char *strings = (char *)strtab_start; - do { - auto *sym = dynsym_start + sym_index; - if (((gnu_chain_[sym_index] ^ hash) >> 1) == 0 && name == strings + sym->st_name) { - return sym->st_value; - } - } while ((gnu_chain_[sym_index++] & 1) == 0); - } - } - return 0; -} - -void ElfImg::MayInitLinearMap() const { - if (symtabs_.empty()) { - if (symtab_start != nullptr && symstr_offset_for_symtab != 0) { - auto hdr = header_debugdata != nullptr ? header_debugdata : header; - for (ElfW(Off) i = 0; i < symtab_count; i++) { - unsigned int st_type = ELF_ST_TYPE(symtab_start[i].st_info); - const char *st_name = - offsetOf(hdr, symstr_offset_for_symtab + symtab_start[i].st_name); - if ((st_type == STT_FUNC || st_type == STT_OBJECT) && symtab_start[i].st_size) { - symtabs_.emplace(st_name, &symtab_start[i]); - } - } - } - } -} - -ElfW(Addr) ElfImg::LinearLookup(std::string_view name) const { - MayInitLinearMap(); - if (auto i = symtabs_.find(name); i != symtabs_.end()) { - return i->second->st_value; - } else { - return 0; - } -} - -std::vector ElfImg::LinearRangeLookup(std::string_view name) const { - MayInitLinearMap(); - std::vector res; - for (auto [i, end] = symtabs_.equal_range(name); i != end; ++i) { - auto offset = i->second->st_value; - res.emplace_back(offset); - LOGD("found {} {:#x} in {} in symtab by linear range lookup", name, offset, elf); - } - return res; -} - -ElfW(Addr) ElfImg::PrefixLookupFirst(std::string_view prefix) const { - MayInitLinearMap(); - if (auto i = symtabs_.lower_bound(prefix); - i != symtabs_.end() && i->first.starts_with(prefix)) { - LOGD("found prefix {} of {} {:#x} in {} in symtab by linear lookup", prefix, i->first, - i->second->st_value, elf); - return i->second->st_value; - } else { - return 0; - } -} - -ElfImg::~ElfImg() { - // open elf file local - if (buffer) { - free(buffer); - buffer = nullptr; - } - // use mmap - if (header) { - munmap(header, size); - } -} - -ElfW(Addr) ElfImg::getSymbOffset(std::string_view name, uint32_t gnu_hash, - uint32_t elf_hash) const { - if (auto offset = GnuLookup(name, gnu_hash); offset > 0) { - LOGD("found {} {:#x} in {} in dynsym by gnuhash", name, offset, elf); - return offset; - } else if (offset = ElfLookup(name, elf_hash); offset > 0) { - LOGD("found {} {:#x} in {} in dynsym by elfhash", name, offset, elf); - return offset; - } else if (offset = LinearLookup(name); offset > 0) { - LOGD("found {} {:#x} in {} in symtab by linear lookup", name, offset, elf); - return offset; - } else { - return 0; - } -} - -constexpr inline bool contains(std::string_view a, std::string_view b) { - return a.find(b) != std::string_view::npos; -} - -// A clean and simple struct to hold parsed map entry data. -struct MapEntry { - uintptr_t start_addr; - char perms[5] = {0}; // Assured null-termination - std::string pathname; -}; - -bool ElfImg::findModuleBase() { - // Open the maps file using standard C file I/O. - FILE *maps = fopen("/proc/self/maps", "r"); - if (!maps) { - LOGE("failed to open /proc/self/maps"); - return false; - } - - char line_buffer[512]; // A reasonable fixed-size buffer for map lines. - std::vector filtered_list; - - // Step 1: Filter all entries containing `elf` in its path. - while (fgets(line_buffer, sizeof(line_buffer), maps)) { - // Use an intermediate variable of a known, large type to avoid format warnings. - // `unsigned long long` and `%llx` are standard and portable. - unsigned long long temp_start; - char path_buffer[256] = {0}; - char p[5] = {0}; - - // Use the portable `%llx` specifier. - int items_parsed = - sscanf(line_buffer, "%llx-%*x %4s %*x %*s %*d %255s", &temp_start, p, path_buffer); - - // The filter condition: must parse the path, and it must contain the elf name. - if (items_parsed == 3 && strstr(path_buffer, elf.c_str()) != nullptr) { - MapEntry entry; - // Safely assign the parsed value to the uintptr_t. - entry.start_addr = static_cast(temp_start); - strncpy(entry.perms, p, 4); - entry.pathname = path_buffer; - filtered_list.push_back(std::move(entry)); - } - } - fclose(maps); - - if (filtered_list.empty()) { - LOGE("Could not find any mappings for {}", elf.data()); - return false; - } - - // Also part of Step 1: Print the filtered list for debugging. - LOGD("Found {} filtered map entries for {}:", filtered_list.size(), elf.data()); - for (const auto &entry : filtered_list) { - LOGD(" {:#x} {} {}", entry.start_addr, entry.perms, entry.pathname); - } - - const MapEntry *found_block = nullptr; - - // Step 2: In the filtered list, search for the first `r--p` whose next entry is `r-xp`. - for (size_t i = 0; i < filtered_list.size() - 1; ++i) { - if (strcmp(filtered_list[i].perms, "r--p") == 0 && - strcmp(filtered_list[i + 1].perms, "r-xp") == 0) { - found_block = &filtered_list[i]; - LOGD("Found `r--p` -> `r-xp` pattern. Choosing base from `r--p` block at {:#x}", - found_block->start_addr); - break; // Pattern found, exit loop. - } - } - - // Step 2 (Fallback): If the pattern was not found, find the first `r-xp` entry. - if (!found_block) { - LOGD("`r--p` -> `r-xp` pattern not found. Falling back to first `r-xp` entry."); - for (const auto &entry : filtered_list) { - if (strcmp(entry.perms, "r-xp") == 0) { - found_block = &entry; - LOGD("Found first `r-xp` block at {:#x}", found_block->start_addr); - break; // Fallback found, exit loop. - } - } - } - - // Step 3 (Fallback): If the pattern was not found, find the first `r--p` entry. - if (!found_block) { - LOGD("`r-xp` pattern not found. Falling back to first `r--p` entry."); - for (const auto &entry : filtered_list) { - if (strcmp(entry.perms, "r--p") == 0) { - found_block = &entry; - LOGD("Found first `r--p` block at {:#x}", found_block->start_addr); - break; // Fallback found, exit loop. - } - } - } - - if (!found_block) { - LOGE("Fatal: Could not determine a base address for {}", elf.data()); - return false; - } - - // Step 3: Use the starting address of the found block as the base address. - base = reinterpret_cast(found_block->start_addr); - elf = found_block->pathname; // Update elf path to the canonical one. - - LOGD("get module base {}: {:#x}", elf, found_block->start_addr); - LOGD("update path: {}", elf); - - return true; -} diff --git a/core/src/main/jni/src/jni/dex_parser.cpp b/core/src/main/jni/src/jni/dex_parser.cpp deleted file mode 100644 index 6d73545f2..000000000 --- a/core/src/main/jni/src/jni/dex_parser.cpp +++ /dev/null @@ -1,823 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2023 LSPosed Contributors - */ - -#include "dex_parser.h" -#include "native_util.h" -#include "slicer/reader.h" - -#include -#include -#include - -namespace { - using Value = std::tuple /*data*/>; - using Array = std::vector; - using ArrayList = std::list; - using Element = std::tuple; - using ElementList = std::vector; - using Annotation = std::tuple; - using AnnotationList = std::vector; - - class DexParser : public dex::Reader { - public: - DexParser(const dex::u1 *data, size_t size) : dex::Reader(data, size, nullptr, 0) {} - - struct ClassData { - std::vector interfaces; - std::vector static_fields; - std::vector static_fields_access_flags; - std::vector instance_fields; - std::vector instance_fields_access_flags; - std::vector direct_methods; - std::vector direct_methods_access_flags; - std::vector direct_methods_code; - std::vector virtual_methods; - std::vector virtual_methods_access_flags; - std::vector virtual_methods_code; - std::vector annotations; - }; - - struct MethodBody { - bool loaded; - std::vector referred_strings; - std::vector accessed_fields; - std::vector assigned_fields; - std::vector invoked_methods; - std::vector opcodes; - }; - - std::vector class_data; - phmap::flat_hash_map> field_annotations; - phmap::flat_hash_map> method_annotations; - phmap::flat_hash_map> parameter_annotations; - - phmap::flat_hash_map method_bodies; - }; - - template - static std::vector ParseIntValue(const dex::u1 **pptr, size_t size) { - static_assert(std::is_integral::value, "must be an integral type"); - std::vector ret(sizeof(T)); - T &value = *reinterpret_cast(ret.data()); - for (size_t i = 0; i < size; ++i) { - value |= T(*(*pptr)++) << (i * 8); - } - - // sign-extend? - if constexpr (std::is_signed_v) { - size_t shift = (sizeof(T) - size) * 8; - value = T(value << shift) >> shift; - } - return ret; - } - - template - static std::vector ParseFloatValue(const dex::u1 **pptr, size_t size) { - std::vector ret(sizeof(T)); - T &value = *reinterpret_cast(ret.data()); - int start_byte = sizeof(T) - size; - for (dex::u1 *p = reinterpret_cast(&value) + start_byte; size > 0; - --size) { - *p++ = *(*pptr)++; - } - return ret; - } - - Annotation - ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, - ArrayList &array_list); - - Array - ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list); - - Value - ParseValue(const dex::u1 **value, AnnotationList &annotation_list, ArrayList &array_list) { - Value res; - auto &[type, value_content] = res; - auto header = *(*value)++; - type = header & dex::kEncodedValueTypeMask; - dex::u1 arg = header >> dex::kEncodedValueArgShift; - switch (type) { - case dex::kEncodedByte: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedShort: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedChar: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedInt: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedLong: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedFloat: - value_content = ParseFloatValue(value, arg + 1); - break; - case dex::kEncodedDouble: - value_content = ParseFloatValue(value, arg + 1); - break; - case dex::kEncodedMethodType: - case dex::kEncodedMethodHandle: - case dex::kEncodedString: - case dex::kEncodedType: - case dex::kEncodedField: - case dex::kEncodedMethod: - case dex::kEncodedEnum: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedArray: - value_content.resize(sizeof(jint)); - *reinterpret_cast(value_content.data()) = static_cast(array_list.size()); - array_list.emplace_back(ParseArray(value, annotation_list, array_list)); - break; - case dex::kEncodedAnnotation: - value_content.resize(sizeof(jint)); - *reinterpret_cast(value_content.data()) = static_cast(annotation_list.size()); - annotation_list.emplace_back(ParseAnnotation(value, annotation_list, array_list)); - break; - case dex::kEncodedNull: - break; - case dex::kEncodedBoolean: - value_content = {static_cast(arg == 1)}; - break; - default: - __builtin_unreachable(); - } - return res; - } - - Annotation - ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, - ArrayList &array_list) { - Annotation ret = {dex::kVisibilityEncoded, - dex::ReadULeb128(annotation), - ElementList{}}; - auto &[vis, type, element_list] = ret; - auto size = dex::ReadULeb128(annotation); - element_list.resize(size); - for (size_t j = 0; j < size; ++j) { - auto &[name, value] = element_list[j]; - name = static_cast(dex::ReadULeb128(annotation)); - value = ParseValue(annotation, annotation_list, array_list); - } - return ret; - } - - Array - ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list) { - auto size = dex::ReadULeb128(array); - Array ret; - ret.reserve(size); - for (size_t i = 0; i < size; ++i) { - ret.emplace_back(ParseValue(array, annotation_list, array_list)); - } - return ret; - } - - void ParseAnnotationSet(dex::Reader &dex, AnnotationList &annotation_list, - ArrayList &array_list, std::vector &indices, - const dex::AnnotationSetItem *annotation_set) { - if (annotation_set == nullptr) { - return; - } - for (size_t i = 0; i < annotation_set->size; ++i) { - auto *item = dex.dataPtr(annotation_set->entries[i]); - auto *annotation_data = item->annotation; - indices.emplace_back(annotation_list.size()); - auto &[visibility, type, element_list] = annotation_list.emplace_back( - ParseAnnotation(&annotation_data, annotation_list, array_list)); - visibility = item->visibility; - } - } -} - -namespace lspd { - LSP_DEF_NATIVE_METHOD(jobject, DexParserBridge, openDex, jobject data, jlongArray args) { - auto dex_size = env->GetDirectBufferCapacity(data); - if (dex_size == -1) { - env->ThrowNew(env->FindClass("java/io/IOException"), "Invalid dex data"); - return nullptr; - } - auto *dex_data = env->GetDirectBufferAddress(data); - - auto *dex_reader = new DexParser(reinterpret_cast(dex_data), dex_size); - auto *args_ptr = env->GetLongArrayElements(args, nullptr); - auto include_annotations = args_ptr[1]; - env->ReleaseLongArrayElements(args, args_ptr, JNI_ABORT); - env->SetLongArrayRegion(args, 0, 1, reinterpret_cast(&dex_reader)); - auto &dex = *dex_reader; - if (dex.IsCompact()) { - env->ThrowNew(env->FindClass("java/io/IOException"), "Compact dex is not supported"); - return nullptr; - } - auto object_class = env->FindClass("java/lang/Object"); - auto string_class = env->FindClass("java/lang/String"); - auto int_array_class = env->FindClass("[I"); - auto out = env->NewObjectArray(8, object_class, nullptr); - auto out0 = env->NewObjectArray(static_cast(dex.StringIds().size()), - string_class, nullptr); - auto strings = dex.StringIds(); - for (size_t i = 0; i < strings.size(); ++i) { - const auto *ptr = dex.dataPtr(strings[i].string_data_off); - [[maybe_unused]] size_t len = dex::ReadULeb128(&ptr); - auto str = env->NewStringUTF(reinterpret_cast(ptr)); - env->SetObjectArrayElement(out0, static_cast(i), str); - env->DeleteLocalRef(str); - } - env->SetObjectArrayElement(out, 0, out0); - env->DeleteLocalRef(out0); - - auto types = dex.TypeIds(); - auto out1 = env->NewIntArray(static_cast(types.size())); - auto *out1_ptr = env->GetIntArrayElements(out1, nullptr); - for (size_t i = 0; i < types.size(); ++i) { - out1_ptr[i] = static_cast(types[i].descriptor_idx); - } - env->ReleaseIntArrayElements(out1, out1_ptr, 0); - env->SetObjectArrayElement(out, 1, out1); - env->DeleteLocalRef(out1); - - auto protos = dex.ProtoIds(); - auto out2 = env->NewObjectArray(static_cast(protos.size()), - int_array_class, nullptr); - auto empty_type_list = dex::TypeList{.size = 0, .list = {}}; - for (size_t i = 0; i < protos.size(); ++i) { - auto &proto = protos[i]; - const auto ¶ms = proto.parameters_off ? *dex.dataPtr( - proto.parameters_off) : empty_type_list; - - auto out2i = env->NewIntArray(static_cast(2 + params.size)); - auto *out2i_ptr = env->GetIntArrayElements(out2i, nullptr); - out2i_ptr[0] = static_cast(proto.shorty_idx); - out2i_ptr[1] = static_cast(proto.return_type_idx); - for (size_t j = 0; j < params.size; ++j) { - out2i_ptr[2 + j] = static_cast(params.list[j].type_idx); - } - env->ReleaseIntArrayElements(out2i, out2i_ptr, 0); - env->SetObjectArrayElement(out2, static_cast(i), out2i); - env->DeleteLocalRef(out2i); - } - env->SetObjectArrayElement(out, 2, out2); - env->DeleteLocalRef(out2); - - auto fields = dex.FieldIds(); - auto out3 = env->NewIntArray(static_cast(3 * fields.size())); - auto *out3_ptr = env->GetIntArrayElements(out3, nullptr); - for (size_t i = 0; i < fields.size(); ++i) { - auto &field = fields[i]; - out3_ptr[3 * i] = static_cast(field.class_idx); - out3_ptr[3 * i + 1] = static_cast(field.type_idx); - out3_ptr[3 * i + 2] = static_cast(field.name_idx); - } - env->ReleaseIntArrayElements(out3, out3_ptr, 0); - env->SetObjectArrayElement(out, 3, out3); - env->DeleteLocalRef(out3); - - auto methods = dex.MethodIds(); - auto out4 = env->NewIntArray(static_cast(3 * methods.size())); - auto *out4_ptr = env->GetIntArrayElements(out4, nullptr); - for (size_t i = 0; i < methods.size(); ++i) { - out4_ptr[3 * i] = static_cast(methods[i].class_idx); - out4_ptr[3 * i + 1] = static_cast(methods[i].proto_idx); - out4_ptr[3 * i + 2] = static_cast(methods[i].name_idx); - } - env->ReleaseIntArrayElements(out4, out4_ptr, 0); - env->SetObjectArrayElement(out, 4, out4); - env->DeleteLocalRef(out4); - - auto classes = dex.ClassDefs(); - dex.class_data.resize(classes.size()); - - AnnotationList annotation_list; - ArrayList array_list; - - for (size_t i = 0; i < classes.size(); ++i) { - auto &class_def = classes[i]; - - dex::u4 static_fields_count = 0; - dex::u4 instance_fields_count = 0; - dex::u4 direct_methods_count = 0; - dex::u4 virtual_methods_count = 0; - const dex::u1 *class_data_ptr = nullptr; - - const dex::AnnotationsDirectoryItem *annotations = nullptr; - const dex::AnnotationSetItem *class_annotation = nullptr; - dex::u4 field_annotations_count = 0; - dex::u4 method_annotations_count = 0; - dex::u4 parameter_annotations_count = 0; - - auto &class_data = dex.class_data[i]; - - if (class_def.interfaces_off) { - auto defined_interfaces = dex.dataPtr(class_def.interfaces_off); - class_data.interfaces.resize(defined_interfaces->size); - for (size_t k = 0; k < class_data.interfaces.size(); ++k) { - class_data.interfaces[k] = defined_interfaces->list[k].type_idx; - } - } - - if (class_def.annotations_off != 0) { - annotations = dex.dataPtr(class_def.annotations_off); - if (annotations->class_annotations_off != 0) { - class_annotation = dex.dataPtr( - annotations->class_annotations_off); - } - field_annotations_count = annotations->fields_size; - method_annotations_count = annotations->methods_size; - parameter_annotations_count = annotations->parameters_size; - } - - if (class_def.class_data_off != 0) { - class_data_ptr = dex.dataPtr(class_def.class_data_off); - static_fields_count = dex::ReadULeb128(&class_data_ptr); - instance_fields_count = dex::ReadULeb128(&class_data_ptr); - direct_methods_count = dex::ReadULeb128(&class_data_ptr); - virtual_methods_count = dex::ReadULeb128(&class_data_ptr); - class_data.static_fields.resize(static_fields_count); - class_data.static_fields_access_flags.resize(static_fields_count); - class_data.instance_fields.resize(instance_fields_count); - class_data.instance_fields_access_flags.resize(instance_fields_count); - class_data.direct_methods.resize(direct_methods_count); - class_data.direct_methods_access_flags.resize(direct_methods_count); - class_data.direct_methods_code.resize(direct_methods_count); - class_data.virtual_methods.resize(virtual_methods_count); - class_data.virtual_methods_access_flags.resize(virtual_methods_count); - class_data.virtual_methods_code.resize(virtual_methods_count); - } - - if (class_data_ptr) { - for (size_t k = 0, field_idx = 0; k < static_fields_count; ++k) { - class_data.static_fields[k] = static_cast(field_idx += dex::ReadULeb128( - &class_data_ptr)); - class_data.static_fields_access_flags[k] = static_cast(dex::ReadULeb128( - &class_data_ptr)); - } - - for (size_t k = 0, field_idx = 0; k < instance_fields_count; ++k) { - class_data.instance_fields[k] = static_cast(field_idx += dex::ReadULeb128( - &class_data_ptr)); - class_data.instance_fields_access_flags[k] = static_cast(dex::ReadULeb128( - &class_data_ptr)); - } - - for (size_t k = 0, method_idx = 0; k < direct_methods_count; ++k) { - class_data.direct_methods[k] = static_cast(method_idx += dex::ReadULeb128( - &class_data_ptr)); - class_data.direct_methods_access_flags[k] = static_cast(dex::ReadULeb128( - &class_data_ptr)); - auto code_off = dex::ReadULeb128(&class_data_ptr); - class_data.direct_methods_code[k] = code_off ? dex.dataPtr(code_off) - : nullptr; - } - - for (size_t k = 0, method_idx = 0; k < virtual_methods_count; ++k) { - class_data.virtual_methods[k] = static_cast(method_idx += dex::ReadULeb128( - &class_data_ptr)); - class_data.virtual_methods_access_flags[k] = static_cast(dex::ReadULeb128( - &class_data_ptr)); - auto code_off = dex::ReadULeb128(&class_data_ptr); - class_data.virtual_methods_code[k] = code_off ? dex.dataPtr(code_off) - : nullptr; - } - } - - if (!include_annotations) continue; - ParseAnnotationSet(dex, annotation_list, array_list, class_data.annotations, - class_annotation); - - auto *field_annotations = annotations - ? reinterpret_cast( - annotations + 1) : nullptr; - for (size_t k = 0; k < field_annotations_count; ++k) { - auto *field_annotation = dex.dataPtr( - field_annotations[k].annotations_off); - ParseAnnotationSet(dex, annotation_list, array_list, - dex.field_annotations[static_cast(field_annotations[k].field_idx)], - field_annotation); - } - - auto *method_annotations = field_annotations - ? reinterpret_cast( - field_annotations + field_annotations_count) - : nullptr; - for (size_t k = 0; k < method_annotations_count; ++k) { - auto *method_annotation = dex.dataPtr( - method_annotations[k].annotations_off); - ParseAnnotationSet(dex, annotation_list, array_list, - dex.method_annotations[static_cast(method_annotations[k].method_idx)], - method_annotation); - } - - auto *parameter_annotations = method_annotations - ? reinterpret_cast( - method_annotations + method_annotations_count) - : nullptr; - for (size_t k = 0; k < parameter_annotations_count; ++k) { - auto *parameter_annotation = dex.dataPtr( - parameter_annotations[k].annotations_off); - auto &indices = dex.parameter_annotations[static_cast(parameter_annotations[k].method_idx)]; - for (size_t l = 0; l < parameter_annotation->size; ++l) { - if (parameter_annotation->list[l].annotations_off != 0) { - auto *parameter_annotation_item = dex.dataPtr( - parameter_annotation->list[l].annotations_off); - ParseAnnotationSet(dex, annotation_list, array_list, indices, - parameter_annotation_item); - } - indices.emplace_back(dex::kNoIndex); - } - } - } - - if (!include_annotations) return out; - - auto out5 = env->NewIntArray(static_cast(2 * annotation_list.size())); - auto out6 = env->NewObjectArray(static_cast(2 * annotation_list.size()), object_class, - nullptr); - auto out5_ptr = env->GetIntArrayElements(out5, nullptr); - size_t i = 0; - for (auto &[visibility, type, items]: annotation_list) { - auto out6i0 = env->NewIntArray(static_cast(2 * items.size())); - auto out6i0_ptr = env->GetIntArrayElements(out6i0, nullptr); - auto out6i1 = env->NewObjectArray(static_cast(items.size()), object_class, - nullptr); - size_t j = 0; - for (auto&[name, value]: items) { - auto &[value_type, value_data] = value; - auto java_value = value_data.empty() ? nullptr : env->NewDirectByteBuffer( - value_data.data(), value_data.size()); - env->SetObjectArrayElement(out6i1, static_cast(j), java_value); - out6i0_ptr[2 * j] = name; - out6i0_ptr[2 * j + 1] = value_type; - env->DeleteLocalRef(java_value); - ++j; - } - env->ReleaseIntArrayElements(out6i0, out6i0_ptr, 0); - env->SetObjectArrayElement(out6, static_cast(2 * i), out6i0); - env->SetObjectArrayElement(out6, static_cast(2 * i + 1), out6i1); - out5_ptr[2 * i] = visibility; - out5_ptr[2 * i + 1] = type; - env->DeleteLocalRef(out6i0); - env->DeleteLocalRef(out6i1); - ++i; - } - env->ReleaseIntArrayElements(out5, out5_ptr, 0); - env->SetObjectArrayElement(out, 5, out5); - env->SetObjectArrayElement(out, 6, out6); - env->DeleteLocalRef(out5); - env->DeleteLocalRef(out6); - - auto out7 = env->NewObjectArray(static_cast(2 * array_list.size()), object_class, - nullptr); - i = 0; - for (auto &array: array_list) { - - auto out7i0 = env->NewIntArray(static_cast(array.size())); - auto out7i0_ptr = env->GetIntArrayElements(out7i0, nullptr); - auto out7i1 = env->NewObjectArray(static_cast(array.size()), object_class, - nullptr); - size_t j = 0; - for (auto &value: array) { - auto &[value_type, value_data] = value; - auto java_value = value_data.empty() ? nullptr : env->NewDirectByteBuffer( - value_data.data(), value_data.size()); - out7i0_ptr[j] = value_type; - env->SetObjectArrayElement(out7i1, static_cast(j), java_value); - env->DeleteLocalRef(java_value); - ++j; - } - env->ReleaseIntArrayElements(out7i0, out7i0_ptr, 0); - env->SetObjectArrayElement(out7, static_cast(2 * i), out7i0); - env->SetObjectArrayElement(out7, static_cast(2 * i + 1), out7i1); - env->DeleteLocalRef(out7i0); - env->DeleteLocalRef(out7i1); - ++i; - } - env->SetObjectArrayElement(out, 7, out7); - env->DeleteLocalRef(out7); - - return out; - } - - LSP_DEF_NATIVE_METHOD(void, DexParserBridge, closeDex, jlong cookie) { - if (cookie != 0) - delete reinterpret_cast(cookie); - } - - LSP_DEF_NATIVE_METHOD(void, DexParserBridge, visitClass, jlong cookie, jobject visitor, - jclass field_visitor_class, - jclass method_visitor_class, - jobject class_visit_method, - jobject field_visit_method, - jobject method_visit_method, - jobject method_body_visit_method, - jobject stop_method) { - static constexpr dex::u1 kOpcodeMask = 0xff; - static constexpr dex::u1 kOpcodeNoOp = 0x00; - static constexpr dex::u1 kOpcodeConstString = 0x1a; - static constexpr dex::u1 kOpcodeConstStringJumbo = 0x1b; - static constexpr dex::u1 kOpcodeIGetStart = 0x52; - static constexpr dex::u1 kOpcodeIGetEnd = 0x58; - static constexpr dex::u1 kOpcodeSGetStart = 0x60; - static constexpr dex::u1 kOpcodeSGetEnd = 0x66; - static constexpr dex::u1 kOpcodeIPutStart = 0x59; - static constexpr dex::u1 kOpcodeIPutEnd = 0x5f; - static constexpr dex::u1 kOpcodeSPutStart = 0x67; - static constexpr dex::u1 kOpcodeSPutEnd = 0x6d; - static constexpr dex::u1 kOpcodeInvokeStart = 0x6e; - static constexpr dex::u1 kOpcodeInvokeEnd = 0x72; - static constexpr dex::u1 kOpcodeInvokeRangeStart = 0x74; - static constexpr dex::u1 kOpcodeInvokeRangeEnd = 0x78; - static constexpr dex::u2 kInstPackedSwitchPlayLoad = 0x0100; - static constexpr dex::u2 kInstSparseSwitchPlayLoad = 0x0200; - static constexpr dex::u2 kInstFillArrayDataPlayLoad = 0x0300; - - if (cookie == 0) { - return; - } - auto &dex = *reinterpret_cast(cookie); - auto *visit_class = env->FromReflectedMethod(class_visit_method); - auto *visit_field = env->FromReflectedMethod(field_visit_method); - auto *visit_method = env->FromReflectedMethod(method_visit_method); - auto *visit_method_body = env->FromReflectedMethod(method_body_visit_method); - auto *stop = env->FromReflectedMethod(stop_method); - - auto classes = dex.ClassDefs(); - - - for (size_t i = 0; i < classes.size(); ++i) { - auto &class_def = classes[i]; - auto &class_data = dex.class_data[i]; - auto interfaces = env->NewIntArray( - static_cast(class_data.interfaces.size())); - env->SetIntArrayRegion(interfaces, 0, - static_cast(class_data.interfaces.size()), - class_data.interfaces.data()); - auto static_fields = env->NewIntArray( - static_cast(class_data.static_fields.size())); - env->SetIntArrayRegion(static_fields, 0, - static_cast(class_data.static_fields.size()), - class_data.static_fields.data()); - auto static_fields_access_flags = env->NewIntArray( - static_cast(class_data.static_fields_access_flags.size())); - env->SetIntArrayRegion(static_fields_access_flags, 0, - static_cast( - class_data.static_fields_access_flags.size()), - class_data.static_fields_access_flags.data()); - auto instance_fields = env->NewIntArray( - static_cast(class_data.instance_fields.size())); - env->SetIntArrayRegion(instance_fields, 0, - static_cast(class_data.instance_fields.size()), - class_data.instance_fields.data()); - auto instance_fields_access_flags = env->NewIntArray( - static_cast(class_data.instance_fields_access_flags.size())); - env->SetIntArrayRegion(instance_fields_access_flags, 0, - static_cast( - class_data.instance_fields_access_flags.size()), - class_data.instance_fields_access_flags.data()); - auto direct_methods = env->NewIntArray( - static_cast(class_data.direct_methods.size())); - env->SetIntArrayRegion(direct_methods, 0, - static_cast(class_data.direct_methods.size()), - class_data.direct_methods.data()); - auto direct_methods_access_flags = env->NewIntArray( - static_cast(class_data.direct_methods_access_flags.size())); - env->SetIntArrayRegion(direct_methods_access_flags, 0, - static_cast( - class_data.direct_methods_access_flags.size()), - class_data.direct_methods_access_flags.data()); - auto virtual_methods = env->NewIntArray( - static_cast(class_data.virtual_methods.size())); - env->SetIntArrayRegion(virtual_methods, 0, - static_cast(class_data.virtual_methods.size()), - class_data.virtual_methods.data()); - auto virtual_methods_access_flags = env->NewIntArray( - static_cast(class_data.virtual_methods_access_flags.size())); - env->SetIntArrayRegion(virtual_methods_access_flags, 0, - static_cast( - class_data.virtual_methods_access_flags.size()), - class_data.virtual_methods_access_flags.data()); - auto class_annotations = env->NewIntArray( - static_cast(class_data.annotations.size())); - env->SetIntArrayRegion(class_annotations, 0, - static_cast(class_data.annotations.size()), - class_data.annotations.data()); - jobject member_visitor = env->CallObjectMethod(visitor, visit_class, - static_cast(class_def.class_idx), - static_cast(class_def.access_flags), - static_cast(class_def.superclass_idx), - interfaces, - static_cast(class_def.source_file_idx), - static_fields, - static_fields_access_flags, - instance_fields, - instance_fields_access_flags, - direct_methods, - direct_methods_access_flags, - virtual_methods, - virtual_methods_access_flags, - class_annotations - ); - env->DeleteLocalRef(interfaces); - env->DeleteLocalRef(static_fields); - env->DeleteLocalRef(static_fields_access_flags); - env->DeleteLocalRef(instance_fields); - env->DeleteLocalRef(instance_fields_access_flags); - env->DeleteLocalRef(direct_methods); - env->DeleteLocalRef(direct_methods_access_flags); - env->DeleteLocalRef(virtual_methods); - env->DeleteLocalRef(virtual_methods_access_flags); - env->DeleteLocalRef(class_annotations); - if (member_visitor && env->IsInstanceOf(member_visitor, field_visitor_class)) { - jboolean stopped = JNI_FALSE; - for (auto &[fields, fields_access_flags]: { - std::make_tuple(class_data.static_fields, - class_data.static_fields_access_flags), - std::make_tuple(class_data.instance_fields, - class_data.instance_fields_access_flags)}) { - for (size_t j = 0; j < fields.size(); j++) { - auto field_idx = fields[j]; - auto access_flags = fields_access_flags[j]; - auto &field_annotations = dex.field_annotations[field_idx]; - auto annotations = env->NewIntArray( - static_cast(field_annotations.size())); - env->SetIntArrayRegion(annotations, 0, - static_cast(field_annotations.size()), - field_annotations.data()); - env->CallVoidMethod(member_visitor, visit_field, field_idx, - access_flags, annotations); - env->DeleteLocalRef(annotations); - stopped = env->CallBooleanMethod(member_visitor, stop); - if (stopped == JNI_TRUE) break; - } - if (stopped == JNI_TRUE) break; - } - } - if (member_visitor && env->IsInstanceOf(member_visitor, method_visitor_class)) { - jboolean stopped = JNI_FALSE; - for (auto &[methods, methods_access_flags, methods_code]: { - std::make_tuple(class_data.direct_methods, - class_data.direct_methods_access_flags, - class_data.direct_methods_code), - std::make_tuple(class_data.virtual_methods, - class_data.virtual_methods_access_flags, - class_data.virtual_methods_code)}) { - for (size_t j = 0; j < methods.size(); j++) { - auto method_idx = methods[j]; - auto access_flags = methods_access_flags[j]; - auto code = methods_code[j]; - auto method_annotation = dex.method_annotations[method_idx]; - auto method_annotations = env->NewIntArray( - static_cast(method_annotation.size())); - env->SetIntArrayRegion(method_annotations, 0, - static_cast(method_annotation.size()), - method_annotation.data()); - auto parameter_annotation = dex.parameter_annotations[method_idx]; - auto parameter_annotations = env->NewIntArray( - static_cast(parameter_annotation.size())); - env->SetIntArrayRegion(parameter_annotations, 0, - static_cast(parameter_annotation.size()), - parameter_annotation.data()); - auto body_visitor = env->CallObjectMethod(member_visitor, visit_method, - method_idx, - access_flags, code != nullptr, - method_annotations, - parameter_annotations); - env->DeleteLocalRef(method_annotations); - env->DeleteLocalRef(parameter_annotations); - if (body_visitor && code != nullptr) { - auto body = dex.method_bodies[method_idx]; - if (!body.loaded) { - std::set referred_strings; - std::set assigned_fields; - std::set accessed_fields; - std::set invoked_methods; - - const dex::u2 *inst = code->insns; - const dex::u2 *end = inst + code->insns_size; - while (inst < end) { - dex::u1 opcode = *inst & kOpcodeMask; - body.opcodes.push_back(static_cast(opcode)); - if (opcode == kOpcodeConstString) { - auto str_idx = inst[1]; - referred_strings.emplace(str_idx); - } - if (opcode == kOpcodeConstStringJumbo) { - auto str_idx = *reinterpret_cast(&inst[1]); - referred_strings.emplace(static_cast(str_idx)); - } - if ((opcode >= kOpcodeIGetStart && opcode <= kOpcodeIGetEnd) || - (opcode >= kOpcodeSGetStart && opcode <= kOpcodeSGetEnd)) { - auto field_idx = inst[1]; - accessed_fields.emplace(field_idx); - } - if ((opcode >= kOpcodeIPutStart && opcode <= kOpcodeIPutEnd) || - (opcode >= kOpcodeSPutStart && opcode <= kOpcodeSPutEnd)) { - auto field_idx = inst[1]; - assigned_fields.emplace(field_idx); - } - if ((opcode >= kOpcodeInvokeStart && - opcode <= kOpcodeInvokeEnd) || - (opcode >= kOpcodeInvokeRangeStart && - opcode <= kOpcodeInvokeRangeEnd)) { - auto callee = inst[1]; - invoked_methods.emplace(callee); - } - if (opcode == kOpcodeNoOp) { - if (*inst == kInstPackedSwitchPlayLoad) { - inst += inst[1] * 2 + 3; - } else if (*inst == kInstSparseSwitchPlayLoad) { - inst += inst[1] * 4 + 1; - } else if (*inst == kInstFillArrayDataPlayLoad) { - inst += (*reinterpret_cast(&inst[2]) * - inst[1] + 1) / - 2 + 3; - } - } - inst += dex::opcode_len[opcode]; - } - body.referred_strings.insert(body.referred_strings.end(), - referred_strings.begin(), - referred_strings.end()); - body.assigned_fields.insert(body.assigned_fields.end(), - assigned_fields.begin(), - assigned_fields.end()); - body.accessed_fields.insert(body.accessed_fields.end(), - accessed_fields.begin(), - accessed_fields.end()); - body.invoked_methods.insert(body.invoked_methods.end(), - invoked_methods.begin(), - invoked_methods.end()); - body.loaded = true; - } - auto referred_strings = env->NewIntArray( - static_cast(body.referred_strings.size())); - env->SetIntArrayRegion(referred_strings, 0, - static_cast(body.referred_strings.size()), - body.referred_strings.data()); - auto accessed_fields = env->NewIntArray( - static_cast(body.accessed_fields.size())); - env->SetIntArrayRegion(accessed_fields, 0, - static_cast(body.accessed_fields.size()), - body.accessed_fields.data()); - auto assigned_fields = env->NewIntArray( - static_cast(body.assigned_fields.size())); - env->SetIntArrayRegion(assigned_fields, 0, - static_cast(body.assigned_fields.size()), - body.assigned_fields.data()); - auto invoked_methods = env->NewIntArray( - static_cast(body.invoked_methods.size())); - env->SetIntArrayRegion(invoked_methods, 0, - static_cast(body.invoked_methods.size()), - body.invoked_methods.data()); - auto opcodes = env->NewByteArray( - static_cast(body.opcodes.size())); - env->SetByteArrayRegion(opcodes, 0, - static_cast(body.opcodes.size()), - body.opcodes.data()); - env->CallVoidMethod(body_visitor, visit_method_body, - referred_strings, - invoked_methods, - accessed_fields, assigned_fields, opcodes); - } - stopped = env->CallBooleanMethod(member_visitor, stop); - if (stopped == JNI_TRUE) break; - } - if (stopped == JNI_TRUE) break; - } - } - if (env->CallBooleanMethod(visitor, stop) == JNI_TRUE) break; - } - } - - static JNINativeMethod gMethods[] = { - LSP_NATIVE_METHOD(DexParserBridge, openDex, - "(Ljava/nio/ByteBuffer;[J)Ljava/lang/Object;"), - LSP_NATIVE_METHOD(DexParserBridge, closeDex, "(J)V"), - LSP_NATIVE_METHOD(DexParserBridge, visitClass, - "(JLjava/lang/Object;Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"), - }; - - - void RegisterDexParserBridge(JNIEnv *env) { - REGISTER_LSP_NATIVE_METHODS(DexParserBridge); - } -} diff --git a/core/src/main/jni/src/jni/dex_parser.h b/core/src/main/jni/src/jni/dex_parser.h deleted file mode 100644 index c6785d465..000000000 --- a/core/src/main/jni/src/jni/dex_parser.h +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2023 LSPosed Contributors - */ -#pragma once - -#include - -namespace lspd { - void RegisterDexParserBridge(JNIEnv *env); -} diff --git a/core/src/main/jni/src/jni/hook_bridge.cpp b/core/src/main/jni/src/jni/hook_bridge.cpp deleted file mode 100644 index 1d78f3c1d..000000000 --- a/core/src/main/jni/src/jni/hook_bridge.cpp +++ /dev/null @@ -1,361 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -#include "hook_bridge.h" -#include "native_util.h" -#include "lsplant.hpp" -#include -#include -#include -#include -#include - -using namespace lsplant; - -namespace { -struct ModuleCallback { - jmethodID before_method; - jmethodID after_method; -}; - -struct HookItem { - std::multimap> legacy_callbacks; - std::multimap> modern_callbacks; -private: - std::atomic backup {nullptr}; - static_assert(decltype(backup)::is_always_lock_free); - inline static jobject FAILED = reinterpret_cast(std::numeric_limits::max()); -public: - jobject GetBackup() { - backup.wait(nullptr, std::memory_order::acquire); - if (auto bk = backup.load(std::memory_order_relaxed); bk != FAILED) { - return bk; - } else { - return nullptr; - } - } - void SetBackup(jobject newBackup) { - jobject null = nullptr; - backup.compare_exchange_strong(null, newBackup ? newBackup : FAILED, - std::memory_order_acq_rel, std::memory_order_relaxed); - backup.notify_all(); - } -}; - -template , - class Eq = phmap::priv::hash_default_eq, - class Alloc = phmap::priv::Allocator>, size_t N = 4> -using SharedHashMap = phmap::parallel_flat_hash_map; - -SharedHashMap> hooked_methods; - -jmethodID invoke = nullptr; -jmethodID callback_ctor = nullptr; -jfieldID before_method_field = nullptr; -jfieldID after_method_field = nullptr; -} - -namespace lspd { -LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jboolean useModernApi, jobject hookMethod, - jclass hooker, jint priority, jobject callback) { - bool newHook = false; -#ifndef NDEBUG - struct finally { - std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); - bool &newHook; - ~finally() { - auto finish = std::chrono::steady_clock::now(); - if (newHook) { - LOGV("New hook took {}us", - std::chrono::duration_cast(finish - start).count()); - } - } - } finally { - .newHook = newHook - }; -#endif - auto target = env->FromReflectedMethod(hookMethod); - HookItem * hook_item = nullptr; - hooked_methods.lazy_emplace_l(target, [&hook_item](auto &it) { - hook_item = it.second.get(); - }, [&hook_item, &target, &newHook](const auto &ctor) { - auto ptr = std::make_unique(); - hook_item = ptr.get(); - ctor(target, std::move(ptr)); - newHook = true; - }); - if (newHook) { - auto init = env->GetMethodID(hooker, "", "(Ljava/lang/reflect/Executable;)V"); - auto callback_method = env->ToReflectedMethod(hooker, env->GetMethodID(hooker, "callback", - "([Ljava/lang/Object;)Ljava/lang/Object;"), - false); - auto hooker_object = env->NewObject(hooker, init, hookMethod); - hook_item->SetBackup(lsplant::Hook(env, hookMethod, hooker_object, callback_method)); - env->DeleteLocalRef(hooker_object); - } - jobject backup = hook_item->GetBackup(); - if (!backup) return JNI_FALSE; - JNIMonitor monitor(env, backup); - if (useModernApi) { - if (before_method_field == nullptr) { - auto callback_class = JNI_GetObjectClass(env, callback); - callback_ctor = JNI_GetMethodID(env, callback_class, "", "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"); - before_method_field = JNI_GetFieldID(env, callback_class, "beforeInvocation", "Ljava/lang/reflect/Method;"); - after_method_field = JNI_GetFieldID(env, callback_class, "afterInvocation", "Ljava/lang/reflect/Method;"); - } - auto before_method = JNI_GetObjectField(env, callback, before_method_field); - auto after_method = JNI_GetObjectField(env, callback, after_method_field); - auto callback_type = ModuleCallback { - .before_method = env->FromReflectedMethod(before_method.get()), - .after_method = env->FromReflectedMethod(after_method.get()), - }; - hook_item->modern_callbacks.emplace(priority, callback_type); - } else { - hook_item->legacy_callbacks.emplace(priority, env->NewGlobalRef(callback)); - } - return JNI_TRUE; -} - -LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jboolean useModernApi, jobject hookMethod, jobject callback) { - auto target = env->FromReflectedMethod(hookMethod); - HookItem * hook_item = nullptr; - hooked_methods.if_contains(target, [&hook_item](const auto &it) { - hook_item = it.second.get(); - }); - if (!hook_item) return JNI_FALSE; - jobject backup = hook_item->GetBackup(); - if (!backup) return JNI_FALSE; - JNIMonitor monitor(env, backup); - if (useModernApi) { - auto before_method = JNI_GetObjectField(env, callback, before_method_field); - auto before = env->FromReflectedMethod(before_method.get()); - for (auto i = hook_item->modern_callbacks.begin(); i != hook_item->modern_callbacks.end(); ++i) { - if (before == i->second.before_method) { - hook_item->modern_callbacks.erase(i); - return JNI_TRUE; - } - } - } else { - for (auto i = hook_item->legacy_callbacks.begin(); i != hook_item->legacy_callbacks.end(); ++i) { - if (env->IsSameObject(i->second, callback)) { - hook_item->legacy_callbacks.erase(i); - return JNI_TRUE; - } - } - } - return JNI_FALSE; -} - -LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, deoptimizeMethod, jobject hookMethod, - jclass hooker, jint priority, jobject callback) { - return lsplant::Deoptimize(env, hookMethod); -} - -LSP_DEF_NATIVE_METHOD(jobject, HookBridge, invokeOriginalMethod, jobject hookMethod, - jobject thiz, jobjectArray args) { - auto target = env->FromReflectedMethod(hookMethod); - HookItem * hook_item = nullptr; - hooked_methods.if_contains(target, [&hook_item](const auto &it) { - hook_item = it.second.get(); - }); - return env->CallObjectMethod(hook_item ? hook_item->GetBackup() : hookMethod, invoke, thiz, args); -} - -LSP_DEF_NATIVE_METHOD(jobject, HookBridge, allocateObject, jclass cls) { - return env->AllocObject(cls); -} - -LSP_DEF_NATIVE_METHOD(jobject, HookBridge, invokeSpecialMethod, jobject method, jcharArray shorty, - jclass cls, jobject thiz, jobjectArray args) { - static auto* const get_int = env->GetMethodID(env->FindClass("java/lang/Integer"), "intValue", "()I"); - static auto* const get_double = env->GetMethodID(env->FindClass("java/lang/Double"), "doubleValue", "()D"); - static auto* const get_long = env->GetMethodID(env->FindClass("java/lang/Long"), "longValue", "()J"); - static auto* const get_float = env->GetMethodID(env->FindClass("java/lang/Float"), "floatValue", "()F"); - static auto* const get_short = env->GetMethodID(env->FindClass("java/lang/Short"), "shortValue", "()S"); - static auto* const get_byte = env->GetMethodID(env->FindClass("java/lang/Byte"), "byteValue", "()B"); - static auto* const get_char = env->GetMethodID(env->FindClass("java/lang/Character"), "charValue", "()C"); - static auto* const get_boolean = env->GetMethodID(env->FindClass("java/lang/Boolean"), "booleanValue", "()Z"); - static auto* const set_int = env->GetStaticMethodID(env->FindClass("java/lang/Integer"), "valueOf", "(I)Ljava/lang/Integer;"); - static auto* const set_double = env->GetStaticMethodID(env->FindClass("java/lang/Double"), "valueOf", "(D)Ljava/lang/Double;"); - static auto* const set_long = env->GetStaticMethodID(env->FindClass("java/lang/Long"), "valueOf", "(J)Ljava/lang/Long;"); - static auto* const set_float = env->GetStaticMethodID(env->FindClass("java/lang/Float"), "valueOf", "(F)Ljava/lang/Float;"); - static auto* const set_short = env->GetStaticMethodID(env->FindClass("java/lang/Short"), "valueOf", "(S)Ljava/lang/Short;"); - static auto* const set_byte = env->GetStaticMethodID(env->FindClass("java/lang/Byte"), "valueOf", "(B)Ljava/lang/Byte;"); - static auto* const set_char = env->GetStaticMethodID(env->FindClass("java/lang/Character"), "valueOf", "(C)Ljava/lang/Character;"); - static auto* const set_boolean = env->GetStaticMethodID(env->FindClass("java/lang/Boolean"), "valueOf", "(Z)Ljava/lang/Boolean;"); - - auto target = env->FromReflectedMethod(method); - auto param_len = env->GetArrayLength(shorty) - 1; - if (env->GetArrayLength(args) != param_len) { - env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "args.length != parameters.length"); - return nullptr; - } - if (thiz == nullptr) { - env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "this == null"); - return nullptr; - } - std::vector a(param_len); - auto *const shorty_char = env->GetCharArrayElements(shorty, nullptr); - for (jint i = 0; i != param_len; ++i) { - jobject element; - switch(shorty_char[i + 1]) { - case 'I': - a[i].i = env->CallIntMethod(element = env->GetObjectArrayElement(args, i), get_int); - break; - case 'D': - a[i].d = env->CallDoubleMethod(element = env->GetObjectArrayElement(args, i), get_double); - break; - case 'J': - a[i].j = env->CallLongMethod(element = env->GetObjectArrayElement(args, i), get_long); - break; - case 'F': - a[i].f = env->CallFloatMethod(element = env->GetObjectArrayElement(args, i), get_float); - break; - case 'S': - a[i].s = env->CallShortMethod(element = env->GetObjectArrayElement(args, i), get_short); - break; - case 'B': - a[i].b = env->CallByteMethod(element = env->GetObjectArrayElement(args, i), get_byte); - break; - case 'C': - a[i].c = env->CallCharMethod(element = env->GetObjectArrayElement(args, i), get_char); - break; - case 'Z': - a[i].z = env->CallBooleanMethod(element = env->GetObjectArrayElement(args, i), get_boolean); - break; - default: - case 'L': - a[i].l = env->GetObjectArrayElement(args, i); - element = nullptr; - break; - } - if (element) env->DeleteLocalRef(element); - if (env->ExceptionCheck()) return nullptr; - } - jobject value = nullptr; - switch(shorty_char[0]) { - case 'I': - value = env->CallStaticObjectMethod(jclass{nullptr}, set_int, env->CallNonvirtualIntMethodA(thiz, cls, target, a.data())); - break; - case 'D': - value = env->CallStaticObjectMethod(jclass{nullptr}, set_double, env->CallNonvirtualDoubleMethodA(thiz, cls, target, a.data())); - break; - case 'J': - value = env->CallStaticObjectMethod(jclass{nullptr}, set_long, env->CallNonvirtualLongMethodA(thiz, cls, target, a.data())); - break; - case 'F': - value = env->CallStaticObjectMethod(jclass{nullptr}, set_float, env->CallNonvirtualFloatMethodA(thiz, cls, target, a.data())); - break; - case 'S': - value = env->CallStaticObjectMethod(jclass{nullptr}, set_short, env->CallNonvirtualShortMethodA(thiz, cls, target, a.data())); - break; - case 'B': - value = env->CallStaticObjectMethod(jclass{nullptr}, set_byte, env->CallNonvirtualByteMethodA(thiz, cls, target, a.data())); - break; - case 'C': - value = env->CallStaticObjectMethod(jclass{nullptr}, set_char, env->CallNonvirtualCharMethodA(thiz, cls, target, a.data())); - break; - case 'Z': - value = env->CallStaticObjectMethod(jclass{nullptr}, set_boolean, env->CallNonvirtualBooleanMethodA(thiz, cls, target, a.data())); - break; - case 'L': - value = env->CallNonvirtualObjectMethodA(thiz, cls, target, a.data()); - break; - default: - case 'V': - env->CallNonvirtualVoidMethodA(thiz, cls, target, a.data()); - break; - } - env->ReleaseCharArrayElements(shorty, shorty_char, JNI_ABORT); - return value; -} - -LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, instanceOf, jobject object, jclass expected_class) { - return env->IsInstanceOf(object, expected_class); -} - -LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, setTrusted, jobject cookie) { - return lsplant::MakeDexFileTrusted(env, cookie); -} - -LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass callback_class, jobject method) { - auto target = env->FromReflectedMethod(method); - HookItem *hook_item = nullptr; - hooked_methods.if_contains(target, [&hook_item](const auto &it) { - hook_item = it.second.get(); - }); - if (!hook_item) return nullptr; - jobject backup = hook_item->GetBackup(); - if (!backup) return nullptr; - JNIMonitor monitor(env, backup); - - auto res = env->NewObjectArray(2, env->FindClass("[Ljava/lang/Object;"), nullptr); - auto modern = env->NewObjectArray((jsize) hook_item->modern_callbacks.size(), env->FindClass("java/lang/Object"), nullptr); - auto legacy = env->NewObjectArray((jsize) hook_item->legacy_callbacks.size(), env->FindClass("java/lang/Object"), nullptr); - for (jsize i = 0; auto callback: hook_item->modern_callbacks) { - auto before_method = JNI_ToReflectedMethod(env, clazz, callback.second.before_method, JNI_TRUE); - auto after_method = JNI_ToReflectedMethod(env, clazz, callback.second.after_method, JNI_TRUE); - auto callback_object = JNI_NewObject(env, callback_class, callback_ctor, before_method, after_method); - env->SetObjectArrayElement(modern, i++, env->NewLocalRef(callback_object.get())); - } - for (jsize i = 0; auto callback: hook_item->legacy_callbacks) { - env->SetObjectArrayElement(legacy, i++, env->NewLocalRef(callback.second)); - } - env->SetObjectArrayElement(res, 0, modern); - env->SetObjectArrayElement(res, 1, legacy); - return res; -} - -LSP_DEF_NATIVE_METHOD(jobject, HookBridge, getStaticInitializer, jclass target_class) { - // is the internal name for a static initializer. - // Its signature is always ()V (no arguments, void return). - jmethodID mid = env->GetStaticMethodID(target_class, "", "()V"); - if (!mid) { - // If GetStaticMethodID fails, it throws an exception. - // We clear it and return null to let the Java side handle it gracefully. - env->ExceptionClear(); - return nullptr; - } - // Convert the method ID to a java.lang.reflect.Method object. - // The last parameter must be JNI_TRUE because it's a static method. - return env->ToReflectedMethod(target_class, mid, JNI_TRUE); -} - -static JNINativeMethod gMethods[] = { - LSP_NATIVE_METHOD(HookBridge, hookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Class;ILjava/lang/Object;)Z"), - LSP_NATIVE_METHOD(HookBridge, unhookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Object;)Z"), - LSP_NATIVE_METHOD(HookBridge, deoptimizeMethod, "(Ljava/lang/reflect/Executable;)Z"), - LSP_NATIVE_METHOD(HookBridge, invokeOriginalMethod, "(Ljava/lang/reflect/Executable;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"), - LSP_NATIVE_METHOD(HookBridge, invokeSpecialMethod, "(Ljava/lang/reflect/Executable;[CLjava/lang/Class;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"), - LSP_NATIVE_METHOD(HookBridge, allocateObject, "(Ljava/lang/Class;)Ljava/lang/Object;"), - LSP_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"), - LSP_NATIVE_METHOD(HookBridge, setTrusted, "(Ljava/lang/Object;)Z"), - LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/Executable;)[[Ljava/lang/Object;"), - LSP_NATIVE_METHOD(HookBridge, getStaticInitializer, "(Ljava/lang/Class;)Ljava/lang/reflect/Method;"), -}; - -void RegisterHookBridge(JNIEnv *env) { - jclass method = env->FindClass("java/lang/reflect/Method"); - invoke = env->GetMethodID( - method, "invoke", - "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"); - env->DeleteLocalRef(method); - REGISTER_LSP_NATIVE_METHODS(HookBridge); -} -} // namespace lspd diff --git a/core/src/main/jni/src/jni/hook_bridge.h b/core/src/main/jni/src/jni/hook_bridge.h deleted file mode 100644 index d2c99465f..000000000 --- a/core/src/main/jni/src/jni/hook_bridge.h +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ -#pragma once - -#include - -namespace lspd { - void RegisterHookBridge(JNIEnv *env); -} diff --git a/core/src/main/jni/src/jni/native_api.cpp b/core/src/main/jni/src/jni/native_api.cpp deleted file mode 100644 index ff41f4ed3..000000000 --- a/core/src/main/jni/src/jni/native_api.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -// -// Created by 双草酸酯 on 2/7/21. -// -#include "native_api.h" -#include "native_util.h" -#include "utils/jni_helper.hpp" -#include "../native_api.h" - -using namespace lsplant; - -namespace lspd { - LSP_DEF_NATIVE_METHOD(void, NativeAPI, recordNativeEntrypoint, jstring jstr) { - lsplant::JUTFString str(env, jstr); - RegisterNativeLib(str); - } - - static JNINativeMethod gMethods[] = { - LSP_NATIVE_METHOD(NativeAPI, recordNativeEntrypoint, "(Ljava/lang/String;)V") - }; - - void RegisterNativeAPI(JNIEnv *env) { - REGISTER_LSP_NATIVE_METHODS(NativeAPI); - } -} diff --git a/core/src/main/jni/src/jni/native_api.h b/core/src/main/jni/src/jni/native_api.h deleted file mode 100644 index be541808c..000000000 --- a/core/src/main/jni/src/jni/native_api.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -#pragma once - -#include - -namespace lspd { - void RegisterNativeAPI(JNIEnv *); -} diff --git a/core/src/main/jni/src/jni/resources_hook.cpp b/core/src/main/jni/src/jni/resources_hook.cpp deleted file mode 100644 index 18d8246db..000000000 --- a/core/src/main/jni/src/jni/resources_hook.cpp +++ /dev/null @@ -1,230 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -#include -#include "dex_builder.h" -#include "framework/androidfw/resource_types.h" -#include "elf_util.h" -#include "native_util.h" -#include "resources_hook.h" -#include "config_bridge.h" - -using namespace lsplant; - -namespace lspd { - using TYPE_GET_ATTR_NAME_ID = int32_t (*)(void *, int); - - using TYPE_STRING_AT = char16_t *(*)(const void *, int32_t, size_t *); - - using TYPE_RESTART = void (*)(void *); - - using TYPE_NEXT = int32_t (*)(void *); - - static jclass classXResources; - static jmethodID methodXResourcesTranslateAttrId; - static jmethodID methodXResourcesTranslateResId; - - static TYPE_NEXT ResXMLParser_next = nullptr; - static TYPE_RESTART ResXMLParser_restart = nullptr; - static TYPE_GET_ATTR_NAME_ID ResXMLParser_getAttributeNameID = nullptr; - - static std::string GetXResourcesClassName() { - auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); - if (obfs_map.empty()) { - LOGW("GetXResourcesClassName: obfuscation_map empty?????"); - } - static auto name = lspd::JavaNameToSignature( - obfs_map.at("android.content.res.XRes")) // TODO: kill this hardcoded name - .substr(1) + "ources"; - LOGD("{}", name.c_str()); - return name; - } - - static bool PrepareSymbols() { - SandHook::ElfImg fw(kLibFwName); - if (!fw.isValid()) { - return false; - }; - if (!(ResXMLParser_next = fw.getSymbAddress( - "_ZN7android12ResXMLParser4nextEv"))) { - return false; - } - if (!(ResXMLParser_restart = fw.getSymbAddress( - "_ZN7android12ResXMLParser7restartEv"))) { - return false; - }; - if (!(ResXMLParser_getAttributeNameID = fw.getSymbAddress( - LP_SELECT("_ZNK7android12ResXMLParser18getAttributeNameIDEj", - "_ZNK7android12ResXMLParser18getAttributeNameIDEm")))) { - return false; - } - return android::ResStringPool::setup(InitInfo { - .art_symbol_resolver = [&](auto s) { - return fw.template getSymbAddress<>(s); - } - }); - } - - LSP_DEF_NATIVE_METHOD(jboolean, ResourcesHook, initXResourcesNative) { - const auto x_resources_class_name = GetXResourcesClassName(); - if (auto classXResources_ = Context::GetInstance()->FindClassFromCurrentLoader(env, - x_resources_class_name)) { - classXResources = JNI_NewGlobalRef(env, classXResources_); - } else { - LOGE("Error while loading XResources class '{}':", x_resources_class_name); - return JNI_FALSE; - } - methodXResourcesTranslateResId = JNI_GetStaticMethodID( - env, classXResources, "translateResId", - fmt::format("(IL{};Landroid/content/res/Resources;)I", x_resources_class_name)); - if (!methodXResourcesTranslateResId) { - return JNI_FALSE; - } - methodXResourcesTranslateAttrId = JNI_GetStaticMethodID( - env, classXResources, "translateAttrId", - fmt::format("(Ljava/lang/String;L{};)I", x_resources_class_name)); - if (!methodXResourcesTranslateAttrId) { - return JNI_FALSE; - } - if (!PrepareSymbols()) { - return JNI_FALSE; - } - return JNI_TRUE; - } - - // @ApiSensitive(Level.MIDDLE) - LSP_DEF_NATIVE_METHOD(jboolean, ResourcesHook, makeInheritable, jclass target_class) { - if (MakeClassInheritable(env, target_class)) { - return JNI_TRUE; - } - return JNI_FALSE; - } - - LSP_DEF_NATIVE_METHOD(jobject, ResourcesHook, buildDummyClassLoader, jobject parent, - jstring resource_super_class, jstring typed_array_super_class) { - using namespace startop::dex; - static auto in_memory_classloader = JNI_NewGlobalRef(env, JNI_FindClass(env, - "dalvik/system/InMemoryDexClassLoader")); - static jmethodID initMid = JNI_GetMethodID(env, in_memory_classloader, "", - "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); - DexBuilder dex_file; - - ClassBuilder xresource_builder{ - dex_file.MakeClass("xposed.dummy.XResourcesSuperClass")}; - xresource_builder.setSuperClass(TypeDescriptor::FromClassname(JUTFString(env, resource_super_class).get())); - - ClassBuilder xtypearray_builder{ - dex_file.MakeClass("xposed.dummy.XTypedArraySuperClass")}; - xtypearray_builder.setSuperClass(TypeDescriptor::FromClassname(JUTFString(env, typed_array_super_class).get())); - - slicer::MemView image{dex_file.CreateImage()}; - - auto dex_buffer = env->NewDirectByteBuffer(const_cast(image.ptr()), image.size()); - return JNI_NewObject(env, in_memory_classloader, initMid, - dex_buffer, parent).release(); - } - - LSP_DEF_NATIVE_METHOD(void, ResourcesHook, rewriteXmlReferencesNative, - jlong parserPtr, jobject origRes, jobject repRes) { - auto parser = (android::ResXMLParser *) parserPtr; - - if (parser == nullptr) - return; - - const android::ResXMLTree &mTree = parser->mTree; - auto mResIds = (uint32_t *) mTree.mResIds; - android::ResXMLTree_attrExt *tag; - int attrCount; - - do { - switch (ResXMLParser_next(parser)) { - case android::ResXMLParser::START_TAG: - tag = (android::ResXMLTree_attrExt *) parser->mCurExt; - attrCount = tag->attributeCount; - for (int idx = 0; idx < attrCount; idx++) { - auto attr = (android::ResXMLTree_attribute *) - (((const uint8_t *) tag) - + tag->attributeStart - + tag->attributeSize * idx); - - // find resource IDs for attribute names - int32_t attrNameID = ResXMLParser_getAttributeNameID(parser, idx); - // only replace attribute name IDs for app packages - if (attrNameID >= 0 && (size_t) attrNameID < mTree.mNumResIds && - mResIds[attrNameID] >= 0x7f000000) { - auto attrName = mTree.mStrings.stringAt(attrNameID); - jint attrResID = env->CallStaticIntMethod(classXResources, - methodXResourcesTranslateAttrId, - env->NewString( - (const jchar *) attrName.data_, - attrName.length_), - origRes); - if (env->ExceptionCheck()) - goto leave; - - mResIds[attrNameID] = attrResID; - } - - // find original resource IDs for reference values (app packages only) - if (attr->typedValue.dataType != android::Res_value::TYPE_REFERENCE) - continue; - - jint oldValue = attr->typedValue.data; - if (oldValue < 0x7f000000) - continue; - - jint newValue = env->CallStaticIntMethod(classXResources, - methodXResourcesTranslateResId, - oldValue, origRes, repRes); - if (env->ExceptionCheck()) - goto leave; - - if (newValue != oldValue) - attr->typedValue.data = newValue; - } - continue; - case android::ResXMLParser::END_DOCUMENT: - case android::ResXMLParser::BAD_DOCUMENT: - goto leave; - default: - continue; - } - } while (true); - - leave: - ResXMLParser_restart(parser); - } - - static JNINativeMethod gMethods[] = { - LSP_NATIVE_METHOD(ResourcesHook, initXResourcesNative, "()Z"), - LSP_NATIVE_METHOD(ResourcesHook, makeInheritable,"(Ljava/lang/Class;)Z"), - LSP_NATIVE_METHOD(ResourcesHook, buildDummyClassLoader, - "(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/ClassLoader;"), - LSP_NATIVE_METHOD(ResourcesHook, rewriteXmlReferencesNative, - "(JLandroid/content/res/XResources;Landroid/content/res/Resources;)V") - }; - - void RegisterResourcesHook(JNIEnv *env) { - auto sign = fmt::format("(JL{};Landroid/content/res/Resources;)V", GetXResourcesClassName()); - gMethods[3].signature = sign.c_str(); - - REGISTER_LSP_NATIVE_METHODS(ResourcesHook); - } -} diff --git a/core/src/main/jni/src/jni/resources_hook.h b/core/src/main/jni/src/jni/resources_hook.h deleted file mode 100644 index 5b26063d0..000000000 --- a/core/src/main/jni/src/jni/resources_hook.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -#pragma once - -#include "jni.h" - -namespace lspd { - void RegisterResourcesHook(JNIEnv *); - -} // namespace lspd diff --git a/core/src/main/jni/src/native_api.cpp b/core/src/main/jni/src/native_api.cpp deleted file mode 100644 index ad56ee422..000000000 --- a/core/src/main/jni/src/native_api.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -// -// Created by kotori on 2/4/21. -// - -#include "native_api.h" -#include "logging.h" -#include "utils/hook_helper.hpp" -#include -#include -#include -#include "elf_util.h" -#include "symbol_cache.h" - - -/* - * Module: define xposed_native file in /assets, each line is a .so file name - * LSP: Hook do_dlopen, if any .so file matches the name above, try to call - * "native_init(void*)" function in target so with function pointer of "init" below. - * Module: Call init function with the pointer of callback function. - * LSP: Store the callback function pointer (multiple callback allowed) and return - * LsposedNativeAPIEntries struct. - * Module: Since JNI is not yet available at that time, module can store the struct to somewhere else, - * and handle them in JNI_Onload or later. - * Module: Do some MAGIC provided by LSPosed framework. - * LSP: If any so loaded by target app, we will send a callback to the specific module callback function. - * But an exception is, if the target skipped dlopen and handle linker stuffs on their own, the - * callback will not work. - */ - -using lsplant::operator""_sym; - -namespace lspd { - - std::list moduleLoadedCallbacks; - std::list moduleNativeLibs; - std::unique_ptr> protected_page( - mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0), - [](void *ptr) { munmap(ptr, 4096); }); - - const auto[entries] = []() { - auto *entries = new(protected_page.get()) NativeAPIEntries{ - .version = 2, - .hookFunc = &HookInline, - .unhookFunc = &UnhookInline, - }; - - mprotect(protected_page.get(), 4096, PROT_READ); - return std::make_tuple(entries); - }(); - - void RegisterNativeLib(const std::string &library_name) { - static bool initialized = []() { - return InstallNativeAPI(lsplant::InitInfo { - .inline_hooker = [](auto t, auto r) { - void* bk = nullptr; - return HookInline(t, r, &bk) == 0 ? bk : nullptr; - }, - .art_symbol_resolver = [](auto symbol){ - return GetLinker()->getSymbAddress(symbol); - }, - }); - }(); - if (!initialized) [[unlikely]] return; - LOGD("native_api: Registered {}", library_name); - moduleNativeLibs.push_back(library_name); - } - - bool hasEnding(std::string_view fullString, std::string_view ending) { - if (fullString.length() >= ending.length()) { - return (0 == fullString.compare(fullString.length() - ending.length(), ending.length(), - ending)); - } - return false; - } - - inline static auto do_dlopen_ = "__dl__Z9do_dlopenPKciPK17android_dlextinfoPKv"_sym.hook->*[] - - (const char* name, int flags, const void* extinfo, const void* caller_addr) static -> void* { - auto *handle = backup(name, flags, extinfo, caller_addr); - std::string ns; - if (name) { - ns = std::string(name); - } else { - ns = "NULL"; - } - LOGD("native_api: do_dlopen({})", ns); - if (handle == nullptr) { - return handle; - } - for (std::string_view module_lib: moduleNativeLibs) { - // the so is a module so - if (hasEnding(ns, module_lib)) [[unlikely]] { - LOGD("Loading module native library {}", module_lib); - void *native_init_sym = dlsym(handle, "native_init"); - if (native_init_sym == nullptr) [[unlikely]] { - LOGD("Failed to get symbol \"native_init\" from library {}", - module_lib); - break; - } - auto native_init = reinterpret_cast(native_init_sym); - auto *callback = native_init(entries); - if (callback) { - moduleLoadedCallbacks.push_back(callback); - // return directly to avoid module interaction - return handle; - } - } - } - - // Callbacks - for (auto &callback: moduleLoadedCallbacks) { - callback(name, handle); - } - return handle; - }; - - bool InstallNativeAPI(const lsplant::HookHandler & handler) { - return handler(do_dlopen_); - } -} diff --git a/core/src/main/jni/src/native_api.h b/core/src/main/jni/src/native_api.h deleted file mode 100644 index 4c6c4388e..000000000 --- a/core/src/main/jni/src/native_api.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -// -// Created by kotori on 2/4/21. -// - -#ifndef LSPOSED_NATIVE_API_H -#define LSPOSED_NATIVE_API_H - -#include -#include -#include -#include - -#include "config.h" -#include "utils/hook_helper.hpp" - -typedef int (*HookFunType)(void *func, void *replace, void **backup); - -typedef int (*UnhookFunType)(void *func); - -typedef void (*NativeOnModuleLoaded)(const char *name, void *handle); - -typedef struct { - uint32_t version; - HookFunType hookFunc; - UnhookFunType unhookFunc; -} NativeAPIEntries; - -typedef NativeOnModuleLoaded (*NativeInit)(const NativeAPIEntries *entries); - -namespace lspd { - bool InstallNativeAPI(const lsplant::HookHandler& handler); - - void RegisterNativeLib(const std::string &library_name); - - inline int HookInline(void *original, void *replace, void **backup) { - if constexpr (isDebug) { - Dl_info info; - if (dladdr(original, &info)) - LOGD("Dobby hooking {} ({}) from {} ({})", - info.dli_sname ? info.dli_sname : "(unknown symbol)", - info.dli_saddr ? info.dli_saddr : original, - info.dli_fname ? info.dli_fname : "(unknown file)", info.dli_fbase); - } - return DobbyHook(original, reinterpret_cast(replace), reinterpret_cast(backup)); - } - - inline int UnhookInline(void *original) { - if constexpr (isDebug) { - Dl_info info; - if (dladdr(original, &info)) - LOGD("Dobby unhooking {} ({}) from {} ({})", - info.dli_sname ? info.dli_sname : "(unknown symbol)", - info.dli_saddr ? info.dli_saddr : original, - info.dli_fname ? info.dli_fname : "(unknown file)", info.dli_fbase); - } - return DobbyDestroy(original); - } -} - -#endif //LSPOSED_NATIVE_API_H diff --git a/core/src/main/jni/src/symbol_cache.cpp b/core/src/main/jni/src/symbol_cache.cpp deleted file mode 100644 index 826333f8b..000000000 --- a/core/src/main/jni/src/symbol_cache.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -// -// Created by kotori on 2/7/21. -// - -#include "symbol_cache.h" -#include "elf_util.h" -#include "macros.h" -#include "config.h" -#include -#include - -namespace lspd { - std::unique_ptr &GetArt(bool release) { - static std::unique_ptr kArtImg = nullptr; - if (release) { - kArtImg.reset(); - } else if (!kArtImg) { - kArtImg = std::make_unique(kLibArtName); - } - return kArtImg; - } - - std::unique_ptr &GetLibBinder(bool release) { - static std::unique_ptr kImg = nullptr; - if (release) { - kImg.reset(); - } else if (!kImg) { - kImg = std::make_unique(kLibBinderName); - } - return kImg; - } - - std::unique_ptr &GetLinker(bool release) { - static std::unique_ptr kImg = nullptr; - if (release) { - kImg.reset(); - } else if (!kImg) { - kImg = std::make_unique(kLinkerName); - } - return kImg; - } -} // namespace lspd diff --git a/core/src/main/jni/template/config.cpp b/core/src/main/jni/template/config.cpp deleted file mode 100644 index 3f6e95681..000000000 --- a/core/src/main/jni/template/config.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "config.h" - -namespace lspd { -const int versionCode = ${VERSION_CODE}; -const char* const versionName = "${VERSION_NAME}"; -} From c1ab555468c4db8839f0deed10e66fa0da6012b4 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 4 Mar 2026 23:48:19 +0100 Subject: [PATCH 15/24] update doc --- zygisk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zygisk/README.md b/zygisk/README.md index 61639cd22..b1f44786d 100644 --- a/zygisk/README.md +++ b/zygisk/README.md @@ -67,7 +67,7 @@ The full lifecycle of a Vector-instrumented process follows this sequence: ### The IPC Protocol The communication between the native loader and the Kotlin framework relies on specific conventions: -* **Transaction Code**: The custom code `_LSP` (bitwise constructed) must remain synchronized between `ipc_bridge.cpp` (Native) and `BridgeService.kt` (Kotlin). +* **Transaction Code**: The custom code `_VEC` (bitwise constructed) must remain synchronized between `ipc_bridge.cpp` (Native) and `BridgeService.kt` (Kotlin). * **The "Out-Parameter" List**: In `ParasiticManagerHooker.start()`, you will see an empty list `mutableListOf()`. It is used as an "out-parameter" for the Binder call, allowing the root daemon to push the Manager Service Binder back to the loader. From 8c0bfda928f978214eeab253d6b35c24600bc2ad Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 4 Mar 2026 23:48:55 +0100 Subject: [PATCH 16/24] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 45633 -> 46175 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f8e1ee3125fe0768e9a76ee977ac089eb657005e..61285a659d17295f1de7c53e24fdf13ad755c379 100644 GIT binary patch delta 37058 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 NNswbwE82Y!{{uvZmpcFe delta 36446 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 JWd7DQ`9Hc?h(rJY diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bad7c2462..37f78a6af 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 17a14b98cb10dec2f02202062c5fd7712765ecd0 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 5 Mar 2026 00:30:47 +0100 Subject: [PATCH 17/24] Fix file names in CI workflow --- .github/workflows/core.yml | 39 +++++++------------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index c690959a1..c0a3d55ce 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -2,11 +2,6 @@ name: Core on: workflow_dispatch: - inputs: - post_telegram: - description: 'Post to Telegram' - required: true - type: boolean push: branches: [ master ] tags: [ v* ] @@ -86,29 +81,29 @@ jobs: if: success() id: prepareArtifact run: | - zygiskReleaseName=`ls magisk-loader/release/LSPosed-v*-zygisk-release.zip | awk -F '(/|.zip)' '{print $3}'` && echo "zygiskReleaseName=$zygiskReleaseName" >> $GITHUB_OUTPUT - zygiskDebugName=`ls magisk-loader/release/LSPosed-v*-zygisk-debug.zip | awk -F '(/|.zip)' '{print $3}'` && echo "zygiskDebugName=$zygiskDebugName" >> $GITHUB_OUTPUT - unzip magisk-loader/release/LSPosed-v*-zygisk-release.zip -d LSPosed-zygisk-release - unzip magisk-loader/release/LSPosed-v*-zygisk-debug.zip -d LSPosed-zygisk-debug + zygiskReleaseName=`ls zygisk/release/Vector-v*-Release.zip | awk -F '(/|.zip)' '{print $3}'` && echo "zygiskReleaseName=$zygiskReleaseName" >> $GITHUB_OUTPUT + zygiskDebugName=`ls zygisk/release/Vector-v*-Debug.zip | awk -F '(/|.zip)' '{print $3}'` && echo "zygiskDebugName=$zygiskDebugName" >> $GITHUB_OUTPUT + unzip zygisk/release/Vector-v*-Release.zip -d Vector-Release + unzip zygisk/release/Vector-v*-Debug.zip -d Vector-Debug - name: Upload zygisk release uses: actions/upload-artifact@v6 with: name: ${{ steps.prepareArtifact.outputs.zygiskReleaseName }} - path: "./LSPosed-zygisk-release/*" + path: "./Vector-Release/*" - name: Upload zygisk debug uses: actions/upload-artifact@v6 with: name: ${{ steps.prepareArtifact.outputs.zygiskDebugName }} - path: "./LSPosed-zygisk-debug/*" + path: "./Vector-Debug/*" - name: Upload mappings uses: actions/upload-artifact@v6 with: name: mappings path: | - magisk-loader/build/outputs/mapping + zygisk/build/outputs/mapping app/build/outputs/mapping - name: Upload symbols @@ -117,23 +112,3 @@ jobs: name: symbols path: build/symbols - - name: Post to channel - if: ${{ success() && github.event_name != 'pull_request' && github.ref == 'refs/heads/master' && github.ref_type != 'tag' && inputs.post_telegram != 'false' }} - env: - CHANNEL_ID: ${{ secrets.CHANNEL_ID }} - DISCUSSION_ID: ${{ secrets.DISCUSSION_ID }} - TOPIC_ID: ${{ secrets.TOPIC_ID }} - BOT_TOKEN: ${{ secrets.BOT_TOKEN }} - COMMIT_MESSAGE: ${{ github.event.head_commit.message }} - COMMIT_URL: ${{ github.event.head_commit.url }} - run: | - if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then - OUTPUT="magisk-loader/release/" - export riruRelease=$(find $OUTPUT -name "LSPosed-v*-riru-release.zip") - export riruDebug=$(find $OUTPUT -name "LSPosed-v*-riru-debug.zip") - export zygiskRelease=$(find $OUTPUT -name "LSPosed-v*-zygisk-release.zip") - export zygiskDebug=$(find $OUTPUT -name "LSPosed-v*-zygisk-debug.zip") - ESCAPED=`python3 -c 'import json,os,urllib.parse; msg = json.dumps(os.environ["COMMIT_MESSAGE"]); print(urllib.parse.quote(msg if len(msg) <= 1024 else json.dumps(os.environ["COMMIT_URL"])))'` - curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FriruRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FzygiskRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FriruDebug%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FzygiskDebug%22%2C%22caption%22:${ESCAPED}%7D%5D" -F riruRelease="@$riruRelease" -F riruDebug="@$riruDebug" -F zygiskRelease="@$zygiskRelease" -F zygiskDebug="@$zygiskDebug" - # curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${DISCUSSION_ID}&message_thread_id=${TOPIC_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FriruRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FzygiskRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FriruDebug%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FzygiskDebug%22%2C%22caption%22:${ESCAPED}%7D%5D" -F riruRelease="@$riruRelease" -F riruDebug="@$riruDebug" -F zygiskRelease="@$zygiskRelease" -F zygiskDebug="@$zygiskDebug" - fi From 0cf71ee8862b32184ee33101ad8de816bd853b17 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 5 Mar 2026 08:41:42 +0100 Subject: [PATCH 18/24] Improve bias parsing of elf binary --- native/src/elf/elf_image.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/native/src/elf/elf_image.cpp b/native/src/elf/elf_image.cpp index eb86fc0ff..cb4ff16da 100644 --- a/native/src/elf/elf_image.cpp +++ b/native/src/elf/elf_image.cpp @@ -106,7 +106,11 @@ void ElfImage::parseHeaders(ElfW(Ehdr) * header) { case SHT_PROGBITS: // The load bias is the difference between // the virtual address of a loaded segment and its offset in the file. - // We calculate it once. + + // Ensure we skip early sections like .interp or .note + // by waiting until after dynsym and strtab are found. + if (dynsym_ == nullptr || strtab_start_ == nullptr) break; + if (bias_ == 0 && section_h->sh_flags & SHF_ALLOC && section_h->sh_addr > 0) { bias_ = section_h->sh_addr - section_h->sh_offset; } From 607c8f3317843a9dbe7a775eb205ad187f347911 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 5 Mar 2026 16:41:27 +0100 Subject: [PATCH 19/24] Change directory for daemon script --- zygisk/module/service.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zygisk/module/service.sh b/zygisk/module/service.sh index 88300abab..e49f8859b 100644 --- a/zygisk/module/service.sh +++ b/zygisk/module/service.sh @@ -1,5 +1,6 @@ -# Extract the directory path +# Extract the directory path and change directory MODDIR="${0%/*}" +cd "$MODDIR" || exit 1 # Start the daemon directly in the background within a private mount namespace unshare --propagation slave -m "$MODDIR/daemon" --system-server-max-retry=3 "$@" & From ebda7e08782ce110cb41a4ecbdd8806689eb5cd6 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 5 Mar 2026 17:28:43 +0100 Subject: [PATCH 20/24] Set boolean flag for bias_ initialization Using bias_ comparison could be buggy --- native/include/elf/elf_image.h | 1 + native/src/elf/elf_image.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/native/include/elf/elf_image.h b/native/include/elf/elf_image.h index 9cf19bd39..e0b768400 100644 --- a/native/include/elf/elf_image.h +++ b/native/include/elf/elf_image.h @@ -135,6 +135,7 @@ class ElfImage { void *file_map_ = nullptr; size_t file_size_ = 0; ElfW(Addr) bias_ = 0; + bool bias_calculated_ = false; // Pointers into the mapped ELF file data. ElfW(Ehdr) *header_ = nullptr; diff --git a/native/src/elf/elf_image.cpp b/native/src/elf/elf_image.cpp index cb4ff16da..7d2df6703 100644 --- a/native/src/elf/elf_image.cpp +++ b/native/src/elf/elf_image.cpp @@ -111,8 +111,9 @@ void ElfImage::parseHeaders(ElfW(Ehdr) * header) { // by waiting until after dynsym and strtab are found. if (dynsym_ == nullptr || strtab_start_ == nullptr) break; - if (bias_ == 0 && section_h->sh_flags & SHF_ALLOC && section_h->sh_addr > 0) { + if (!bias_calculated_ && section_h->sh_flags & SHF_ALLOC && section_h->sh_addr > 0) { bias_ = section_h->sh_addr - section_h->sh_offset; + bias_calculated_ = true; } break; case SHT_HASH: From fe15eb6ae64bfebcbe0abfb9fda216cfc83ae112 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 5 Mar 2026 22:14:27 +0100 Subject: [PATCH 21/24] Avoid leaving log trace in release version --- zygisk/src/main/cpp/ipc_bridge.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zygisk/src/main/cpp/ipc_bridge.cpp b/zygisk/src/main/cpp/ipc_bridge.cpp index 839a15445..4e81f96a9 100644 --- a/zygisk/src/main/cpp/ipc_bridge.cpp +++ b/zygisk/src/main/cpp/ipc_bridge.cpp @@ -293,7 +293,7 @@ lsplant::ScopedLocalRef IPCBridge::RequestAppBinder(JNIEnv *env, jstrin } } } else { - LOGW("Transact call to request app binder failed."); + LOGD("Transact call to request app binder failed."); } return result_binder; From f77b1142f3e8181e33f78844fc9aeb515b112695 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 6 Mar 2026 07:21:49 +0100 Subject: [PATCH 22/24] Fix LD_PRELOAD namespace restriction using memfd The Android dynamic linker (Bionic) restricts loading libraries from unauthorized paths via LD_PRELOAD. This causes the linker to reject the hook library with a "not accessible for the namespace" fatal error. To bypass this restriction, we exploit a fallback in Bionic that explicitly skips the namespace accessibility check for files located on tmpfs. We use `memfd_create` to create an anonymous tmpfs-backed file descriptor, copy the library into it using `sendfile`, and pass the new memfd to LD_PRELOAD instead. Reference: function `load_library` in https://cs.android.com/android/platform/superproject/main/+/main:bionic/linker/linker.cpp --- zygisk/module/sepolicy.rule | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zygisk/module/sepolicy.rule b/zygisk/module/sepolicy.rule index d0b254e05..cc170e6b2 100644 --- a/zygisk/module/sepolicy.rule +++ b/zygisk/module/sepolicy.rule @@ -7,7 +7,7 @@ type xposed_file file_type typeattribute xposed_file mlstrustedobject allow {dex2oat installd isolated_app shell} xposed_file {file dir} * -allow dex2oat unlabeled file * +allow dex2oat {unlabeled tmpfs} file * type xposed_data file_type typeattribute xposed_data mlstrustedobject From 6b44aeeb4b8a08289d4b9b33017e47c11e6ea31a Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 6 Mar 2026 11:05:07 +0100 Subject: [PATCH 23/24] Refactor obfuscation maps in daemon Correct bugs in dynamical construction of XResources methods. --- daemon/src/main/jni/obfuscation.cpp | 318 +++++++++++++++------------- daemon/src/main/jni/obfuscation.h | 92 ++++---- native/include/common/utils.h | 14 -- native/src/jni/resources_hook.cpp | 23 +- 4 files changed, 243 insertions(+), 204 deletions(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index e7adca26e..96d7c2048 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -1,136 +1,136 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -// -// Created by Kotori2 on 2021/12/1. -// +#include "obfuscation.h" +#include +#include +#include #include +#include +#include +#include +#include #include + #include +#include +#include #include -#include -#include -#include -#include -#include -#include -#include "slicer/reader.h" -#include "slicer/writer.h" -#include "obfuscation.h" -#include "logging.h" +#include +#include +#include -using namespace lsplant; namespace { -std::mutex init_lock{}; -std::map signatures = { - {"Lde/robv/android/xposed/", ""}, - { "Landroid/app/AndroidApp", ""}, - { "Landroid/content/res/XRes", ""}, - { "Landroid/content/res/XModule", ""}, - { "Lorg/matrix/vector/core/", ""}, - { "Lorg/matrix/vector/nativebridge/", ""}, - { "Lorg/matrix/vector/service/", ""}, + +std::once_flag init_flag; + +std::map signatures = { + {"Lde/robv/android/xposed/", ""}, {"Landroid/app/AndroidApp", ""}, + {"Landroid/content/res/XRes", ""}, {"Landroid/content/res/XModule", ""}, + {"Lorg/matrix/vector/core/", ""}, {"Lorg/matrix/vector/nativebridge/", ""}, + {"Lorg/matrix/vector/service/", ""}, }; -jclass class_file_descriptor; -jmethodID method_file_descriptor_ctor; +jclass class_file_descriptor = nullptr; +jmethodID method_file_descriptor_ctor = nullptr; -jclass class_shared_memory; -jmethodID method_shared_memory_ctor; +jclass class_shared_memory = nullptr; +jmethodID method_shared_memory_ctor = nullptr; -bool inited = false; -} +} // anonymous namespace +// Converts Dex signatures to Java format. +// Trailing slashes are translated to dots, which correctly aligns with +// Java's string matching expectations for package prefixes. static std::string to_java(const std::string &signature) { std::string java(signature, 1); - replace(java.begin(), java.end(), '/', '.'); + std::replace(java.begin(), java.end(), '/', '.'); return java; } -void maybeInit(JNIEnv *env) { - if (inited) [[likely]] return; - std::lock_guard l(init_lock); - LOGD("ObfuscationManager.init"); - if (auto file_descriptor = JNI_FindClass(env, "java/io/FileDescriptor")) { - class_file_descriptor = JNI_NewGlobalRef(env, file_descriptor); - } else return; - - method_file_descriptor_ctor = JNI_GetMethodID(env, class_file_descriptor, "", "(I)V"); - - if (auto shared_memory = JNI_FindClass(env, "android/os/SharedMemory")) { - class_shared_memory = JNI_NewGlobalRef(env, shared_memory); - } else return; - - method_shared_memory_ctor = JNI_GetMethodID(env, class_shared_memory, "", "(Ljava/io/FileDescriptor;)V"); - - auto regen = [](std::string_view original_signature) { - static auto& chrs = "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - thread_local static std::mt19937 rg{std::random_device{}()}; - thread_local static std::uniform_int_distribution pick(0, sizeof(chrs) - 2); - thread_local static std::uniform_int_distribution choose_slash(0, 10); - - std::string out; - size_t length = original_signature.size(); - out.reserve(length); - out += "L"; +static void ensureInitialized(JNIEnv *env) { + // Thread-safe one-time initialization + std::call_once(init_flag, [&]() { + LOGD("ObfuscationManager.init"); + + if (auto file_descriptor = lsplant::JNI_FindClass(env, "java/io/FileDescriptor")) { + class_file_descriptor = + static_cast(lsplant::JNI_NewGlobalRef(env, file_descriptor)); + } else + return; + + method_file_descriptor_ctor = + lsplant::JNI_GetMethodID(env, class_file_descriptor, "", "(I)V"); + + if (auto shared_memory = lsplant::JNI_FindClass(env, "android/os/SharedMemory")) { + class_shared_memory = + static_cast(lsplant::JNI_NewGlobalRef(env, shared_memory)); + } else + return; + + method_shared_memory_ctor = lsplant::JNI_GetMethodID(env, class_shared_memory, "", + "(Ljava/io/FileDescriptor;)V"); + + auto regen = [](std::string_view original_signature) { + static constexpr auto chrs = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + thread_local static std::mt19937 rg{std::random_device{}()}; + thread_local static std::uniform_int_distribution pick( + 0, strlen(chrs) - 1); + thread_local static std::uniform_int_distribution choose_slash( + 0, 10); + + std::string out; + size_t length = original_signature.size(); + out.reserve(length); + out += "L"; + + for (size_t i = 1; i < length - 1; i++) { + if (choose_slash(rg) > 8 && // 20% chance for a slash + out.back() != '/' && // Avoid consecutive slashes + i != 1 && // No slash immediately after 'L' + i != length - 2) { // No slash right before the end + out += "/"; + } else { + out += chrs[pick(rg)]; + } + } - for (size_t i = 1; i < length - 1; i++) { - if (choose_slash(rg) > 8 && // 80% alphabet + 20% slashes - out[i - 1] != '/' && // slashes could not stick together - i != 1 && // the first character should not be slash - i != length - 2) { // and the last character + // Respect the original termination character type to prevent + if (original_signature.back() == '/') { out += "/"; } else { out += chrs[pick(rg)]; } - } - out += "/"; - return out; - }; + if (out.length() != original_signature.length()) { + LOGE("Length mismatch! Org: %zu vs New: %zu. '%s' -> '%s'", + original_signature.length(), out.length(), + std::string(original_signature).c_str(), out.c_str()); + } - for (auto &i: signatures) { - i.second = regen(i.first); - LOGD("%s => %s", i.first.c_str(), i.second.c_str()); - } + return out; + }; + + for (auto &i : signatures) { + i.second = regen(i.first); + LOGD("%s => %s", i.first.c_str(), i.second.c_str()); + } - LOGD("ObfuscationManager init successfully"); - inited = true; + LOGD("ObfuscationManager init successfully"); + }); } -// https://stackoverflow.com/questions/4844022/jni-create-hashmap with modifications -jobject stringMapToJavaHashMap(JNIEnv *env, const decltype(signatures)& map) { +static jobject stringMapToJavaHashMap(JNIEnv *env, const std::map &map) { jclass mapClass = env->FindClass("java/util/HashMap"); - if(mapClass == nullptr) - return nullptr; + if (mapClass == nullptr) return nullptr; jmethodID init = env->GetMethodID(mapClass, "", "()V"); jobject hashMap = env->NewObject(mapClass, init); - jmethodID put = env->GetMethodID(mapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); + jmethodID put = env->GetMethodID(mapClass, "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); - auto citr = map.begin(); - for( ; citr != map.end(); ++citr) { - jstring keyJava = env->NewStringUTF(citr->first.c_str()); - jstring valueJava = env->NewStringUTF(citr->second.c_str()); + for (const auto &[key, value] : map) { + jstring keyJava = env->NewStringUTF(key.c_str()); + jstring valueJava = env->NewStringUTF(value.c_str()); env->CallObjectMethod(hashMap, put, keyJava, valueJava); @@ -138,72 +138,106 @@ jobject stringMapToJavaHashMap(JNIEnv *env, const decltype(signatures)& map) { env->DeleteLocalRef(valueJava); } - auto hashMapGobal = static_cast(env->NewGlobalRef(hashMap)); + jobject hashMapGlobal = env->NewGlobalRef(hashMap); env->DeleteLocalRef(hashMap); env->DeleteLocalRef(mapClass); - return hashMapGobal; + return hashMapGlobal; } -extern "C" -JNIEXPORT jobject JNICALL -Java_org_lsposed_lspd_service_ObfuscationManager_getSignatures(JNIEnv *env, [[maybe_unused]] jclass obfuscation_manager) { - maybeInit(env); +extern "C" JNIEXPORT jobject JNICALL Java_org_lsposed_lspd_service_ObfuscationManager_getSignatures( + JNIEnv *env, [[maybe_unused]] jclass clazz) { + ensureInitialized(env); + static jobject signatures_jni = nullptr; - if (signatures_jni) return signatures_jni; - decltype(signatures) signatures_java; - for (const auto &i: signatures) { - signatures_java[to_java(i.first)] = to_java(i.second); - } - signatures_jni = stringMapToJavaHashMap(env, signatures_java); + static std::once_flag jni_map_flag; + + // Thread-safe, one-time JNI HashMap translation + std::call_once(jni_map_flag, [&]() { + std::map signatures_java; + for (const auto &i : signatures) { + signatures_java[to_java(i.first)] = to_java(i.second); + } + signatures_jni = stringMapToJavaHashMap(env, signatures_java); + }); + return signatures_jni; } -static int obfuscateDex(const void *dex, size_t size) { - // const char* new_sig = obfuscated_signature.c_str(); - dex::Reader reader{reinterpret_cast(dex), size}; - +static int obfuscateDexBuffer(const void *dex_data, size_t size) { + // LOGD("obfuscateDexBuffer: dex_data=%p, size=%zu", dex_data, size); + dex::Reader reader{reinterpret_cast(dex_data), size}; reader.CreateFullIr(); auto ir = reader.GetIr(); - for (auto &i: ir->strings) { + + LOGD("Mutating strings in-place"); + // Mutate strings in-place. + for (auto &i : ir->strings) { const char *s = i->c_str(); - for (const auto &signature: signatures) { - char* p = const_cast(strstr(s, signature.first.c_str())); - if (p) { - auto new_sig = signature.second.c_str(); - // NOLINTNEXTLINE bugprone-not-null-terminated-result - memcpy(p, new_sig, strlen(new_sig)); - } + for (const auto &signature : signatures) { + char *p = const_cast(strstr(s, signature.first.c_str())); + if (p) memcpy(p, signature.second.c_str(), signature.first.length()); } } - dex::Writer writer(ir); + dex::Writer writer(ir); size_t new_size; - WA allocator; - auto *p_dex = writer.CreateImage(&allocator, &new_size); // allocates memory only once - return allocator.GetFd(p_dex); + DexAllocator allocator; + + // CreateImage calls allocator.Allocate() + auto *image = writer.CreateImage(&allocator, &new_size); + LOGD("writer.CreateImage returned: %p", image); + + return allocator.GetFd(); } -extern "C" -JNIEXPORT jobject -Java_org_lsposed_lspd_service_ObfuscationManager_obfuscateDex(JNIEnv *env, [[maybe_unused]] jclass obfuscation_manager, - jobject memory) { - maybeInit(env); +extern "C" JNIEXPORT jobject JNICALL Java_org_lsposed_lspd_service_ObfuscationManager_obfuscateDex( + JNIEnv *env, [[maybe_unused]] jclass clazz, jobject memory) { + ensureInitialized(env); + int fd = ASharedMemory_dupFromJava(env, memory); - auto size = ASharedMemory_getSize(fd); - LOGD("fd=%d, size=%zu", fd, size); + if (fd < 0) return nullptr; - const void* mem = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + auto size = ASharedMemory_getSize(fd); + LOGD("obfuscateDex: fd=%d, size=%zu", fd, size); + + // CRITICAL: We MUST use MAP_SHARED here, not MAP_PRIVATE. + // 1. Android's SharedMemory is backed by purely virtual IPC buffers (ashmem/memfd). + // If we use MAP_PRIVATE, the kernel attempts to create a Copy-On-Write snapshot. + // Because the Java side just populated this virtual buffer and immediately passed + // it to JNI, mapping it MAP_PRIVATE often results in mapping unpopulated zero-pages, + // which causes Slicer to read a corrupted/empty header and abort. + // 2. Using MAP_SHARED gives us direct pointers to the exact physical memory pages + // populated by Java. + // 3. ZERO-COPY ARCHITECTURE: Because Slicer's IR holds direct pointers to this mapped + // memory, mutating strings in-place (via const_cast) instantly updates the IR + // without allocating new memory. Since the Java caller discards the original + // SharedMemory buffer anyway, this in-place mutation is completely safe and highly + // efficient. + void *mem = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { - LOGE("old dex map failed?"); + LOGE("Failed to map input dex"); + close(fd); return nullptr; } - auto new_fd = obfuscateDex(mem, size); + // Process the DEX and obtain a new file descriptor for the output + int new_fd = obfuscateDexBuffer(mem, size); + + // Safely unmap and close the input buffer mapping + munmap(mem, size); + close(fd); + + if (new_fd < 0) { + LOGE("Obfuscation failed to create new dex buffer"); + return nullptr; + } - // construct new shared mem with fd - auto java_fd = JNI_NewObject(env, class_file_descriptor, method_file_descriptor_ctor, new_fd); - auto java_sm = JNI_NewObject(env, class_shared_memory, method_shared_memory_ctor, java_fd); + // Construct new SharedMemory object around the new_fd + auto java_fd = + lsplant::JNI_NewObject(env, class_file_descriptor, method_file_descriptor_ctor, new_fd); + auto java_sm = + lsplant::JNI_NewObject(env, class_shared_memory, method_shared_memory_ctor, java_fd); return java_sm.release(); } diff --git a/daemon/src/main/jni/obfuscation.h b/daemon/src/main/jni/obfuscation.h index ec9dd1513..16b81124c 100644 --- a/daemon/src/main/jni/obfuscation.h +++ b/daemon/src/main/jni/obfuscation.h @@ -1,45 +1,65 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - #pragma once -#include -#include -#include "utils/jni_helper.hpp" -class WA: public dex::Writer::Allocator { - // addr: {size, fd} - phmap::flat_hash_map> allocated_; +#include +#include +#include +#include + +#include "logging.h" + +// Custom allocator for dex::Writer that creates an ashmem region. +// Manages the virtual memory mapping lifecycle to prevent memory leaks. +class DexAllocator : public dex::Writer::Allocator { + void* mapped_mem_ = nullptr; + size_t mapped_size_ = 0; + int fd_ = -1; + public: inline void* Allocate(size_t size) override { - auto fd = ASharedMemory_create("", size); - auto *mem = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - allocated_[mem] = {size, fd}; - return mem; + LOGD("DexAllocator: attempting to allocate %zu bytes", size); + + fd_ = ASharedMemory_create("obfuscated_dex", size); + if (fd_ < 0) { + // Log the specific error + PLOGE("DexAllocator: ASharedMemory_create"); + return nullptr; + } + + mapped_size_ = size; + // MAP_SHARED is required for the output buffer so that Slicer's writes + // are immediately reflected in the underlying file descriptor. + mapped_mem_ = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0); + + if (mapped_mem_ == MAP_FAILED) { + PLOGE("DexAllocator: mmap"); + close(fd_); + fd_ = -1; + mapped_mem_ = nullptr; + } + + LOGD("DexAllocator: success, mapped at %p, fd=%d", mapped_mem_, fd_); + return mapped_mem_; } + inline void Free(void* ptr) override { - auto alloc_data = allocated_.at(ptr); - close(alloc_data.second); - allocated_.erase(ptr); + if (ptr == mapped_mem_ && mapped_mem_ != nullptr) { + munmap(mapped_mem_, mapped_size_); + close(fd_); + mapped_mem_ = nullptr; + fd_ = -1; + mapped_size_ = 0; + } } - inline int GetFd(void* ptr) { - auto alloc_data = allocated_.find(ptr); - if (alloc_data == allocated_.end()) return -1; - return (*alloc_data).second.second; + + inline int GetFd() const { return fd_; } + + inline ~DexAllocator() { + // Unmap the virtual memory upon destruction to prevent memory leaks. + if (mapped_mem_ != nullptr && mapped_mem_ != MAP_FAILED) { + munmap(mapped_mem_, mapped_size_); + } + // Notice: We do NOT close(fd_) here! + // The file descriptor is extracted via GetFd() and handed over to Java's SharedMemory, + // which assumes lifecycle ownership of it. } }; diff --git a/native/include/common/utils.h b/native/include/common/utils.h index 0ab4cd85b..f3aa03966 100644 --- a/native/include/common/utils.h +++ b/native/include/common/utils.h @@ -13,20 +13,6 @@ namespace vector::native { -/** - * @brief Converts a Java class name (dot-separated) to a JNI signature format. - * - * Example: "java.lang.String" -> "Ljava/lang/String;" - * Note: This implementation only prepends 'L' and does not append ';'. - * - * @param className The dot-separated Java class name. - * @return The class name in JNI format (e.g., "Ljava/lang/Object"). - */ -[[nodiscard]] inline std::string JavaNameToSignature(std::string className) { - std::replace(className.begin(), className.end(), '.', '/'); - return "L" + className; -} - /** * @brief Returns the number of elements in a statically-allocated C-style array. * diff --git a/native/src/jni/resources_hook.cpp b/native/src/jni/resources_hook.cpp index ec43f7b56..ea95e3953 100644 --- a/native/src/jni/resources_hook.cpp +++ b/native/src/jni/resources_hook.cpp @@ -1,7 +1,10 @@ #include +#include +#include +#include + #include "common/config.h" -#include "common/utils.h" #include "elf/elf_image.h" #include "elf/symbol_cache.h" #include "framework/android_types.h" @@ -33,9 +36,7 @@ static TYPE_RESTART ResXMLParser_restart = nullptr; static TYPE_GET_ATTR_NAME_ID ResXMLParser_getAttributeNameID = nullptr; /** - * @brief Constructs the JNI class name for the XResources class at runtime. - * - * @return The JNI-style class name (e.g., "org/some/obfuscated/XResources"). + * @brief Constructs the class name for the XResources class at runtime. */ static std::string GetXResourcesClassName() { // Use a static local variable to ensure this lookup and string manipulation @@ -47,16 +48,12 @@ static std::string GetXResourcesClassName() { } // The key is the original, unobfuscated class name prefix. // The value is the new, obfuscated prefix. - // TODO: The key "android.content.res.XRes" is hardcoded and fragile. auto it = obfs_map.find("android.content.res.XRes"); if (it == obfs_map.end()) { LOGE("Could not find obfuscated name for XResources."); return std::string(); } - // The map gives something like "a.b.c." and we transform it into - // the full JNI class name "a/b/c/XResources". - std::string jni_name = JavaNameToSignature(it->second).substr(1); // "a/b/c/" - jni_name += "ources"; // This seems to be a hardcoded way to append "XResources" + std::string jni_name = it->second + "ources"; LOGD("Resolved XResources class name to: {}", jni_name.c_str()); return jni_name; }(); @@ -123,10 +120,12 @@ VECTOR_DEF_NATIVE_METHOD(jboolean, ResourcesHook, initXResourcesNative) { } // Dynamically build the method signature using the (possibly obfuscated) class name. + std::string x_resources_jni_name = "L" + x_resources_class_name + ";"; + std::replace(x_resources_jni_name.begin(), x_resources_jni_name.end(), '.', '/'); + methodXResourcesTranslateResId = env->GetStaticMethodID( classXResources, "translateResId", - fmt::format("(IL{};Landroid/content/res/Resources;)I", "L" + x_resources_class_name + ";") - .c_str()); + fmt::format("(I{}Landroid/content/res/Resources;)I", x_resources_jni_name).c_str()); if (!methodXResourcesTranslateResId) { LOGE("Failed to find method: XResources.translateResId"); return JNI_FALSE; @@ -134,7 +133,7 @@ VECTOR_DEF_NATIVE_METHOD(jboolean, ResourcesHook, initXResourcesNative) { methodXResourcesTranslateAttrId = env->GetStaticMethodID( classXResources, "translateAttrId", - fmt::format("(Ljava/lang/String;L{};)I", "L" + x_resources_class_name + ";").c_str()); + fmt::format("(Ljava/lang/String;{})I", x_resources_jni_name).c_str()); if (!methodXResourcesTranslateAttrId) { LOGE("Failed to find method: XResources.translateAttrId"); return JNI_FALSE; From 92338edf47e2eed6e8cb9b61c9dcea33cca8fd1c Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 6 Mar 2026 14:01:23 +0100 Subject: [PATCH 24/24] Clean leftover --- magisk-loader/magisk_module/sepolicy.rule | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 magisk-loader/magisk_module/sepolicy.rule diff --git a/magisk-loader/magisk_module/sepolicy.rule b/magisk-loader/magisk_module/sepolicy.rule deleted file mode 100644 index cc170e6b2..000000000 --- a/magisk-loader/magisk_module/sepolicy.rule +++ /dev/null @@ -1,14 +0,0 @@ -allow dex2oat dex2oat_exec file execute_no_trans -allow dex2oat system_linker_exec file execute_no_trans - -allow shell shell dir write - -type xposed_file file_type -typeattribute xposed_file mlstrustedobject -allow {dex2oat installd isolated_app shell} xposed_file {file dir} * - -allow dex2oat {unlabeled tmpfs} file * - -type xposed_data file_type -typeattribute xposed_data mlstrustedobject -allow * xposed_data {file dir} *