inject changes

This commit is contained in:
chiteroman 2025-03-04 20:42:55 +01:00
parent 24ca377ec3
commit b8e81f760c
30 changed files with 884 additions and 741 deletions

View File

@ -2,9 +2,9 @@ name: Android CI
on:
push:
branches: [ "main" ]
branches: [ "inject" ]
pull_request:
branches: [ "main" ]
branches: [ "inject" ]
jobs:
build:

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ local.properties
*.dex
*.so
*.zip
*.jar

4
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "app/src/main/cpp/Dobby"]
path = app/src/main/cpp/Dobby
[submodule "inject/src/main/cpp/Dobby"]
path = inject/src/main/cpp/Dobby
url = https://github.com/JingMatrix/Dobby.git

View File

@ -1,136 +0,0 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "es.chiteroman.playintegrityfix"
compileSdk = 35
buildToolsVersion = "35.0.1"
ndkVersion = "28.0.13004108"
buildFeatures {
prefab = true
}
packaging {
jniLibs {
excludes += "**/libdobby.so"
}
resources {
excludes += "**"
}
}
defaultConfig {
applicationId = "es.chiteroman.playintegrityfix"
minSdk = 26
targetSdk = 35
versionCode = 18600
versionName = "v18.6"
multiDexEnabled = false
externalNativeBuild {
cmake {
abiFilters(
"arm64-v8a",
"armeabi-v7a"
)
arguments(
"-DCMAKE_BUILD_TYPE=MinSizeRel",
"-DANDROID_STL=none"
)
cFlags(
"-std=c23",
"-fvisibility=hidden",
"-fvisibility-inlines-hidden"
)
cppFlags(
"-std=c++23",
"-fno-exceptions",
"-fno-rtti",
"-fvisibility=hidden",
"-fvisibility-inlines-hidden"
)
}
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
multiDexEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
}
dependencies {
implementation(libs.cxx)
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>("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")
}

View File

@ -1,13 +0,0 @@
cmake_minimum_required(VERSION 3.5)
project("playintegrityfix")
find_package(cxx REQUIRED CONFIG)
link_libraries(cxx::cxx)
add_library(${CMAKE_PROJECT_NAME} SHARED main.cpp)
add_subdirectory(Dobby)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE log dobby_static)

View File

