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 diff --git a/build.gradle.kts b/build.gradle.kts index 50028f667..841dc99c9 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,43 +18,90 @@ 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" + +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("-DVECTOR_ROOT=${rootDir.absolutePath}", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON") + ) + val flags = arrayOf( - "-DEXTERNAL_ROOT=${File(rootDir.absolutePath, "external")}", - "-DCORE_ROOT=${File(rootDir.absolutePath, "core/src/main/jni")}", - "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", + "-DINJECTED_UID=$injectedPackageUid", + "-DVERSION_CODE=${verCode}", + "-DVERSION_NAME='\"${verName}\"'", + "-Wno-gnu-string-literal-operator-template", + "-Wno-c++2b-extensions", ) - ) - val flags = arrayOf( - "-DINJECTED_AID=$injectedPackageUid", - "-Wno-gnu-string-literal-operator-template", - "-Wno-c++2b-extensions", - ) cFlags.addAll(flags) cppFlags.addAll(flags) abiFilters("arm64-v8a", "armeabi-v7a", "x86", "x86_64") } buildTypes { if (it.name == "release") { - arguments += "-DDEBUG_SYMBOLS_PATH=${ + arguments += + "-DDEBUG_SYMBOLS_PATH=${ layout.buildDirectory.dir("symbols").get().asFile.absolutePath }" } } } -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") @@ -77,9 +110,7 @@ val androidCompileNdkVersion by extra("29.0.13113456") val androidSourceCompatibility by extra(JavaVersion.VERSION_21) val androidTargetCompatibility by extra(JavaVersion.VERSION_21) -tasks.register("Delete", Delete::class) { - delete(rootProject.layout.buildDirectory) -} +tasks.register("Delete", Delete::class) { delete(rootProject.layout.buildDirectory) } subprojects { plugins.withType(AndroidBasePlugin::class.java) { @@ -127,6 +158,7 @@ tasks.register("format") { source = project.fileTree(rootDir) include("*.gradle.kts", "*/build.gradle.kts") dependsOn(":xposed:ktfmtFormat") + dependsOn(":zygisk:ktfmtFormat") } ktfmt { kotlinLangStyle() } 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/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/core/src/main/jni/CMakeLists.txt b/core/src/main/jni/CMakeLists.txt deleted file mode 100644 index dce214922..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(${EXTERNAL_ROOT} 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 ${EXTERNAL_ROOT}/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}"; -} 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..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 = "LSPosed"; + 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/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..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<>(); @@ -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/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/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 133ca0202..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/lsposed/lspd/core/", ""}, - { "Lorg/lsposed/lspd/nativebridge/", ""}, - { "Lorg/lsposed/lspd/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/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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f8e1ee312..61285a659 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ 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 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/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/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/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/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 9c2d7e317..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_AID; -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/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..f3aa03966 --- /dev/null +++ b/native/include/common/utils.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include +#include +#include + +/** + * @file utils.h + * @brief Miscellaneous utility functions and templates for the native library. + */ + +namespace vector::native { + +/** + * @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..e0b768400 --- /dev/null +++ b/native/include/elf/elf_image.h @@ -0,0 +1,194 @@ +#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; + bool bias_calculated_ = false; + + // 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..7d2df6703 --- /dev/null +++ b/native/src/elf/elf_image.cpp @@ -0,0 +1,382 @@ +#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. + + // 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_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: + // 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..ea95e3953 --- /dev/null +++ b/native/src/jni/resources_hook.cpp @@ -0,0 +1,306 @@ +#include + +#include +#include +#include + +#include "common/config.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 class name for the XResources class at runtime. + */ +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. + 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(); + } + std::string jni_name = it->second + "ources"; + 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. + 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("(I{}Landroid/content/res/Resources;)I", x_resources_jni_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;{})I", x_resources_jni_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 diff --git a/settings.gradle.kts b/settings.gradle.kts index fcb822daf..0b57d6a2d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,7 @@ dependencyResolutionManagement { } rootProject.name = "LSPosed" + include( ":app", ":core", @@ -30,4 +31,5 @@ include( ":services:manager-service", ":services:daemon-service", ":xposed", + ":zygisk", ) diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index 9b2ea6edf..29b5ffc69 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -11,13 +11,10 @@ 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 { 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..527fb8307 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt @@ -0,0 +1,61 @@ +package org.matrix.vector.nativebridge + +import dalvik.annotation.optimization.FastNative +import java.lang.reflect.Executable +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +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..b2fb29201 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt @@ -0,0 +1,5 @@ +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..42955ed72 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt @@ -0,0 +1,26 @@ +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, + ) +} diff --git a/magisk-loader/.gitignore b/zygisk/.gitignore similarity index 100% rename from magisk-loader/.gitignore rename to zygisk/.gitignore diff --git a/zygisk/README.md b/zygisk/README.md new file mode 100644 index 000000000..b1f44786d --- /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 `_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. + +### 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/magisk-loader/magisk_module/META-INF/com/google/android/update-binary b/zygisk/module/META-INF/com/google/android/update-binary similarity index 100% rename from magisk-loader/magisk_module/META-INF/com/google/android/update-binary rename to zygisk/module/META-INF/com/google/android/update-binary diff --git a/magisk-loader/magisk_module/META-INF/com/google/android/updater-script b/zygisk/module/META-INF/com/google/android/updater-script similarity index 100% rename from magisk-loader/magisk_module/META-INF/com/google/android/updater-script rename to zygisk/module/META-INF/com/google/android/updater-script diff --git a/magisk-loader/magisk_module/action.sh b/zygisk/module/action.sh similarity index 55% rename from magisk-loader/magisk_module/action.sh rename to zygisk/module/action.sh index 42359ce19..05a2b4267 100644 --- a/magisk-loader/magisk_module/action.sh +++ b/zygisk/module/action.sh @@ -1,4 +1,4 @@ -DEFAULT_MANAGER_PACKAGE_NAME=org.lsposed.manager -INJECTED_PACKAGE_NAME=com.android.shell +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..7a9451dc8 --- /dev/null +++ b/zygisk/module/customize.sh @@ -0,0 +1,141 @@ +SKIPUNZIP=1 + +# ========================================================= +# Utils functions to extract and verify installation package + +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 "*********************************************************" +} + +# 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 + + # 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" + + 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" + + # 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 ! 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 + +# 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 [ "$API" -ge 29 ]; then + ui_print "- Extracting dex2oat binaries" + mkdir -p "$MODPATH/bin" + + # 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/$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 + + ui_print "- Patching binaries for anti-detection" + DEV_PATH=$(tr -dc 'a-z0-9' >"$MODPATH/system.prop" +fi + +ui_print "- Welcome to Vector!" diff --git a/zygisk/module/daemon b/zygisk/module/daemon new file mode 100644 index 000000000..514eac7c0 --- /dev/null +++ b/zygisk/module/daemon @@ -0,0 +1,46 @@ +#!/system/bin/sh + +dir="${0%/*}" +tmpDaemonApk="/data/local/tmp/daemon.apk" +debug="@DEBUG@" + +# 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 + +# 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" + 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 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 + # 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" = "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/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/magisk-loader/magisk_module/sepolicy.rule b/zygisk/module/sepolicy.rule similarity index 100% rename from magisk-loader/magisk_module/sepolicy.rule rename to zygisk/module/sepolicy.rule diff --git a/zygisk/module/service.sh b/zygisk/module/service.sh new file mode 100644 index 000000000..e49f8859b --- /dev/null +++ b/zygisk/module/service.sh @@ -0,0 +1,6 @@ +# 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 "$@" & diff --git a/magisk-loader/magisk_module/system.prop b/zygisk/module/system.prop similarity index 100% rename from magisk-loader/magisk_module/system.prop rename to zygisk/module/system.prop diff --git a/zygisk/module/uninstall.sh b/zygisk/module/uninstall.sh new file mode 100644 index 000000000..2b72940bd --- /dev/null +++ b/zygisk/module/uninstall.sh @@ -0,0 +1 @@ +rm -rf /data/adb/lspd diff --git a/magisk-loader/proguard-rules.pro b/zygisk/proguard-rules.pro similarity index 78% rename from magisk-loader/proguard-rules.pro rename to zygisk/proguard-rules.pro index 36e55739f..7eee05bdc 100644 --- a/magisk-loader/proguard-rules.pro +++ b/zygisk/proguard-rules.pro @@ -1,10 +1,10 @@ --keepclasseswithmembers class org.lsposed.lspd.core.Main { +-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.lsposed.lspd.service.BridgeService { +-keepclasseswithmembers class org.matrix.vector.service.BridgeService { public static boolean *(android.os.IBinder, int, long, long, int); } 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/magisk-loader/src/main/jni/CMakeLists.txt b/zygisk/src/main/cpp/CMakeLists.txt similarity index 61% rename from magisk-loader/src/main/jni/CMakeLists.txt rename to zygisk/src/main/cpp/CMakeLists.txt index 82600d7a6..52a4794fa 100644 --- a/magisk-loader/src/main/jni/CMakeLists.txt +++ b/zygisk/src/main/cpp/CMakeLists.txt @@ -1,24 +1,18 @@ cmake_minimum_required(VERSION 3.10) -project(lspd) -add_subdirectory(${CORE_ROOT} core) +project(zygisk) -configure_file(template/loader.cpp src/loader.cpp) +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) -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) +add_subdirectory(${VECTOR_ROOT}/native native) +add_library(${PROJECT_NAME} SHARED module.cpp ipc_bridge.cpp) target_include_directories(${PROJECT_NAME} PUBLIC include) -target_include_directories(${PROJECT_NAME} PRIVATE src) -target_link_libraries(${PROJECT_NAME} core log) +target_link_libraries(${PROJECT_NAME} native 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} 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..2a1d0a4eb --- /dev/null +++ b/zygisk/src/main/cpp/include/ipc_bridge.h @@ -0,0 +1,177 @@ +#pragma once + +#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_). + */ + 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/magisk-loader/src/main/jni/api/zygisk.hpp b/zygisk/src/main/cpp/include/zygisk.hpp similarity index 91% rename from magisk-loader/src/main/jni/api/zygisk.hpp rename to zygisk/src/main/cpp/include/zygisk.hpp index 6bd6c6f6f..ab764954c 100644 --- a/magisk-loader/src/main/jni/api/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 new file mode 100644 index 000000000..4e81f96a9 --- /dev/null +++ b/zygisk/src/main/cpp/ipc_bridge.cpp @@ -0,0 +1,577 @@ +#include "ipc_bridge.h" + +#include +#include +#include + +#include +#include +#include + +using namespace std::literals::string_view_literals; + +#include + +namespace vector::native::module { + +// 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 { + LOGV("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 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 = ('_' << 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; + +// ========================================================================================= +// 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; + } + + LOGV("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}; + } + + // 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) { + LOGE("Could not get rendezvous service '{}'. Manager not available?", + kBridgeServiceName.data()); + return {env, nullptr}; + } + + // 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}; + } + + // Write the request data to the 'data' parcel. + 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()); + + // 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) { + // 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 { + LOGD("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_)); + + LOGV("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(); + } + + LOGV("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; + } + + // 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; + } + + // --- 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..43f5c4a96 --- /dev/null +++ b/zygisk/src/main/cpp/module.cpp @@ -0,0 +1,453 @@ +#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 * 10000. +constexpr int PER_USER_RANGE = 100000; + +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; +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(); + obfuscation_map_t 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) { + LOGV("Loading framework DEX into memory (size: {}).", dex.size()); + + // Get the system ClassLoader. This will be the parent of our new loader. + auto classloader_class = lsplant::JNI_FindClass(env, "java/lang/ClassLoader"); + if (!classloader_class) { + LOGE("Failed to find java.lang.ClassLoader"); + return; + } + auto getsyscl_mid = lsplant::JNI_GetStaticMethodID( + env, classloader_class.get(), "getSystemClassLoader", "()Ljava/lang/ClassLoader;"); + auto system_classloader = + lsplant::JNI_CallStaticObjectMethod(env, classloader_class.get(), getsyscl_mid); + if (!system_classloader) { + LOGE("Failed to get SystemClassLoader"); + return; + } + + // Create a Java ByteBuffer wrapping our in-memory DEX data. + auto byte_buffer_class = lsplant::JNI_FindClass(env, "java/nio/ByteBuffer"); + if (!byte_buffer_class) { + LOGE("Failed to find java.nio.ByteBuffer"); + return; + } + auto dex_buffer = + lsplant::ScopedLocalRef(env, env->NewDirectByteBuffer(dex.data(), dex.size())); + if (!dex_buffer) { + LOGE("Failed to create DirectByteBuffer for DEX."); + return; + } + + // Create an InMemoryDexClassLoader instance. + auto in_memory_cl_class = lsplant::JNI_FindClass(env, "dalvik/system/InMemoryDexClassLoader"); + if (!in_memory_cl_class) { + LOGE("Failed to find InMemoryDexClassLoader."); + return; + } + auto init_mid = lsplant::JNI_GetMethodID(env, in_memory_cl_class.get(), "", + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); + if (!init_mid) { + LOGE("Failed to find InMemoryDexClassLoader constructor."); + return; + } + + auto new_cl = + lsplant::ScopedLocalRef(env, env->NewObject(in_memory_cl_class.get(), init_mid, + dex_buffer.get(), system_classloader.get())); + if (env->ExceptionCheck() || !new_cl) { + LOGE("Failed to create InMemoryDexClassLoader instance."); + env->ExceptionClear(); + return; + } + + // Store a global reference to our new ClassLoader. + inject_class_loader_ = env->NewGlobalRef(new_cl.get()); + LOGV("Framework ClassLoader created successfully."); +} + +void VectorModule::SetupEntryClass(JNIEnv *env) { + if (!inject_class_loader_) { + LOGE("Cannot setup entry class: ClassLoader is null."); + return; + } + + // Use the obfuscation map from the config to get the real class name. + const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); + std::string entry_class_name; + entry_class_name = obfs_map.at("org.matrix.vector.core.") + "Main"; + + // We must find the class through our custom ClassLoader. + auto entry_class = this->FindClassFromLoader(env, inject_class_loader_, entry_class_name); + if (!entry_class) { + LOGE("Failed to find entry class '{}' in the loaded DEX.", entry_class_name.c_str()); + return; + } + + // Store a global reference to the entry class. + entry_class_ = lsplant::JNI_NewGlobalRef(env, entry_class); + LOGV("Framework entry class '{}' located.", entry_class_name.c_str()); +} + +void VectorModule::onLoad(zygisk::Api *api, JNIEnv *env) { + this->api_ = api; + this->env_ = env; + + // Create two singlton instances for classes Context and ConfigBridge + instance_.reset(this); + ConfigImpl::Init(); + LOGD("Vector Zygisk module loaded"); +} + +void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { + // Reset state for this new process fork. + should_inject_ = false; + is_manager_app_ = false; + + // --- Manager App Special Handling --- + // We identify our manager app by a special UID and + // grant it internet permissions by adding it to the INET group. + if (args->uid == 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) { + 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; + LOGV("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) { + LOGD("No IPC binder obtained 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_); + + LOGV("Injected Vector framework into '{}'.", nice_name_str.get()); + SetAllowUnload(false); // We are injected, PREVENT module unloading. +} + +void VectorModule::preServerSpecialize(zygisk::ServerSpecializeArgs *args) { + // The system server is always a target for injection. + should_inject_ = true; + LOGI("System server process detected. Marking for injection."); + + // Initialize our IPC bridge singleton. + IPCBridge::GetInstance().Initialize(env_); +} + +void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args) { + if (!should_inject_) { + SetAllowUnload(true); + return; + } + + LOGD("Attempting injection into system_server."); + + // --- Device-Specific Workaround --- + // Some ZTE devices require argv[0] to be explicitly set to "system_server" + // for certain services to function correctly after modification. + if (__system_property_find("ro.vendor.product.ztename")) { + LOGV("Applying ZTE-specific workaround: setting argv[0] to system_server."); + auto process_class = lsplant::ScopedLocalRef(env_, env_->FindClass("android/os/Process")); + if (process_class) { + auto set_argv0_mid = + env_->GetStaticMethodID(process_class.get(), "setArgV0", "(Ljava/lang/String;)V"); + auto name_str = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system_server")); + if (set_argv0_mid && name_str) { + env_->CallStaticVoidMethod(process_class.get(), set_argv0_mid, name_str.get()); + } + } + if (env_->ExceptionCheck()) { + LOGW("Exception occurred during ZTE workaround."); + env_->ExceptionClear(); + } + } + + // --- 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("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..8c7240115 --- /dev/null +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -0,0 +1,442 @@ +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.core.ApplicationServiceClient.serviceClient +import org.lsposed.lspd.util.Hookers +import org.lsposed.lspd.util.Utils + +/** 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..32a033bc1 --- /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.lsposed.lspd.util.Utils +import org.matrix.vector.service.BridgeService + +/** + * 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..267a7cf32 --- /dev/null +++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt @@ -0,0 +1,50 @@ +package org.matrix.vector.core + +import android.os.IBinder +import android.os.Process +import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient +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 + +/** Main entry point for the Java-side loader, 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) { + // Initialize system-specific resolution hooks if in system_server + if (isSystem) { + ParasiticManagerSystemHooker.start() + } + + // Initialize Xposed bridge components + val appService = ILSPApplicationService.Stub.asInterface(binder) + Startup.initXposed(isSystem, niceName, appDir, appService) + + // Configure logging levels from the service client + runCatching { Utils.Log.muted = serviceClient.isLogMuted } + .onFailure { t -> Utils.logE("Failed to configure logs from service", t) } + + // 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()) { + Utils.logI("Parasitic manager loaded into host, skipping standard bootstrap.") + return + } + + // 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..5aeb86a20 --- /dev/null +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt @@ -0,0 +1,187 @@ +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.service.ILSPosedService +import org.lsposed.lspd.util.Utils.Log + +/** + * 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 ('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 { + 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 { + 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..7df7b2be0 --- /dev/null +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt @@ -0,0 +1,37 @@ +package org.matrix.vector.service + +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 { + + 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) 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 @@ +{ }