From 7e5a96cd7880dffe39c80b916d0a64f97fc9cf91 Mon Sep 17 00:00:00 2001 From: jiqiu2021 Date: Fri, 27 Jun 2025 16:15:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0gadget=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/jiqiu/configapp/AppListFragment.java | 39 +++++ .../com/jiqiu/configapp/ConfigManager.java | 144 ++++++++++++++++++ .../src/main/res/layout/dialog_app_config.xml | 41 ++++- module/src/main/cpp/config.cpp | 37 ++++- module/src/main/cpp/config.h | 9 ++ module/src/main/cpp/hack_new.cpp | 6 + 6 files changed, 273 insertions(+), 3 deletions(-) diff --git a/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java b/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java index e50778c..99e753b 100644 --- a/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java +++ b/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java @@ -140,6 +140,8 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog RadioButton radioStandardInjection = dialogView.findViewById(R.id.radioStandardInjection); RadioButton radioRiruInjection = dialogView.findViewById(R.id.radioRiruInjection); RadioButton radioCustomLinkerInjection = dialogView.findViewById(R.id.radioCustomLinkerInjection); + CheckBox checkboxEnableGadget = dialogView.findViewById(R.id.checkboxEnableGadget); + com.google.android.material.button.MaterialButton btnConfigureGadget = dialogView.findViewById(R.id.btnConfigureGadget); appIcon.setImageDrawable(appInfo.getAppIcon()); appName.setText(appInfo.getAppName()); @@ -155,6 +157,33 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog radioStandardInjection.setChecked(true); } + // Load gadget config + ConfigManager.GadgetConfig gadgetConfig = configManager.getAppGadgetConfig(appInfo.getPackageName()); + checkboxEnableGadget.setChecked(gadgetConfig != null); + btnConfigureGadget.setEnabled(gadgetConfig != null); + + // Setup gadget listeners + checkboxEnableGadget.setOnCheckedChangeListener((buttonView, isChecked) -> { + btnConfigureGadget.setEnabled(isChecked); + if (!isChecked) { + // Remove gadget config when unchecked + configManager.setAppGadgetConfig(appInfo.getPackageName(), null); + } + }); + + btnConfigureGadget.setOnClickListener(v -> { + ConfigManager.GadgetConfig currentConfig = configManager.getAppGadgetConfig(appInfo.getPackageName()); + if (currentConfig == null) { + currentConfig = new ConfigManager.GadgetConfig(); + } + + GadgetConfigDialog gadgetDialog = GadgetConfigDialog.newInstance(currentConfig); + gadgetDialog.setOnGadgetConfigListener(config -> { + configManager.setAppGadgetConfig(appInfo.getPackageName(), config); + }); + gadgetDialog.show(getParentFragmentManager(), "gadget_config"); + }); + // Setup SO list List globalSoFiles = configManager.getAllSoFiles(); List appSoFiles = configManager.getAppSoFiles(appInfo.getPackageName()); @@ -187,6 +216,16 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog } configManager.setAppInjectionMethod(appInfo.getPackageName(), selectedMethod); + // Save gadget config if enabled + if (checkboxEnableGadget.isChecked()) { + ConfigManager.GadgetConfig currentGadgetConfig = configManager.getAppGadgetConfig(appInfo.getPackageName()); + if (currentGadgetConfig == null) { + // Create default config if not already configured + currentGadgetConfig = new ConfigManager.GadgetConfig(); + configManager.setAppGadgetConfig(appInfo.getPackageName(), currentGadgetConfig); + } + } + // Save SO selection if (soListRecyclerView.getAdapter() != null) { SoSelectionAdapter adapter = (SoSelectionAdapter) soListRecyclerView.getAdapter(); diff --git a/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java b/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java index 272816d..e880090 100644 --- a/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java +++ b/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java @@ -262,6 +262,119 @@ public class ConfigManager { saveConfig(); } + public GadgetConfig getAppGadgetConfig(String packageName) { + AppConfig appConfig = config.perAppConfig.get(packageName); + if (appConfig == null) { + return null; + } + return appConfig.gadgetConfig; + } + + public void setAppGadgetConfig(String packageName, GadgetConfig gadgetConfig) { + AppConfig appConfig = config.perAppConfig.get(packageName); + if (appConfig == null) { + appConfig = new AppConfig(); + config.perAppConfig.put(packageName, appConfig); + } + + // Remove old gadget from SO list if exists + if (appConfig.gadgetConfig != null) { + String oldGadgetName = appConfig.gadgetConfig.gadgetName; + appConfig.soFiles.removeIf(soFile -> soFile.name.equals(oldGadgetName)); + } + + appConfig.gadgetConfig = gadgetConfig; + + // Add new gadget to SO list if configured + if (gadgetConfig != null) { + // Check if gadget SO file exists in global storage + String gadgetPath = SO_STORAGE_DIR + "/" + gadgetConfig.gadgetName; + Shell.Result checkResult = Shell.cmd("test -f \"" + gadgetPath + "\" && echo 'exists'").exec(); + + if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) { + // Add gadget as a SO file + SoFile gadgetSoFile = new SoFile(); + gadgetSoFile.name = gadgetConfig.gadgetName; + gadgetSoFile.storedPath = gadgetPath; + gadgetSoFile.originalPath = gadgetPath; + + // Check if already in list + boolean alreadyExists = false; + for (SoFile soFile : appConfig.soFiles) { + if (soFile.name.equals(gadgetSoFile.name)) { + alreadyExists = true; + break; + } + } + + if (!alreadyExists) { + appConfig.soFiles.add(gadgetSoFile); + Log.i(TAG, "Added gadget SO to app's SO list: " + gadgetSoFile.name); + } + } else { + Log.w(TAG, "Gadget SO file not found in storage: " + gadgetPath); + Log.w(TAG, "Please ensure " + gadgetConfig.gadgetName + " is added to SO library"); + } + } + + saveConfig(); + + // If app is enabled, deploy both gadget SO and config file + if (appConfig.enabled) { + if (gadgetConfig != null) { + deployGadgetConfigFile(packageName, gadgetConfig); + } + // Re-deploy all SO files including gadget + deploySoFilesToApp(packageName); + } + } + + private void deployGadgetConfigFile(String packageName, GadgetConfig gadgetConfig) { + try { + // Create gadget config JSON + String configJson = String.format( + "{\n" + + " \"interaction\": {\n" + + " \"type\": \"listen\",\n" + + " \"address\": \"%s\",\n" + + " \"port\": %d,\n" + + " \"on_port_conflict\": \"%s\",\n" + + " \"on_load\": \"%s\"\n" + + " }\n" + + "}", + gadgetConfig.address, + gadgetConfig.port, + gadgetConfig.onPortConflict, + gadgetConfig.onLoad + ); + + // Write to temp file + String tempFile = context.getCacheDir() + "/" + gadgetConfig.gadgetName + ".config"; + java.io.FileWriter writer = new java.io.FileWriter(tempFile); + writer.write(configJson); + writer.close(); + + // Copy to app's files directory + String filesDir = "/data/data/" + packageName + "/files"; + String gadgetConfigName = gadgetConfig.gadgetName.replace(".so", ".config.so"); + String targetPath = filesDir + "/" + gadgetConfigName; + + Shell.Result copyResult = Shell.cmd("cp " + tempFile + " " + targetPath).exec(); + if (copyResult.isSuccess()) { + // Set permissions + Shell.cmd("chmod 644 " + targetPath).exec(); + Log.i(TAG, "Deployed gadget config to: " + targetPath); + } else { + Log.e(TAG, "Failed to deploy gadget config: " + String.join("\n", copyResult.getErr())); + } + + // Clean up temp file + new java.io.File(tempFile).delete(); + } catch (Exception e) { + Log.e(TAG, "Failed to create gadget config file", e); + } + } + // Copy SO files directly to app's data directory private void deploySoFilesToApp(String packageName) { AppConfig appConfig = config.perAppConfig.get(packageName); @@ -350,6 +463,11 @@ public class ConfigManager { } Log.i(TAG, "Deployment complete for: " + packageName); + + // Deploy gadget config if configured + if (appConfig.gadgetConfig != null) { + deployGadgetConfigFile(packageName, appConfig.gadgetConfig); + } } // Clean up deployed SO files when app is disabled @@ -395,6 +513,23 @@ public class ConfigManager { } } + // Clean up gadget config file if exists + if (appConfig.gadgetConfig != null) { + String gadgetConfigName = appConfig.gadgetConfig.gadgetName.replace(".so", ".config.so"); + String configPath = filesDir + "/" + gadgetConfigName; + + Shell.Result checkConfigResult = Shell.cmd("test -f \"" + configPath + "\" && echo 'exists'").exec(); + if (checkConfigResult.isSuccess() && !checkConfigResult.getOut().isEmpty()) { + Shell.Result deleteResult = Shell.cmd("rm -f \"" + configPath + "\"").exec(); + if (deleteResult.isSuccess()) { + Log.i(TAG, "Deleted gadget config: " + configPath); + } else { + // Try with su -c + Shell.cmd("su -c 'rm -f \"" + configPath + "\"'").exec(); + } + } + } + Log.i(TAG, "Cleanup complete for: " + packageName); } @@ -420,6 +555,7 @@ public class ConfigManager { public boolean enabled = false; public List soFiles = new ArrayList<>(); public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker" + public GadgetConfig gadgetConfig = null; } public static class SoFile { @@ -435,4 +571,12 @@ public class ConfigManager { return false; } } + + public static class GadgetConfig { + public String address = "0.0.0.0"; + public int port = 27042; + public String onPortConflict = "fail"; + public String onLoad = "wait"; + public String gadgetName = "libgadget.so"; + } } \ No newline at end of file diff --git a/configapp/src/main/res/layout/dialog_app_config.xml b/configapp/src/main/res/layout/dialog_app_config.xml index d905ee8..5b867bd 100644 --- a/configapp/src/main/res/layout/dialog_app_config.xml +++ b/configapp/src/main/res/layout/dialog_app_config.xml @@ -81,13 +81,52 @@ android:textColor="?android:attr/textColorTertiary" android:visibility="gone" /> + + + + + + + + + + + + address = address; + + std::string portStr = extractValue(gadgetObj, "port"); + if (!portStr.empty()) gadgetConfig->port = std::stoi(portStr); + + std::string onPortConflict = extractValue(gadgetObj, "onPortConflict"); + if (!onPortConflict.empty()) gadgetConfig->onPortConflict = onPortConflict; + + std::string onLoad = extractValue(gadgetObj, "onLoad"); + if (!onLoad.empty()) gadgetConfig->onLoad = onLoad; + + std::string gadgetName = extractValue(gadgetObj, "gadgetName"); + if (!gadgetName.empty()) gadgetConfig->gadgetName = gadgetName; + + appConfig.gadgetConfig = gadgetConfig; + LOGD("Loaded gadget config: %s:%d, name: %s", + gadgetConfig->address.c_str(), gadgetConfig->port, gadgetConfig->gadgetName.c_str()); + } + } + g_config.perAppConfig[packageName] = appConfig; const char* methodName = appConfig.injectionMethod == InjectionMethod::CUSTOM_LINKER ? "custom_linker" : appConfig.injectionMethod == InjectionMethod::RIRU ? "riru" : "standard"; - LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu", - packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size()); + LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu, gadget: %s", + packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size(), + appConfig.gadgetConfig ? "yes" : "no"); } ModuleConfig readConfig() { diff --git a/module/src/main/cpp/config.h b/module/src/main/cpp/config.h index 9ed022b..289e400 100644 --- a/module/src/main/cpp/config.h +++ b/module/src/main/cpp/config.h @@ -19,10 +19,19 @@ namespace Config { CUSTOM_LINKER = 2 }; + struct GadgetConfig { + std::string address = "0.0.0.0"; + int port = 27042; + std::string onPortConflict = "fail"; + std::string onLoad = "wait"; + std::string gadgetName = "libgadget.so"; + }; + struct AppConfig { bool enabled = false; InjectionMethod injectionMethod = InjectionMethod::STANDARD; std::vector soFiles; + GadgetConfig* gadgetConfig = nullptr; }; struct ModuleConfig { diff --git a/module/src/main/cpp/hack_new.cpp b/module/src/main/cpp/hack_new.cpp index 1bd3d68..07ecf2f 100644 --- a/module/src/main/cpp/hack_new.cpp +++ b/module/src/main/cpp/hack_new.cpp @@ -107,6 +107,12 @@ void hack_thread_func(const char *game_data_dir, const char *package_name, JavaV // Load each SO file using the configured method for (const auto &soFile : soFiles) { + // Skip config files + if (soFile.name.find(".config.so") != std::string::npos) { + LOGI("Skipping config file: %s", soFile.name.c_str()); + continue; + } + LOGI("Loading SO: %s (stored as: %s)", soFile.name.c_str(), soFile.storedPath.c_str()); if (method == Config::InjectionMethod::CUSTOM_LINKER) {