@ -1,581 +0,0 @@
#include <android/log.h>
#include <sys/system_properties.h>
#include <unistd.h>
#include "zygisk.hpp"
#include "dobby.h"
#define JSON_NOEXCEPTION 1
#define JSON_NO_IO 1
#include "json.hpp"
#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 TS_PATH "/data/adb/modules/tricky_store"
#define DEFAULT_JSON "/data/adb/modules/playintegrityfix/pif.json"
#define CUSTOM_JSON_FORK "/data/adb/modules/playintegrityfix/custom.pif.json"
#define CUSTOM_JSON "/data/adb/pif.json"
#define VENDING_PACKAGE "com.android.vending"
#define DROIDGUARD_PACKAGE "com.google.android.gms.unstable"
static ssize_t xread(int fd, void *buffer, size_t count) {
ssize_t total = 0;
char *buf = static_cast<char *>(buffer);
while (count > 0) {
ssize_t ret = TEMP_FAILURE_RETRY(read(fd, buf, count));
if (ret < 0) return -1;
buf += ret;
total += ret;
count -= ret;
}
return total;
}
static ssize_t xwrite(int fd, const void *buffer, size_t count) {
ssize_t total = 0;
char *buf = (char *) buffer;
while (count > 0) {
ssize_t ret = TEMP_FAILURE_RETRY(write(fd, buf, count));
if (ret < 0) return -1;
buf += ret;
total += ret;
count -= ret;
}
return total;
}
static bool DEBUG = false;
static std::string DEVICE_INITIAL_SDK_INT, SECURITY_PATCH, BUILD_ID;
typedef void (*T_Callback)(void *, const char *, const char *, uint32_t);
static T_Callback o_callback = nullptr;
static void modify_callback(void *cookie, const char *name, const char *value, uint32_t serial) {
if (!cookie || !name || !value || !o_callback) return;
const char *oldValue = value;
std::string_view prop(name);
if (prop == "init.svc.adbd") {
value = "stopped";
} else if (prop == "sys.usb.state") {
value = "mtp";
} else if (prop.ends_with("api_level")) {
if (!DEVICE_INITIAL_SDK_INT.empty()) {
value = DEVICE_INITIAL_SDK_INT.c_str();
}
} else if (prop.ends_with(".security_patch")) {
if (!SECURITY_PATCH.empty()) {
value = SECURITY_PATCH.c_str();
}
} else if (prop.ends_with(".build.id")) {
if (!BUILD_ID.empty()) {
value = BUILD_ID.c_str();
}
}
if (strcmp(oldValue, value) == 0) {
if (DEBUG) LOGD("[%s]: %s (unchanged)", name, oldValue);
} else {
LOGD("[%s]: %s -> %s", name, oldValue, value);
}
return o_callback(cookie, name, value, serial);
}
static void (*o_system_property_read_callback)(prop_info *, T_Callback, void *) = nullptr;
static void my_system_property_read_callback(prop_info *pi, T_Callback callback, void *cookie) {
if (pi && callback && cookie) o_callback = callback;
return o_system_property_read_callback(pi, modify_callback, cookie);
}
static bool doHook() {
void *ptr = DobbySymbolResolver(nullptr, "__system_property_read_callback");
if (ptr && DobbyHook(ptr, (void *) my_system_property_read_callback,
(void **) &o_system_property_read_callback) == 0) {
LOGD("hook __system_property_read_callback successful at %p", ptr);
return true;
}
LOGE("hook __system_property_read_callback failed!");
return false;
}
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;
}
std::string_view vDir(dir);
bool isGms =
vDir.ends_with("/com.google.android.gms") || vDir.ends_with("/com.android.vending");
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;
}
std::string_view vName(name);
isGmsUnstable = vName == DROIDGUARD_PACKAGE;
isVending = vName == VENDING_PACKAGE;
env->ReleaseStringUTFChars(args->nice_name, name);
if (!isGmsUnstable && !isVending) {
api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY);
return;
}
if (isGmsUnstable)
LOGD("We are in GMS unstable process!");
if (isVending)
LOGD("We are in Play Store process!");
int fd = api->connectCompanion();
size_t dexSize = 0, jsonSize = 0;
std::string jsonStr;
xread(fd, &dexSize, sizeof(size_t));
xread(fd, &jsonSize, sizeof(size_t));
if (dexSize > 0) {
dexVector.resize(dexSize);
xread(fd, dexVector.data(), dexSize);
}
if (jsonSize > 0) {
jsonStr.resize(jsonSize);
jsonSize = xread(fd, jsonStr.data(), jsonSize);
jsonStr[jsonSize] = '\0';
json = nlohmann::json::parse(jsonStr, nullptr, false, true);
}
bool trickyStore = false;
xread(fd, &trickyStore, sizeof(bool));
bool testSignedRom = false;
xread(fd, &testSignedRom, sizeof(bool));
close(fd);
LOGD("Dex file size: %zu", dexSize);
LOGD("Json file size: %zu", jsonSize);
parseJSON();
if (trickyStore) {
LOGD("TrickyStore module detected!");
spoofProvider = false;
spoofProps = false;
}
if (testSignedRom) {
LOGD("--- ROM IS SIGNED WITH TEST KEYS ---");
spoofSignature = true;
}
}
void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override {
if (dexVector.empty()) return;
if (isGmsUnstable) {
UpdateBuildFields();
if (spoofProvider || spoofSignature) {
injectDex();
} else {
LOGD("Dex file won't be injected due spoofProvider and spoofSignature are false");
}
if (spoofProps) {
if (!doHook()) {
dlclose();
}
} else {
dlclose();
}
} else if (isVending) {
doSpoofVending();
dlclose();
}
json.clear();
dexVector.clear();
dexVector.shrink_to_fit();
}
void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override {
api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY);
}
private:
zygisk::Api *api = nullptr;
JNIEnv *env = nullptr;
bool isGmsUnstable = false;
bool isVending = false;
std::vector<char> dexVector;
nlohmann::json json;
bool spoofProps = true;
bool spoofProvider = true;
bool spoofSignature = false;
int spoofVendingSdk = 0;
void dlclose() {
LOGD("dlclose zygisk lib");
api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY);
}
void doSpoofVending() {
if (spoofVendingSdk < 1) return;
int requestSdk = (spoofVendingSdk == 1) ? 32 : spoofVendingSdk;
int targetSdk;
int oldValue;
jclass buildVersionClass = nullptr;
jfieldID sdkIntFieldID = nullptr;
buildVersionClass = env->FindClass("android/os/Build$VERSION");
if (buildVersionClass == nullptr) {
LOGE("Build.VERSION class not found");
env->ExceptionClear();
return;
}
sdkIntFieldID = env->GetStaticFieldID(buildVersionClass, "SDK_INT", "I");
if (sdkIntFieldID == nullptr) {
LOGE("SDK_INT field not found");
env->ExceptionClear();
env->DeleteLocalRef(buildVersionClass);
return;
}
oldValue = env->GetStaticIntField(buildVersionClass, sdkIntFieldID);
targetSdk = std::min(oldValue, requestSdk);
if (oldValue == targetSdk) {
env->DeleteLocalRef(buildVersionClass);
return;
}
env->SetStaticIntField(buildVersionClass, sdkIntFieldID, targetSdk);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
LOGE("SDK_INT field not accessible (JNI Exception)");
} else {
LOGE("[SDK_INT]: %d -> %d", oldValue, targetSdk);
}
env->DeleteLocalRef(buildVersionClass);
}
void parseJSON() {
if (json.empty()) return;
if (json.contains("spoofVendingSdk")) {
if (json["spoofVendingSdk"].is_string()) {
spoofVendingSdk = std::stoi(json["spoofVendingSdk"].get<std::string>());
} else if (json["spoofVendingSdk"].is_number_integer()) {
spoofVendingSdk = json["spoofVendingSdk"].get<int>();
} else {
LOGE("Error parsing spoofVendingSdk!");
}
json.erase("spoofVendingSdk");
}
if (isVending) {
json.clear();
return;
}
if (json.contains("DEVICE_INITIAL_SDK_INT")) {
if (json["DEVICE_INITIAL_SDK_INT"].is_string()) {
DEVICE_INITIAL_SDK_INT = json["DEVICE_INITIAL_SDK_INT"].get<std::string>();
} else if (json["DEVICE_INITIAL_SDK_INT"].is_number_integer()) {
DEVICE_INITIAL_SDK_INT = std::to_string(json["DEVICE_INITIAL_SDK_INT"].get<int>());
} else {
LOGE("Couldn't parse DEVICE_INITIAL_SDK_INT value!");
}
json.erase("DEVICE_INITIAL_SDK_INT");
}
if (json.contains("spoofProvider") && json["spoofProvider"].is_boolean()) {
spoofProvider = json["spoofProvider"].get<bool>();
json.erase("spoofProvider");
}
if (json.contains("spoofProps") && json["spoofProps"].is_boolean()) {
spoofProps = json["spoofProps"].get<bool>();
json.erase("spoofProps");
}
if (json.contains("spoofSignature") && json["spoofSignature"].is_boolean()) {
spoofSignature = json["spoofSignature"].get<bool>();
json.erase("spoofSignature");
}
if (json.contains("DEBUG") && json["DEBUG"].is_boolean()) {
DEBUG = json["DEBUG"].get<bool>();
json.erase("DEBUG");
}
if (json.contains("FINGERPRINT") && json["FINGERPRINT"].is_string()) {
std::string fingerprint = json["FINGERPRINT"].get<std::string>();
std::vector<std::string> vector;
auto parts = fingerprint | std::views::split('/');
for (const auto &part: parts) {
auto subParts = std::string(part.begin(), part.end()) | std::views::split(':');
for (const auto &subPart: subParts) {
vector.emplace_back(subPart.begin(), subPart.end());
}
}
if (vector.size() == 8) {
json["BRAND"] = vector[0];
json["PRODUCT"] = vector[1];
json["DEVICE"] = vector[2];
json["RELEASE"] = vector[3];
json["ID"] = vector[4];
json["INCREMENTAL"] = vector[5];
json["TYPE"] = vector[6];
json["TAGS"] = vector[7];
} else {
LOGE("Error parsing fingerprint values!");
}
}
if (json.contains("SECURITY_PATCH") && json["SECURITY_PATCH"].is_string()) {
SECURITY_PATCH = json["SECURITY_PATCH"].get<std::string>();
}
if (json.contains("ID") && json["ID"].is_string()) {
BUILD_ID = json["ID"].get<std::string>();
}
}
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);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
return;
}
LOGD("create class loader");
auto dexClClass = env->FindClass("dalvik/system/InMemoryDexClassLoader");
auto dexClInit = env->GetMethodID(dexClClass, "<init>",
"(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V");
auto buffer = env->NewDirectByteBuffer(dexVector.data(),
static_cast<jlong>(dexVector.size()));
auto dexCl = env->NewObject(dexClClass, dexClInit, buffer, systemClassLoader);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
return;
}
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;
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
return;
}
LOGD("call init");
auto entryInit = env->GetStaticMethodID(entryPointClass, "init", "(Ljava/lang/String;ZZ)V");
auto jsonStr = env->NewStringUTF(json.dump().c_str());
env->CallStaticVoidMethod(entryPointClass, entryInit, jsonStr, spoofProvider,
spoofSignature);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
}
env->DeleteLocalRef(entryClassName);
env->DeleteLocalRef(entryClassObj);
env->DeleteLocalRef(jsonStr);
env->DeleteLocalRef(dexCl);
env->DeleteLocalRef(buffer);
env->DeleteLocalRef(dexClClass);
env->DeleteLocalRef(clClass);
LOGD("jni memory free");
}
void UpdateBuildFields() {
jclass buildClass = env->FindClass("android/os/Build");
jclass versionClass = env->FindClass("android/os/Build$VERSION");
for (auto &[key, val]: json.items()) {
if (!val.is_string()) continue;
const char *fieldName = key.c_str();
jfieldID fieldID = env->GetStaticFieldID(buildClass, fieldName, "Ljava/lang/String;");
if (env->ExceptionCheck()) {
env->ExceptionClear();
fieldID = env->GetStaticFieldID(versionClass, fieldName, "Ljava/lang/String;");
if (env->ExceptionCheck()) {
env->ExceptionClear();
continue;
}
}
if (fieldID != nullptr) {
std::string str = val.get<std::string>();
const char *value = str.c_str();
jstring jValue = env->NewStringUTF(value);
env->SetStaticObjectField(buildClass, fieldID, jValue);
if (env->ExceptionCheck()) {
env->ExceptionClear();
continue;
}
LOGD("Set '%s' to '%s'", fieldName, value);
}
}
}
};
static std::vector<char> readFile(const char *path) {
FILE *file = fopen(path, "rb");
if (!file) return {};
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
std::vector<char> vector(size);
fread(vector.data(), 1, size, file);
fclose(file);
return vector;
}
static bool checkOtaZip() {
std::array<char, 256> buffer{};
std::string result;
bool found = false;
std::unique_ptr<FILE, decltype(&pclose)> pipe(
popen("unzip -l /system/etc/security/otacerts.zip", "r"), pclose);
if (!pipe) return false;
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
result += buffer.data();
if (result.find("test") != std::string::npos) {
found = true;
break;
}
}
return found;
}
static void companion(int fd) {
std::vector<char> dex, json;
if (std::filesystem::exists(DEX_PATH)) {
dex = readFile(DEX_PATH);
}
if (std::filesystem::exists(CUSTOM_JSON)) {
json = readFile(CUSTOM_JSON);
} else if (std::filesystem::exists(CUSTOM_JSON_FORK)) {
json = readFile(CUSTOM_JSON_FORK);
} else if (std::filesystem::exists(DEFAULT_JSON)) {
json = readFile(DEFAULT_JSON);
}
size_t dexSize = dex.size();
size_t jsonSize = json.size();
xwrite(fd, &dexSize, sizeof(size_t));
xwrite(fd, &jsonSize, sizeof(size_t));
if (dexSize > 0) {
xwrite(fd, dex.data(), dexSize);
}
if (jsonSize > 0) {
xwrite(fd, json.data(), jsonSize);
}
std::string ts(TS_PATH);
bool trickyStore = std::filesystem::exists(ts) &&
!std::filesystem::exists(ts + "/disable") &&
!std::filesystem::exists(ts + "/remove");
xwrite(fd, &trickyStore, sizeof(bool));
bool testSignedRom = checkOtaZip();
xwrite(fd, &testSignedRom, sizeof(bool));
}
REGISTER_ZYGISK_MODULE(PlayIntegrityFix)
REGISTER_ZYGISK_COMPANION(companion)

