commit a1dfdf37461140ebbca46a121799d60384c3bd36 Author: chiteroman <98092901+chiteroman@users.noreply.github.com> Date: Sun Jul 21 18:34:45 2024 +0200 Rebase PIF diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72aac6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +*.dex +*.so +*.zip diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f5effff --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "app/src/main/cpp/Dobby"] + path = app/src/main/cpp/Dobby + url = https://github.com/chiteroman/Dobby.git +[submodule "app/src/main/cpp/cJSON"] + path = app/src/main/cpp/cJSON + url = https://github.com/DaveGamble/cJSON.git diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..3e2867b --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,128 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "es.chiteroman.playintegrityfix" + compileSdk = 35 + buildToolsVersion = "35.0.0" + ndkVersion = "27.0.12077973" + + buildFeatures { + prefab = true + } + + packaging { + resources { + excludes += "**" + } + jniLibs { + excludes += "**/libdobby.so" + } + } + + defaultConfig { + applicationId = "es.chiteroman.playintegrityfix" + minSdk = 26 + targetSdk = 35 + versionCode = 16700 + versionName = "v16.7" + multiDexEnabled = false + + externalNativeBuild { + cmake { + arguments( + "-DANDROID_STL=none", + "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", + "-DANDROID_CPP_FEATURES=no-rtti no-exceptions", + "-DCMAKE_BUILD_TYPE=MinSizeRel", + "-DCMAKE_CXX_STANDARD=23", + "-DCMAKE_C_STANDARD=23", + "-DCMAKE_CXX_STANDARD_REQUIRED=ON", + "-DCMAKE_C_STANDARD_REQUIRED=ON", + "-DCMAKE_VISIBILITY_INLINES_HIDDEN=ON", + "-DCMAKE_CXX_VISIBILITY_PRESET=hidden", + "-DCMAKE_C_VISIBILITY_PRESET=hidden" + ) + } + } + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + multiDexEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } +} + +dependencies { + implementation(libs.cxx) + implementation(libs.bouncycastle) + implementation(libs.hiddenapibypass) +} + +tasks.register("updateModuleProp") { + doLast { + val versionName = project.android.defaultConfig.versionName + val versionCode = project.android.defaultConfig.versionCode + + val modulePropFile = project.rootDir.resolve("module/module.prop") + + var content = modulePropFile.readText() + + content = content.replace(Regex("version=.*"), "version=$versionName") + content = content.replace(Regex("versionCode=.*"), "versionCode=$versionCode") + + modulePropFile.writeText(content) + } +} + +tasks.register("copyFiles") { + dependsOn("updateModuleProp") + + doLast { + val moduleFolder = project.rootDir.resolve("module") + val dexFile = + project.layout.buildDirectory.get().asFile.resolve("intermediates/dex/release/minifyReleaseWithR8/classes.dex") + val soDir = + project.layout.buildDirectory.get().asFile.resolve("intermediates/stripped_native_libs/release/stripReleaseDebugSymbols/out/lib") + + dexFile.copyTo(moduleFolder.resolve("classes.dex"), overwrite = true) + + soDir.walk().filter { it.isFile && it.extension == "so" }.forEach { soFile -> + val abiFolder = soFile.parentFile.name + val destination = moduleFolder.resolve("zygisk/$abiFolder.so") + soFile.copyTo(destination, overwrite = true) + } + } +} + +tasks.register("zip") { + dependsOn("copyFiles") + + archiveFileName.set("PlayIntegrityFix_${project.android.defaultConfig.versionName}.zip") + destinationDirectory.set(project.rootDir.resolve("out")) + + from(project.rootDir.resolve("module")) +} + +afterEvaluate { + tasks["assembleRelease"].finalizedBy("updateModuleProp", "copyFiles", "zip") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..baf082f --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,3 @@ +-keep class es.chiteroman.playintegrityfix.EntryPoint {public ;} +-keep class es.chiteroman.playintegrityfix.CustomKeyStoreSpi +-keep class es.chiteroman.playintegrityfix.CustomProvider \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3307882 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..7ada73e --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.22.1) + +project("playintegrityfix") + +find_package(cxx REQUIRED CONFIG) + +link_libraries(cxx::cxx) + +add_library(${CMAKE_PROJECT_NAME} SHARED main.cpp cJSON/cJSON.c) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE cJSON) + +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE log dobby_static) + +add_subdirectory(Dobby) diff --git a/app/src/main/cpp/Dobby b/app/src/main/cpp/Dobby new file mode 160000 index 0000000..7447fd9 --- /dev/null +++ b/app/src/main/cpp/Dobby @@ -0,0 +1 @@ +Subproject commit 7447fd9209bb43eba1e307f84df17613af5203dd diff --git a/app/src/main/cpp/cJSON b/app/src/main/cpp/cJSON new file mode 160000 index 0000000..424ce4c --- /dev/null +++ b/app/src/main/cpp/cJSON @@ -0,0 +1 @@ +Subproject commit 424ce4ce9668f288fb4ab665775546d3ed709e96 diff --git a/app/src/main/cpp/main.cpp b/app/src/main/cpp/main.cpp new file mode 100644 index 0000000..1a45580 --- /dev/null +++ b/app/src/main/cpp/main.cpp @@ -0,0 +1,286 @@ +#include +#include +#include +#include +#include +#include +#include "zygisk.hpp" +#include "dobby.h" +#include "cJSON.h" + +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "PIF", __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "PIF", __VA_ARGS__) + +#define DEX_PATH "/data/adb/modules/playintegrityfix/classes.dex" + +#define PIF_JSON "/data/adb/pif.json" + +#define PIF_JSON_DEFAULT "/data/adb/modules/playintegrityfix/pif.json" + +static std::string DEVICE_INITIAL_SDK_INT = "21"; +static std::string SECURITY_PATCH; +static std::string BUILD_ID; + +static bool DEBUG = false; + +typedef void (*T_Callback)(void *, const char *, const char *, uint32_t); + +static std::map callbacks; + +static void modify_callback(void *cookie, const char *name, const char *value, uint32_t serial) { + + if (cookie == nullptr || name == nullptr || value == nullptr || + !callbacks.contains(cookie)) + return; + + std::string_view prop(name); + + if (prop == "init.svc.adbd") { + value = "stopped"; + if (!DEBUG) LOGD("[%s]: %s", name, value); + } else if (prop == "sys.usb.state") { + value = "mtp"; + if (!DEBUG) LOGD("[%s]: %s", name, value); + } else if (prop.ends_with("api_level") && !DEVICE_INITIAL_SDK_INT.empty()) { + value = DEVICE_INITIAL_SDK_INT.c_str(); + if (!DEBUG) LOGD("[%s]: %s", name, value); + } else if (prop.ends_with(".security_patch") && !SECURITY_PATCH.empty()) { + value = SECURITY_PATCH.c_str(); + if (!DEBUG) LOGD("[%s]: %s", name, value); + } else if (prop.ends_with(".build.id") && !BUILD_ID.empty()) { + value = BUILD_ID.c_str(); + if (!DEBUG) LOGD("[%s]: %s", name, value); + } + + if (DEBUG) LOGD("[%s]: %s", name, value); + + return callbacks[cookie](cookie, name, value, serial); +} + +static void (*o_system_property_read_callback)(const prop_info *, T_Callback, void *) = nullptr; + +static void +my_system_property_read_callback(const prop_info *pi, T_Callback callback, void *cookie) { + if (pi && callback && cookie) callbacks[cookie] = callback; + return o_system_property_read_callback(pi, modify_callback, cookie); +} + +static void doHook() { + LOGD("JSON contains DEVICE_INITIAL_SDK_INT key. Hooking native prop symbol"); + void *handle = DobbySymbolResolver(nullptr, "__system_property_read_callback"); + if (!handle) { + LOGE("error resolving __system_property_read_callback symbol!"); + return; + } + if (DobbyHook(handle, (void *) my_system_property_read_callback, + (void **) &o_system_property_read_callback)) { + LOGE("hook __system_property_read_callback failed!"); + return; + } + LOGD("hook __system_property_read_callback success at %p", handle); +} + +class PlayIntegrityFix : public zygisk::ModuleBase { +public: + void onLoad(zygisk::Api *api, JNIEnv *env) override { + this->api = api; + this->env = env; + } + + void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { + + if (!args) { + api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); + return; + } + + auto dir = env->GetStringUTFChars(args->app_data_dir, nullptr); + + if (!dir) { + api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); + return; + } + + bool isGms = std::string_view(dir).ends_with("/com.google.android.gms"); + + env->ReleaseStringUTFChars(args->app_data_dir, dir); + + if (!isGms) { + api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); + return; + } + + api->setOption(zygisk::FORCE_DENYLIST_UNMOUNT); + + auto name = env->GetStringUTFChars(args->nice_name, nullptr); + + if (!name) { + api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); + return; + } + + bool isGmsUnstable = std::string_view(name) == "com.google.android.gms.unstable"; + + env->ReleaseStringUTFChars(args->nice_name, name); + + if (!isGmsUnstable) { + api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); + return; + } + + int fd = api->connectCompanion(); + + int dexSize = 0, jsonSize = 0; + std::vector jsonVector; + + read(fd, &dexSize, sizeof(int)); + read(fd, &jsonSize, sizeof(int)); + + if (dexSize > 0) { + dexVector.resize(dexSize); + read(fd, dexVector.data(), dexSize * sizeof(uint8_t)); + } + + if (jsonSize > 0) { + jsonVector.resize(jsonSize); + read(fd, jsonVector.data(), jsonSize * sizeof(uint8_t)); + std::string strJson(jsonVector.cbegin(), jsonVector.cend()); + json = cJSON_ParseWithLength(strJson.c_str(), strJson.size()); + } + + close(fd); + + LOGD("Dex file size: %d", dexSize); + LOGD("Json file size: %d", jsonSize); + } + + void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override { + if (dexVector.empty()) return; + + parseJSON(); + + if (enableHook) doHook(); + else api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); + + injectDex(); + + cJSON_Delete(json); + } + + void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override { + api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); + } + +private: + zygisk::Api *api = nullptr; + JNIEnv *env = nullptr; + std::vector dexVector; + cJSON *json = nullptr; + bool enableHook = false; + + void parseJSON() { + if (!json) return; + + const cJSON *api_level = cJSON_GetObjectItemCaseSensitive(json, "DEVICE_INITIAL_SDK_INT"); + const cJSON *isDebug = cJSON_GetObjectItemCaseSensitive(json, "DEBUG"); + + if (api_level) { + enableHook = true; + if (cJSON_IsNumber(api_level)) { + DEVICE_INITIAL_SDK_INT = std::to_string(api_level->valueint); + } else if (cJSON_IsString(api_level)) { + DEVICE_INITIAL_SDK_INT = api_level->valuestring; + } + cJSON_DeleteItemFromObjectCaseSensitive(json, "DEVICE_INITIAL_SDK_INT"); + } + + if (isDebug && cJSON_IsBool(isDebug)) { + DEBUG = cJSON_IsTrue(isDebug); + cJSON_DeleteItemFromObjectCaseSensitive(json, "DEBUG"); + } + } + + void injectDex() { + LOGD("get system classloader"); + auto clClass = env->FindClass("java/lang/ClassLoader"); + auto getSystemClassLoader = env->GetStaticMethodID(clClass, "getSystemClassLoader", + "()Ljava/lang/ClassLoader;"); + auto systemClassLoader = env->CallStaticObjectMethod(clClass, getSystemClassLoader); + + LOGD("create class loader"); + auto dexClClass = env->FindClass("dalvik/system/InMemoryDexClassLoader"); + auto dexClInit = env->GetMethodID(dexClClass, "", + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); + auto buffer = env->NewDirectByteBuffer(dexVector.data(), + static_cast(dexVector.size())); + auto dexCl = env->NewObject(dexClClass, dexClInit, buffer, systemClassLoader); + + LOGD("load class"); + auto loadClass = env->GetMethodID(clClass, "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;"); + auto entryClassName = env->NewStringUTF("es.chiteroman.playintegrityfix.EntryPoint"); + auto entryClassObj = env->CallObjectMethod(dexCl, loadClass, entryClassName); + + auto entryPointClass = (jclass) entryClassObj; + + LOGD("call init"); + auto entryInit = env->GetStaticMethodID(entryPointClass, "init", "(Ljava/lang/String;)V"); + auto jsonStr = env->NewStringUTF(cJSON_Print(json)); + env->CallStaticVoidMethod(entryPointClass, entryInit, jsonStr); + } +}; + +static std::vector readFile(const char *path) { + + std::vector vector; + + FILE *file = fopen(path, "rb"); + + if (file) { + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + + vector.resize(size); + fread(vector.data(), 1, size, file); + fclose(file); + } else { + LOGD("Couldn't read %s file!", path); + } + + return vector; +} + + +static void companion(int fd) { + + std::vector dex, json; + + if (std::filesystem::exists(DEX_PATH)) { + dex = readFile(DEX_PATH); + } + + if (std::filesystem::exists(PIF_JSON)) { + json = readFile(PIF_JSON); + } else if (std::filesystem::exists(PIF_JSON_DEFAULT)) { + json = readFile(PIF_JSON_DEFAULT); + } + + int dexSize = static_cast(dex.size()); + int jsonSize = static_cast(json.size()); + + write(fd, &dexSize, sizeof(int)); + write(fd, &jsonSize, sizeof(int)); + + if (dexSize > 0) { + write(fd, dex.data(), dexSize * sizeof(uint8_t)); + } + + if (jsonSize > 0) { + write(fd, json.data(), jsonSize * sizeof(uint8_t)); + } +} + +REGISTER_ZYGISK_MODULE(PlayIntegrityFix) + +REGISTER_ZYGISK_COMPANION(companion) diff --git a/app/src/main/cpp/zygisk.hpp b/app/src/main/cpp/zygisk.hpp new file mode 100644 index 0000000..7272633 --- /dev/null +++ b/app/src/main/cpp/zygisk.hpp @@ -0,0 +1,384 @@ +/* Copyright 2022-2023 John "topjohnwu" Wu + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +// This is the public API for Zygisk modules. +// DO NOT MODIFY ANY CODE IN THIS HEADER. + +#pragma once + +#include + +#define ZYGISK_API_VERSION 2 + +/* + +*************** +* Introduction +*************** + +On Android, all app processes are forked from a special daemon called "Zygote". +For each new app process, zygote will fork a new process and perform "specialization". +This specialization operation enforces the Android security sandbox on the newly forked +process to make sure that 3rd party application code is only loaded after it is being +restricted within a sandbox. + +On Android, there is also this special process called "system_server". This single +process hosts a significant portion of system services, which controls how the +Android operating system and apps interact with each other. + +The Zygisk framework provides a way to allow developers to build modules and run custom +code before and after system_server and any app processes' specialization. +This enable developers to inject code and alter the behavior of system_server and app processes. + +Please note that modules will only be loaded after zygote has forked the child process. +THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON! + +********************* +* Development Guide +********************* + +Define a class and inherit zygisk::ModuleBase to implement the functionality of your module. +Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk. + +Example code: + +static jint (*orig_logger_entry_max)(JNIEnv *env); +static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); } + +class ExampleModule : public zygisk::ModuleBase { +public: + void onLoad(zygisk::Api *api, JNIEnv *env) override { + this->api = api; + this->env = env; + } + void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { + JNINativeMethod methods[] = { + { "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max }, + }; + api->hookJniNativeMethods(env, "android/util/Log", methods, 1); + *(void **) &orig_logger_entry_max = methods[0].fnPtr; + } +private: + zygisk::Api *api; + JNIEnv *env; +}; + +REGISTER_ZYGISK_MODULE(ExampleModule) + +----------------------------------------------------------------------------------------- + +Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize, +or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class +never runs in a true superuser environment. + +If your module require access to superuser permissions, you can create and register +a root companion handler function. This function runs in a separate root companion +daemon process, and an Unix domain socket is provided to allow you to perform IPC between +your target process and the root companion process. + +Example code: + +static void example_handler(int socket) { ... } + +REGISTER_ZYGISK_COMPANION(example_handler) + +*/ + +namespace zygisk { + +struct Api; +struct AppSpecializeArgs; +struct ServerSpecializeArgs; + +class ModuleBase { +public: + + // This method is called as soon as the module is loaded into the target process. + // A Zygisk API handle will be passed as an argument. + virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {} + + // This method is called before the app process is specialized. + // At this point, the process just got forked from zygote, but no app specific specialization + // is applied. This means that the process does not have any sandbox restrictions and + // still runs with the same privilege of zygote. + // + // All the arguments that will be sent and used for app specialization is passed as a single + // AppSpecializeArgs object. You can read and overwrite these arguments to change how the app + // process will be specialized. + // + // If you need to run some operations as superuser, you can call Api::connectCompanion() to + // get a socket to do IPC calls with a root companion process. + // See Api::connectCompanion() for more info. + virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {} + + // This method is called after the app process is specialized. + // At this point, the process has all sandbox restrictions enabled for this application. + // This means that this method runs with the same privilege of the app's own code. + virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {} + + // This method is called before the system server process is specialized. + // See preAppSpecialize(args) for more info. + virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {} + + // This method is called after the system server process is specialized. + // At this point, the process runs with the privilege of system_server. + virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {} +}; + +struct AppSpecializeArgs { + // Required arguments. These arguments are guaranteed to exist on all Android versions. + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jint &mount_external; + jstring &se_info; + jstring &nice_name; + jstring &instruction_set; + jstring &app_data_dir; + + // Optional arguments. Please check whether the pointer is null before de-referencing + jboolean *const is_child_zygote; + jboolean *const is_top_app; + jobjectArray *const pkg_data_info_list; + jobjectArray *const whitelisted_data_info_list; + jboolean *const mount_data_dirs; + jboolean *const mount_storage_dirs; + + AppSpecializeArgs() = delete; +}; + +struct ServerSpecializeArgs { + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jlong &permitted_capabilities; + jlong &effective_capabilities; + + ServerSpecializeArgs() = delete; +}; + +namespace internal { +struct api_table; +template void entry_impl(api_table *, JNIEnv *); +} + +// These values are used in Api::setOption(Option) +enum Option : int { + // Force Magisk's denylist unmount routines to run on this process. + // + // Setting this option only makes sense in preAppSpecialize. + // The actual unmounting happens during app process specialization. + // + // Set this option to force all Magisk and modules' files to be unmounted from the + // mount namespace of the process, regardless of the denylist enforcement status. + FORCE_DENYLIST_UNMOUNT = 0, + + // When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize. + // Be aware that after dlclose-ing your module, all of your code will be unmapped from memory. + // YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS. + DLCLOSE_MODULE_LIBRARY = 1, +}; + +// Bit masks of the return value of Api::getFlags() +enum StateFlag : uint32_t { + // The user has granted root access to the current process + PROCESS_GRANTED_ROOT = (1u << 0), + + // The current process was added on the denylist + PROCESS_ON_DENYLIST = (1u << 1), +}; + +// All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded +// from the specialized process afterwards. +struct Api { + + // Connect to a root companion process and get a Unix domain socket for IPC. + // + // This API only works in the pre[XXX]Specialize methods due to SELinux restrictions. + // + // The pre[XXX]Specialize methods run with the same privilege of zygote. + // If you would like to do some operations with superuser permissions, register a handler + // function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func). + // Another good use case for a companion process is that if you want to share some resources + // across multiple processes, hold the resources in the companion process and pass it over. + // + // The root companion process is ABI aware; that is, when calling this method from a 32-bit + // process, you will be connected to a 32-bit companion process, and vice versa for 64-bit. + // + // Returns a file descriptor to a socket that is connected to the socket passed to your + // module's companion request handler. Returns -1 if the connection attempt failed. + int connectCompanion(); + + // Get the file descriptor of the root folder of the current module. + // + // This API only works in the pre[XXX]Specialize methods. + // Accessing the directory returned is only possible in the pre[XXX]Specialize methods + // or in the root companion process (assuming that you sent the fd over the socket). + // Both restrictions are due to SELinux and UID. + // + // Returns -1 if errors occurred. + int getModuleDir(); + + // Set various options for your module. + // Please note that this method accepts one single option at a time. + // Check zygisk::Option for the full list of options available. + void setOption(Option opt); + + // Get information about the current process. + // Returns bitwise-or'd zygisk::StateFlag values. + uint32_t getFlags(); + + // Hook JNI native methods for a class + // + // Lookup all registered JNI native methods and replace it with your own methods. + // The original function pointer will be saved in each JNINativeMethod's fnPtr. + // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr + // will be set to nullptr. + void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); + + // Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory. + // + // Parsing /proc/[PID]/maps will give you the memory map of a process. As an example: + // + //
+ // 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64 + // (More details: https://man7.org/linux/man-pages/man5/proc.5.html) + // + // For ELFs loaded in memory with pathname matching `regex`, replace function `symbol` with `newFunc`. + // If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`. + void pltHookRegister(const char *regex, const char *symbol, void *newFunc, void **oldFunc); + + // For ELFs loaded in memory with pathname matching `regex`, exclude hooks registered for `symbol`. + // If `symbol` is nullptr, then all symbols will be excluded. + void pltHookExclude(const char *regex, const char *symbol); + + // Commit all the hooks that was previously registered. + // Returns false if an error occurred. + bool pltHookCommit(); + +private: + internal::api_table *tbl; + template friend void internal::entry_impl(internal::api_table *, JNIEnv *); +}; + +// Register a class as a Zygisk module + +#define REGISTER_ZYGISK_MODULE(clazz) \ +void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ + zygisk::internal::entry_impl(table, env); \ +} + +// Register a root companion request handler function for your module +// +// The function runs in a superuser daemon process and handles a root companion request from +// your module running in a target process. The function has to accept an integer value, +// which is a Unix domain socket that is connected to the target process. +// See Api::connectCompanion() for more info. +// +// NOTE: the function can run concurrently on multiple threads. +// Be aware of race conditions if you have globally shared resources. + +#define REGISTER_ZYGISK_COMPANION(func) \ +void zygisk_companion_entry(int client) { func(client); } + +/********************************************************* + * The following is internal ABI implementation detail. + * You do not have to understand what it is doing. + *********************************************************/ + +namespace internal { + +struct module_abi { + long api_version; + ModuleBase *impl; + + void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *); + void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *); + void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *); + void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *); + + module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) { + preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); }; + postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); }; + preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); }; + postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); }; + } +}; + +struct api_table { + // Base + void *impl; + bool (*registerModule)(api_table *, module_abi *); + + void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int); + void (*pltHookRegister)(const char *, const char *, void *, void **); + void (*pltHookExclude)(const char *, const char *); + bool (*pltHookCommit)(); + int (*connectCompanion)(void * /* impl */); + void (*setOption)(void * /* impl */, Option); + int (*getModuleDir)(void * /* impl */); + uint32_t (*getFlags)(void * /* impl */); +}; + +template +void entry_impl(api_table *table, JNIEnv *env) { + static Api api; + api.tbl = table; + static T module; + ModuleBase *m = &module; + static module_abi abi(m); + if (!table->registerModule(table, &abi)) return; + m->onLoad(&api, env); +} + +} // namespace internal + +inline int Api::connectCompanion() { + return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1; +} +inline int Api::getModuleDir() { + return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1; +} +inline void Api::setOption(Option opt) { + if (tbl->setOption) tbl->setOption(tbl->impl, opt); +} +inline uint32_t Api::getFlags() { + return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0; +} +inline 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(const char *regex, const char *symbol, void *newFunc, void **oldFunc) { + if (tbl->pltHookRegister) tbl->pltHookRegister(regex, symbol, newFunc, oldFunc); +} +inline void Api::pltHookExclude(const char *regex, const char *symbol) { + if (tbl->pltHookExclude) tbl->pltHookExclude(regex, symbol); +} +inline bool Api::pltHookCommit() { + return tbl->pltHookCommit != nullptr && tbl->pltHookCommit(); +} + +} // namespace zygisk + +extern "C" { + +[[gnu::visibility("default"), maybe_unused]] +void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); + +[[gnu::visibility("default"), maybe_unused]] +void zygisk_companion_entry(int); + +} // extern "C" diff --git a/app/src/main/java/es/chiteroman/playintegrityfix/CustomKeyStoreSpi.java b/app/src/main/java/es/chiteroman/playintegrityfix/CustomKeyStoreSpi.java new file mode 100644 index 0000000..b618f20 --- /dev/null +++ b/app/src/main/java/es/chiteroman/playintegrityfix/CustomKeyStoreSpi.java @@ -0,0 +1,107 @@ +package es.chiteroman.playintegrityfix; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.Date; +import java.util.Enumeration; +import java.util.Locale; + +public final class CustomKeyStoreSpi extends KeyStoreSpi { + public static volatile KeyStoreSpi keyStoreSpi = null; + + @Override + public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException { + return keyStoreSpi.engineGetKey(alias, password); + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace()) { + if (stackTraceElement.getClassName().toLowerCase(Locale.US).contains("droidguard")) { + Log.w(EntryPoint.TAG, "DroidGuard invoke engineGetCertificateChain! Throwing exception..."); + throw new UnsupportedOperationException(); + } + } + return keyStoreSpi.engineGetCertificateChain(alias); + } + + @Override + public Certificate engineGetCertificate(String alias) { + return keyStoreSpi.engineGetCertificate(alias); + } + + @Override + public Date engineGetCreationDate(String alias) { + return keyStoreSpi.engineGetCreationDate(alias); + } + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) throws KeyStoreException { + keyStoreSpi.engineSetKeyEntry(alias, key, password, chain); + } + + @Override + public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException { + keyStoreSpi.engineSetKeyEntry(alias, key, chain); + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { + keyStoreSpi.engineSetCertificateEntry(alias, cert); + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + keyStoreSpi.engineDeleteEntry(alias); + } + + @Override + public Enumeration engineAliases() { + return keyStoreSpi.engineAliases(); + } + + @Override + public boolean engineContainsAlias(String alias) { + return keyStoreSpi.engineContainsAlias(alias); + } + + @Override + public int engineSize() { + return keyStoreSpi.engineSize(); + } + + @Override + public boolean engineIsKeyEntry(String alias) { + return keyStoreSpi.engineIsKeyEntry(alias); + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + return keyStoreSpi.engineIsCertificateEntry(alias); + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + return keyStoreSpi.engineGetCertificateAlias(cert); + } + + @Override + public void engineStore(OutputStream stream, char[] password) throws CertificateException, IOException, NoSuchAlgorithmException { + keyStoreSpi.engineStore(stream, password); + } + + @Override + public void engineLoad(InputStream stream, char[] password) throws CertificateException, IOException, NoSuchAlgorithmException { + keyStoreSpi.engineLoad(stream, password); + } +} diff --git a/app/src/main/java/es/chiteroman/playintegrityfix/CustomProvider.java b/app/src/main/java/es/chiteroman/playintegrityfix/CustomProvider.java new file mode 100644 index 0000000..c14552d --- /dev/null +++ b/app/src/main/java/es/chiteroman/playintegrityfix/CustomProvider.java @@ -0,0 +1,20 @@ +package es.chiteroman.playintegrityfix; + +import java.security.Provider; + +public final class CustomProvider extends Provider { + + public CustomProvider(Provider provider) { + super(provider.getName(), provider.getVersion(), provider.getInfo()); + putAll(provider); + put("KeyStore.AndroidKeyStore", CustomKeyStoreSpi.class.getName()); + } + + @Override + public synchronized Service getService(String type, String algorithm) { + Thread t = new Thread(EntryPoint::spoofFields); + t.setDaemon(true); + t.start(); + return super.getService(type, algorithm); + } +} diff --git a/app/src/main/java/es/chiteroman/playintegrityfix/EntryPoint.java b/app/src/main/java/es/chiteroman/playintegrityfix/EntryPoint.java new file mode 100644 index 0000000..c37eae9 --- /dev/null +++ b/app/src/main/java/es/chiteroman/playintegrityfix/EntryPoint.java @@ -0,0 +1,110 @@ +package es.chiteroman.playintegrityfix; + +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +import org.json.JSONObject; + +import java.lang.reflect.Field; +import java.security.KeyStore; +import java.security.KeyStoreSpi; +import java.security.Provider; +import java.security.Security; +import java.util.HashMap; +import java.util.Map; + +public final class EntryPoint { + public static final String TAG = "PIF"; + private static final Map map = new HashMap<>(); + + static { + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + Field keyStoreSpi = keyStore.getClass().getDeclaredField("keyStoreSpi"); + + keyStoreSpi.setAccessible(true); + + CustomKeyStoreSpi.keyStoreSpi = (KeyStoreSpi) keyStoreSpi.get(keyStore); + + } catch (Throwable t) { + Log.e(TAG, "Couldn't get keyStoreSpi field!", t); + } + + Provider provider = Security.getProvider("AndroidKeyStore"); + + Provider customProvider = new CustomProvider(provider); + + Security.removeProvider("AndroidKeyStore"); + Security.insertProviderAt(customProvider, 1); + } + + public static void init(String json) { + + if (TextUtils.isEmpty(json)) { + Log.e(TAG, "JSON is empty!"); + } else { + try { + JSONObject jsonObject = new JSONObject(json); + + jsonObject.keys().forEachRemaining(s -> { + try { + String value = jsonObject.getString(s); + + if (TextUtils.isEmpty(value)) return; + + Field field = getFieldByName(s); + + if (field == null) return; + + map.put(field, value); + + } catch (Throwable t) { + Log.e(TAG, "Error parsing JSON", t); + } + }); + } catch (Throwable t) { + Log.e(TAG, "Error parsing JSON", t); + } + } + + Log.i(TAG, "Fields ready to spoof: " + map.size()); + + spoofFields(); + } + + static void spoofFields() { + map.forEach((field, s) -> { + try { + if (s.equals(field.get(null))) return; + field.setAccessible(true); + String oldValue = String.valueOf(field.get(null)); + field.set(null, s); + Log.d(TAG, String.format(""" + --------------------------------------- + [%s] + OLD: '%s' + NEW: '%s' + --------------------------------------- + """, field.getName(), oldValue, field.get(null))); + } catch (Throwable t) { + Log.e(TAG, "Error modifying field", t); + } + }); + } + + private static Field getFieldByName(String name) { + Field field; + try { + field = Build.class.getDeclaredField(name); + } catch (NoSuchFieldException e) { + try { + field = Build.VERSION.class.getDeclaredField(name); + } catch (NoSuchFieldException ex) { + return null; + } + } + field.setAccessible(true); + return field; + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..3756278 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,4 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..4387edc --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d4b7ff9 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,13 @@ +[versions] +agp = "8.5.1" +cxx = "27.0.12077973" +bouncycastle = "1.78.1" +hiddenapibypass = "4.3" + +[libraries] +cxx = { group = "org.lsposed.libcxx", name = "libcxx", version.ref = "cxx" } +bouncycastle = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bouncycastle" } +hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..070e073 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Jul 21 17:28:50 CEST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/module/META-INF/com/google/android/update-binary b/module/META-INF/com/google/android/update-binary new file mode 100644 index 0000000..ea4889e --- /dev/null +++ b/module/META-INF/com/google/android/update-binary @@ -0,0 +1,33 @@ +#!/sbin/sh + +################# +# Initialization +################# + +umask 022 + +# echo before loading util_functions +ui_print() { echo "$1"; } + +require_new_magisk() { + ui_print "*******************************" + ui_print " Please install Magisk v20.4+! " + ui_print "*******************************" + exit 1 +} + +######################### +# Load util_functions.sh +######################### + +OUTFD=$2 +ZIPFILE=$3 + +mount /data 2>/dev/null + +[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk +. /data/adb/magisk/util_functions.sh +[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk + +install_module +exit 0 \ No newline at end of file diff --git a/module/META-INF/com/google/android/updater-script b/module/META-INF/com/google/android/updater-script new file mode 100644 index 0000000..11d5c96 --- /dev/null +++ b/module/META-INF/com/google/android/updater-script @@ -0,0 +1 @@ +#MAGISK diff --git a/module/common_func.sh b/module/common_func.sh new file mode 100644 index 0000000..7623096 --- /dev/null +++ b/module/common_func.sh @@ -0,0 +1,17 @@ +# resetprop_if_diff +resetprop_if_diff() { + local NAME="$1" + local EXPECTED="$2" + local CURRENT="$(resetprop "$NAME")" + + [ -z "$CURRENT" ] || [ "$CURRENT" = "$EXPECTED" ] || resetprop -n "$NAME" "$EXPECTED" +} + +# resetprop_if_match +resetprop_if_match() { + local NAME="$1" + local CONTAINS="$2" + local VALUE="$3" + + [[ "$(resetprop "$NAME")" = *"$CONTAINS"* ]] && resetprop -n "$NAME" "$VALUE" +} diff --git a/module/customize.sh b/module/customize.sh new file mode 100644 index 0000000..c6058fd --- /dev/null +++ b/module/customize.sh @@ -0,0 +1,61 @@ +# Error on < Android 8 +if [ "$API" -lt 26 ]; then + abort "- !!! You can't use this module on Android < 8.0" +fi + +# safetynet-fix module is obsolete and it's incompatible with PIF +if [ -d "/data/adb/modules/safetynet-fix" ]; then + touch "/data/adb/modules/safetynet-fix/remove" + ui_print "! safetynet-fix module removed. Do NOT install it again along PIF" +fi + +# playcurl must be removed when flashing PIF +if [ -d "/data/adb/modules/playcurl" ]; then + touch "/data/adb/modules/playcurl/remove" + ui_print "! playcurl module removed!" +fi + +# MagiskHidePropsConf module is obsolete in Android 8+ but it shouldn't give issues +if [ -d "/data/adb/modules/MagiskHidePropsConf" ]; then + ui_print "! WARNING, MagiskHidePropsConf module may cause issues with PIF." +fi + +# tricky_store must be removed when flashing PIF +if [ -d "/data/adb/modules/tricky_store" ]; then + touch "/data/adb/modules/tricky_store/remove" + ui_print "! tricky_store module removed!" +fi + +# Check custom fingerprint +if [ -f "/data/adb/pif.json" ]; then + mv -f "/data/adb/pif.json" "/data/adb/pif.json.old" + ui_print "- Backup custom pif.json" +fi + +REMOVE=" +/system/product/app/XiaomiEUInject +/system/product/app/XiaomiEUInject-Stub +/system/system/app/EliteDevelopmentModule +/system/system/app/XInjectModule +/system/system_ext/app/hentaiLewdbSVTDummy +/system/system_ext/app/PifPrebuilt +" + +if [ "$KSU" = "true" -o "$APATCH" = "true" ]; then + ui_print "- KernelSU/APatch detected, conflicting apps will be automatically removed" +else + ui_print "- Magisk detected, removing conflicting apps one by one :(" + echo "$REMOVE" | grep -v '^$' | while read -r line; do + if [ -d "$line" ]; then + mkdir -p "${MODPATH}${line}" + touch "${MODPATH}${line}/.replace" + ui_print "- Removed dir: $line" + elif [ -f "$line" ]; then + dir=$(dirname "$line") + filename=$(basename "$line") + mkdir -p "${MODPATH}${dir}" + touch "${MODPATH}${dir}/${filename}" + ui_print "- Removed file: $line" + fi + done +fi diff --git a/module/module.prop b/module/module.prop new file mode 100644 index 0000000..97df592 --- /dev/null +++ b/module/module.prop @@ -0,0 +1,7 @@ +id=playintegrityfix +name=Play Integrity Fix +version=v16.7 +versionCode=16700 +author=chiteroman +description=Universal modular fix for Play Integrity (and SafetyNet) on devices running Android 8-15 +updateJson=https://raw.githubusercontent.com/chiteroman/PlayIntegrityFix/main/update.json diff --git a/module/pif.json b/module/pif.json new file mode 100644 index 0000000..a6c8fb5 --- /dev/null +++ b/module/pif.json @@ -0,0 +1,12 @@ +{ + "ID": "AP31.240517.022", + "BRAND": "google", + "DEVICE": "husky", + "FINGERPRINT": "google/husky_beta/husky:15/AP31.240517.022/11948202:user/release-keys", + "MANUFACTURER": "Google", + "MODEL": "Pixel 8 Pro", + "PRODUCT": "husky_beta", + "SECURITY_PATCH": "2024-06-05", + "DEVICE_INITIAL_SDK_INT": 21, + "DEBUG": false +} \ No newline at end of file diff --git a/module/post-fs-data.sh b/module/post-fs-data.sh new file mode 100644 index 0000000..25d3ae3 --- /dev/null +++ b/module/post-fs-data.sh @@ -0,0 +1,37 @@ +MODPATH="${0%/*}" +. $MODPATH/common_func.sh + +# Remove Play Services from Magisk DenyList when set to Enforce in normal mode +if magisk --denylist status; then + magisk --denylist rm com.google.android.gms +fi + +# Conditional early sensitive properties + +# Samsung +resetprop_if_diff ro.boot.warranty_bit 0 +resetprop_if_diff ro.vendor.boot.warranty_bit 0 +resetprop_if_diff ro.vendor.warranty_bit 0 +resetprop_if_diff ro.warranty_bit 0 + +# Xiaomi +resetprop_if_diff ro.secureboot.lockstate locked + +# Realme +resetprop_if_diff ro.boot.realmebootstate green + +# OnePlus +resetprop_if_diff ro.is_ever_orange 0 + +# Microsoft +for PROP in $(resetprop | grep -oE 'ro.*.build.tags'); do + resetprop_if_diff $PROP release-keys +done + +# Other +for PROP in $(resetprop | grep -oE 'ro.*.build.type'); do + resetprop_if_diff $PROP user +done +resetprop_if_diff ro.debuggable 0 +resetprop_if_diff ro.force.debuggable 0 +resetprop_if_diff ro.secure 1 diff --git a/module/service.sh b/module/service.sh new file mode 100644 index 0000000..0b9b4c1 --- /dev/null +++ b/module/service.sh @@ -0,0 +1,44 @@ +MODPATH="${0%/*}" +. $MODPATH/common_func.sh + +# Conditional sensitive properties + +# Magisk Recovery Mode +resetprop_if_match ro.boot.mode recovery unknown +resetprop_if_match ro.bootmode recovery unknown +resetprop_if_match vendor.boot.mode recovery unknown + +# SELinux +resetprop_if_diff ro.boot.selinux enforcing +# use delete since it can be 0 or 1 for enforcing depending on OEM +if [ -n "$(resetprop ro.build.selinux)" ]; then + resetprop --delete ro.build.selinux +fi +# use toybox to protect stat access time reading +if [ "$(toybox cat /sys/fs/selinux/enforce)" = "0" ]; then + chmod 640 /sys/fs/selinux/enforce + chmod 440 /sys/fs/selinux/policy +fi + +# Conditional late sensitive properties + +# must be set after boot_completed for various OEMs +until [[ "$(getprop sys.boot_completed)" == "1" ]]; do + sleep 1 +done + +# SafetyNet/Play Integrity + OEM +# avoid breaking Realme fingerprint scanners +resetprop_if_diff ro.boot.flash.locked 1 +resetprop_if_diff ro.boot.realme.lockstate 1 +# avoid breaking Oppo fingerprint scanners +resetprop_if_diff ro.boot.vbmeta.device_state locked +# avoid breaking OnePlus display modes/fingerprint scanners +resetprop_if_diff vendor.boot.verifiedbootstate green +# avoid breaking OnePlus/Oppo fingerprint scanners on OOS/ColorOS 12+ +resetprop_if_diff ro.boot.verifiedbootstate green +resetprop_if_diff ro.boot.veritymode enforcing +resetprop_if_diff vendor.boot.vbmeta.device_state locked + +# Other +resetprop_if_diff sys.oem_unlock_allowed 0 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..7408c2f --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "PlayIntegrityFix" +include(":app")