View File

@ -1,4 +1,57 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
}
alias(libs.plugins.android.library) apply false
}
tasks.register("copyZygiskFiles") {
doLast {
val moduleFolder = project.rootDir.resolve("module")
val zygiskModule = project.project(":zygisk")
val zygiskBuildDir = zygiskModule.layout.buildDirectory.get().asFile
val classesJar = zygiskBuildDir
.resolve("intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar")
classesJar.copyTo(moduleFolder.resolve("classes.jar"), overwrite = true)
val zygiskSoDir = zygiskBuildDir
.resolve("intermediates/stripped_native_libs/release/stripReleaseDebugSymbols/out/lib")
zygiskSoDir.walk()
.filter { it.isFile && it.name == "libzygisk.so" }
.forEach { soFile ->
val abiFolder = soFile.parentFile.name
val destination = moduleFolder.resolve("zygisk/$abiFolder.so")
soFile.copyTo(destination, overwrite = true)
}
}
}
tasks.register("copyInjectFiles") {
doLast {
val moduleFolder = project.rootDir.resolve("module")
val injectModule = project.project(":inject")
val injectBuildDir = injectModule.layout.buildDirectory.get().asFile
val injectSoDir = injectBuildDir
.resolve("intermediates/stripped_native_libs/release/stripReleaseDebugSymbols/out/lib")
injectSoDir.walk()
.filter { it.isFile && it.name == "libinject.so" }
.forEach { soFile ->
val abiFolder = soFile.parentFile.name
val destination = moduleFolder.resolve("inject/$abiFolder.so")
soFile.copyTo(destination, overwrite = true)
}
}
}
tasks.register<Zip>("zip") {
dependsOn("copyZygiskFiles", "copyInjectFiles")
archiveFileName.set("PlayIntegrityFix.zip")
destinationDirectory.set(project.rootDir.resolve("out"))
from(project.rootDir.resolve("module"))
}

View File

@ -1,11 +1,9 @@
[versions]
agp = "8.8.2"
cxx = "27.0.12077973"
hiddenapibypass = "6.1"
[libraries]
cxx = { group = "org.lsposed.libcxx", name = "libcxx", version.ref = "cxx" }
hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }

View File

72
inject/build.gradle.kts Normal file
View File

@ -0,0 +1,72 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "es.chiteroman.inject"
compileSdk = 35
buildToolsVersion = "35.0.1"
ndkVersion = "28.0.13004108"
buildFeatures {
prefab = true
}
packaging {
jniLibs {
excludes += "**/libdobby.so"
}
resources {
excludes += "**"
}
}
defaultConfig {
minSdk = 26
externalNativeBuild {
cmake {
abiFilters(
"arm64-v8a",
"armeabi-v7a"
)
arguments(
"-DCMAKE_BUILD_TYPE=Release",
"-DANDROID_STL=c++_static",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
"-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON"
)
cFlags("-std=c23")
cppFlags("-std=c++2c")
}
}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
version = "3.28.0+"
}
}
}
afterEvaluate {
tasks.named("assembleRelease") {
finalizedBy(
rootProject.tasks["copyInjectFiles"],
rootProject.tasks["zip"]
)
}
}

View File

21
inject/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.28)
project("inject")
add_library(inject SHARED inject.cpp)
add_subdirectory(Dobby)
target_link_libraries(inject log dobby_static)

View File

@ -0,0 +1,302 @@
#include "dobby.h"
#include "json.hpp"
#include <android/log.h>
#include <jni.h>
#include <sys/system_properties.h>
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "PIF", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "PIF", __VA_ARGS__)
static std::string dir;
static JNIEnv *env;
static nlohmann::json json;
static bool spoofProps = true, spoofProvider = true, spoofSignature = false;
static bool DEBUG = false;
static std::string DEVICE_INITIAL_SDK_INT, SECURITY_PATCH, BUILD_ID;
typedef void (*T_Callback)(void *, const char *, const char *, uint32_t);
static T_Callback o_callback = nullptr;
static void modify_callback(void *cookie, const char *name, const char *value,
uint32_t serial) {
if (!cookie || !name || !value || !o_callback)
return;
const char *oldValue = value;
std::string_view prop(name);
if (prop == "init.svc.adbd") {
value = "stopped";
} else if (prop == "sys.usb.state") {
value = "mtp";
} else if (prop.ends_with("api_level")) {
if (!DEVICE_INITIAL_SDK_INT.empty()) {
value = DEVICE_INITIAL_SDK_INT.c_str();
}
} else if (prop.ends_with(".security_patch")) {
if (!SECURITY_PATCH.empty()) {
value = SECURITY_PATCH.c_str();
}
} else if (prop.ends_with(".build.id")) {
if (!BUILD_ID.empty()) {
value = BUILD_ID.c_str();
}
}
if (strcmp(oldValue, value) == 0) {
if (DEBUG)
LOGD("[%s]: %s (unchanged)", name, oldValue);
} else {
LOGD("[%s]: %s -> %s", name, oldValue, value);
}
return o_callback(cookie, name, value, serial);
}
static void (*o_system_property_read_callback)(prop_info *, T_Callback,
void *) = nullptr;
static void my_system_property_read_callback(prop_info *pi, T_Callback callback,
void *cookie) {
if (pi && callback && cookie)
o_callback = callback;
return o_system_property_read_callback(pi, modify_callback, cookie);
}
static bool doHook() {
void *ptr = DobbySymbolResolver(nullptr, "__system_property_read_callback");
if (ptr && DobbyHook(ptr, (void *) my_system_property_read_callback,
(void **) &o_system_property_read_callback) == 0) {
LOGD("hook __system_property_read_callback successful at %p", ptr);
return true;
}
LOGE("hook __system_property_read_callback failed!");
return false;
}
static void parseJSON() {
if (json.empty())
return;
if (json.contains("DEVICE_INITIAL_SDK_INT")) {
if (json["DEVICE_INITIAL_SDK_INT"].is_string()) {
DEVICE_INITIAL_SDK_INT =
json["DEVICE_INITIAL_SDK_INT"].get<std::string>();
} else if (json["DEVICE_INITIAL_SDK_INT"].is_number_integer()) {
DEVICE_INITIAL_SDK_INT =
std::to_string(json["DEVICE_INITIAL_SDK_INT"].get<int>());
} else {
LOGE("Couldn't parse DEVICE_INITIAL_SDK_INT value!");
}
json.erase("DEVICE_INITIAL_SDK_INT");
}
if (json.contains("spoofProvider") && json["spoofProvider"].is_boolean()) {
spoofProvider = json["spoofProvider"].get<bool>();
json.erase("spoofProvider");
}
if (json.contains("spoofProps") && json["spoofProps"].is_boolean()) {
spoofProps = json["spoofProps"].get<bool>();
json.erase("spoofProps");
}
if (json.contains("spoofSignature") && json["spoofSignature"].is_boolean()) {
spoofSignature = json["spoofSignature"].get<bool>();
json.erase("spoofSignature");
}
if (json.contains("DEBUG") && json["DEBUG"].is_boolean()) {
DEBUG = json["DEBUG"].get<bool>();
json.erase("DEBUG");
}
if (json.contains("FINGERPRINT") && json["FINGERPRINT"].is_string()) {
std::string fingerprint = json["FINGERPRINT"].get<std::string>();
std::vector<std::string> vector;
auto parts = fingerprint | std::views::split('/');
for (const auto &part: parts) {
auto subParts =
std::string(part.begin(), part.end()) | std::views::split(':');
for (const auto &subPart: subParts) {
vector.emplace_back(subPart.begin(), subPart.end());
}
}
if (vector.size() == 8) {
json["BRAND"] = vector[0];
json["PRODUCT"] = vector[1];
json["DEVICE"] = vector[2];
json["RELEASE"] = vector[3];
json["ID"] = vector[4];
json["INCREMENTAL"] = vector[5];
json["TYPE"] = vector[6];
json["TAGS"] = vector[7];
} else {
LOGE("Error parsing fingerprint values!");
}
}
if (json.contains("SECURITY_PATCH") && json["SECURITY_PATCH"].is_string()) {
SECURITY_PATCH = json["SECURITY_PATCH"].get<std::string>();
}
if (json.contains("ID") && json["ID"].is_string()) {
BUILD_ID = json["ID"].get<std::string>();
}
}
static void UpdateBuildFields() {
jclass buildClass = env->FindClass("android/os/Build");
jclass versionClass = env->FindClass("android/os/Build$VERSION");
for (auto &[key, val]: json.items()) {
if (!val.is_string())
continue;
const char *fieldName = key.c_str();
jfieldID fieldID =
env->GetStaticFieldID(buildClass, fieldName, "Ljava/lang/String;");
if (env->ExceptionCheck()) {
env->ExceptionClear();
fieldID =
env->GetStaticFieldID(versionClass, fieldName, "Ljava/lang/String;");
if (env->ExceptionCheck()) {
env->ExceptionClear();
continue;
}
}
if (fieldID != nullptr) {
std::string str = val.get<std::string>();
const char *value = str.c_str();
jstring jValue = env->NewStringUTF(value);
env->SetStaticObjectField(buildClass, fieldID, jValue);
if (env->ExceptionCheck()) {
env->ExceptionClear();
continue;
}
LOGD("Set '%s' to '%s'", fieldName, value);
}
}
}
static 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);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
return;
}
LOGD("create class loader");
auto dexClClass = env->FindClass("dalvik/system/PathClassLoader");
auto dexClInit = env->GetMethodID(
dexClClass, "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V");
auto str1 = env->NewStringUTF((dir + "/classes.dex").c_str());
auto str2 = env->NewStringUTF(dir.c_str());
auto dexCl =
env->NewObject(dexClClass, dexClInit, str1, str2, systemClassLoader);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
return;
}
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;
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
return;
}
LOGD("call init");
auto entryInit = env->GetStaticMethodID(entryPointClass, "init",
"(Ljava/lang/String;ZZ)V");
auto jsonStr = env->NewStringUTF(json.dump().c_str());
env->CallStaticVoidMethod(entryPointClass, entryInit, jsonStr, spoofProvider,
spoofSignature);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
}
env->DeleteLocalRef(entryClassName);
env->DeleteLocalRef(entryClassObj);
env->DeleteLocalRef(jsonStr);
env->DeleteLocalRef(dexCl);
env->DeleteLocalRef(str1);
env->DeleteLocalRef(str2);
env->DeleteLocalRef(dexClClass);
env->DeleteLocalRef(clClass);
LOGD("jni memory free");
}
extern "C" [[gnu::visibility("default"), maybe_unused]] bool
init(JavaVM *vm, const char *rawDir) {
bool close = true;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
LOGE("[INJECT] JNI_ERR!");
return true;
}
dir = rawDir;
LOGD("[INJECT] GMS dir: %s", dir.c_str());
FILE *f = fopen((dir + "/pif.json").c_str(), "r");
json = nlohmann::json::parse(f, nullptr, false, true);
fclose(f);
parseJSON();
UpdateBuildFields();
if (spoofProvider || spoofSignature) {
injectDex();
} else {
LOGD("[INJECT] Dex file won't be injected due spoofProvider and "
"spoofSignature are false");
}
if (spoofProps) {
close = !doHook();
}
LOGD("[INJECT] Done!");
return close;
}

View File

@ -4,5 +4,8 @@
"MODEL": "Pixel 6",
"SECURITY_PATCH": "2025-02-05",
"DEVICE_INITIAL_SDK_INT": 21,
"spoofVendingSdk": 0
"spoofProvider": true,
"spoofProps": true,
"spoofSignature": false,
"DEBUG": false
}

View File

@ -20,4 +20,5 @@ dependencyResolutionManagement {
}
rootProject.name = "PlayIntegrityFix"
include(":app")
include(":zygisk")
include(":inject")

1
zygisk/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

89
zygisk/build.gradle.kts Normal file
View File

@ -0,0 +1,89 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "es.chiteroman.playintegrityfix"
compileSdk = 35
buildToolsVersion = "35.0.1"
ndkVersion = "28.0.13004108"
buildFeatures {
prefab = true
}
packaging {
resources {
excludes += "**"
}
}
defaultConfig {
minSdk = 26
multiDexEnabled = false
externalNativeBuild {
cmake {
abiFilters(
"arm64-v8a",
"armeabi-v7a"
)
arguments(
"-DCMAKE_BUILD_TYPE=MinSizeRel",
"-DANDROID_STL=system",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
)
cFlags(
"-std=c23",
"-fvisibility=hidden",
"-fvisibility-inlines-hidden"
)
cppFlags(
"-std=c++2c",
"-fno-exceptions",
"-fno-rtti",
"-fvisibility=hidden",
"-fvisibility-inlines-hidden"
)
}
}
}
buildTypes {
release {
isMinifyEnabled = true
multiDexEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
version = "3.28.0+"
}
}
}
dependencies {
implementation(libs.hiddenapibypass)
}
afterEvaluate {
tasks.named("assembleRelease") {
finalizedBy(
rootProject.tasks["copyZygiskFiles"],
rootProject.tasks["zip"]
)
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.28)
project("zygisk")
add_library(zygisk SHARED zygisk.cpp)
target_link_libraries(zygisk log)

View File

@ -0,0 +1,314 @@
#include "zygisk.hpp"
#include <android/log.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.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.jar"
#define LIB_64 "/data/adb/modules/playintegrityfix/inject/arm64-v8a.so"
#define LIB_32 "/data/adb/modules/playintegrityfix/inject/armeabi-v7a.so"
#define DEFAULT_JSON "/data/adb/modules/playintegrityfix/pif.json"
#define CUSTOM_JSON_FORK "/data/adb/modules/playintegrityfix/custom.pif.json"
#define CUSTOM_JSON "/data/adb/pif.json"
static ssize_t xread(int fd, void *buffer, size_t count) {
ssize_t total = 0;
auto buf = static_cast<char *>(buffer);
while (count > 0) {
auto ret = TEMP_FAILURE_RETRY(read(fd, buf, count));
if (ret < 0)
return -1;
buf += ret;
total += ret;
count -= ret;
}
return total;
}
static ssize_t xwrite(int fd, const void *buffer, size_t count) {
ssize_t total = 0;
auto buf = (char *) buffer;
while (count > 0) {
auto ret = TEMP_FAILURE_RETRY(write(fd, buf, count));
if (ret < 0)
return -1;
buf += ret;
total += ret;
count -= ret;
}
return total;
}
static bool endsWith(const char *str, const char *suffix) {
if (!str || !suffix)
return false;
auto suffix_len = strlen(suffix);
auto str_len = strlen(str);
if (suffix_len > str_len)
return false;
return strncmp(str + (str_len - suffix_len), suffix, suffix_len) == 0;
}
static char *concatStr(const char *str1, const char *str2) {
if (!str1 || !str2)
return nullptr;
auto len1 = strlen(str1);
auto len2 = strlen(str2);
auto total_length = len1 + len2;
auto concatenated_string =
static_cast<char *>(calloc(total_length + 1, sizeof(char)));
if (!concatenated_string)
return nullptr;
strcpy(concatenated_string, str1);
strcat(concatenated_string, str2);
return concatenated_string;
}
static bool copyFile(const char *source_path, const char *dest_path,
mode_t permissions) {
int source_fd;
int dest_fd;
char buffer[8192];
ssize_t bytes_read, bytes_written;
source_fd = open(source_path, O_RDONLY);
if (source_fd == -1) {
LOGE("Error opening source file: %s", strerror(errno));
return false;
}
dest_fd = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC, permissions);
if (dest_fd == -1) {
LOGE("Error opening destination file: %s", strerror(errno));
close(source_fd);
return false;
}
while ((bytes_read = read(source_fd, buffer, 8192)) > 0) {
bytes_written = write(dest_fd, buffer, bytes_read);
if (bytes_written == -1) {
LOGE("Error writing to destination file: %s", strerror(errno));
close(source_fd);
close(dest_fd);
return false;
}
if (bytes_written != bytes_read) {
LOGD("Warning: Incomplete write to destination file.");
}
}
if (bytes_read == -1) {
LOGE("Error reading from source file: %s", strerror(errno));
close(source_fd);
close(dest_fd);
return false;
}
if (chmod(dest_path, permissions) == -1) {
LOGE("Error setting file permissions: %s", strerror(errno));
close(source_fd);
close(dest_fd);
return false;
}
close(source_fd);
close(dest_fd);
return true;
}
static void companion(int fd) {
bool ok = true;
size_t size = 0;
xread(fd, &size, sizeof(size_t));
auto dir = static_cast<char *>(calloc(size + 1, sizeof(char)));
xread(fd, dir, size);
LOGD("[COMPANION] GMS dir: %s", dir);
auto libFile = concatStr(dir, "/libinject.so");
#if defined(__aarch64__)
ok &= copyFile(LIB_64, libFile, 0777);
#elif defined(__arm__)
ok &= copyFile(LIB_32, libFile, 0777);
#endif
free(libFile);
LOGD("[COMPANION] copied inject lib");
auto dexFile = concatStr(dir, "/classes.jar");
ok &= copyFile(DEX_PATH, dexFile, 0644);
free(dexFile);
LOGD("[COMPANION] copied dex");
auto jsonFile = concatStr(dir, "/pif.json");
if (!copyFile(CUSTOM_JSON, jsonFile, 0777)) {
if (!copyFile(CUSTOM_JSON_FORK, jsonFile, 0777)) {
if (!copyFile(DEFAULT_JSON, jsonFile, 0777)) {
ok = false;
}
}
}
free(jsonFile);
LOGD("[COMPANION] copied json");
free(dir);
xwrite(fd, &ok, sizeof(bool));
}
using namespace zygisk;
class PlayIntegrityFix : public ModuleBase {
public:
void onLoad(Api *api_, JNIEnv *env_) override {
this->api = api_;
this->env = env_;
}
void preAppSpecialize(AppSpecializeArgs *args) override {
const char *name{};
int fd;
size_t size;
bool ok = false;
api->setOption(DLCLOSE_MODULE_LIBRARY);
dir = env->GetStringUTFChars(args->app_data_dir, nullptr);
if (!endsWith(dir, "/com.google.android.gms")) {
goto clear;
}
if (endsWith(dir, "/com.android.vending")) {
api->setOption(FORCE_DENYLIST_UNMOUNT);
goto clear;
}
api->setOption(FORCE_DENYLIST_UNMOUNT);
name = env->GetStringUTFChars(args->nice_name, nullptr);
if (strcmp(name, "com.google.android.gms.unstable") != 0) {
goto clear;
}
env->ReleaseStringUTFChars(args->nice_name, name);
fd = api->connectCompanion();
size = strlen(dir);
xwrite(fd, &size, sizeof(size_t));
xwrite(fd, dir, size);
xread(fd, &ok, sizeof(bool));
close(fd);
if (!ok) {
LOGE("ERROR");
goto clear;
}
return;
clear:
if (name)
env->ReleaseStringUTFChars(args->nice_name, name);
if (dir)
env->ReleaseStringUTFChars(args->app_data_dir, dir);
dir = nullptr;
}
void postAppSpecialize(const AppSpecializeArgs *args) override {
if (!dir)
return;
typedef bool (*InitFunc)(JavaVM *, const char *);
char *lib{};
void *handle{};
InitFunc initPtr;
const char *error{};
bool close = false;
JavaVM *vm{};
lib = concatStr(dir, "/libinject.so");
handle = dlopen(lib, RTLD_NOW);
free(lib);
if (!handle) {
LOGE("Error loading lib: %s", dlerror());
goto clear;
}
dlerror();
initPtr = (InitFunc) dlsym(handle, "init");
error = dlerror();
if (error) {
LOGE("Error loading symbol: %s", error);
dlclose(handle);
goto clear;
}
env->GetJavaVM(&vm);
if (vm)
close = initPtr(vm, dir);
else {
LOGE("jvm is null!");
dlclose(handle);
goto clear;
}
LOGD("DONE");
if (close) {
dlclose(handle);
LOGD("dlclose injected lib!");
}
clear:
env->ReleaseStringUTFChars(args->app_data_dir, dir);
dir = nullptr;
LOGD("clear");
}
void preServerSpecialize(ServerSpecializeArgs *args) override {
api->setOption(DLCLOSE_MODULE_LIBRARY);
}
private:
Api *api{};
JNIEnv *env{};
const char *dir{};
};
REGISTER_ZYGISK_MODULE(PlayIntegrityFix)
REGISTER_ZYGISK_COMPANION(companion)