feat:增加gadget配置界面

This commit is contained in:
jiqiu2021
2025-06-27 16:15:38 +08:00
parent cc4fb60b7b
commit 7e5a96cd78
6 changed files with 273 additions and 3 deletions

View File

@@ -140,6 +140,8 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
RadioButton radioStandardInjection = dialogView.findViewById(R.id.radioStandardInjection); RadioButton radioStandardInjection = dialogView.findViewById(R.id.radioStandardInjection);
RadioButton radioRiruInjection = dialogView.findViewById(R.id.radioRiruInjection); RadioButton radioRiruInjection = dialogView.findViewById(R.id.radioRiruInjection);
RadioButton radioCustomLinkerInjection = dialogView.findViewById(R.id.radioCustomLinkerInjection); 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()); appIcon.setImageDrawable(appInfo.getAppIcon());
appName.setText(appInfo.getAppName()); appName.setText(appInfo.getAppName());
@@ -155,6 +157,33 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
radioStandardInjection.setChecked(true); 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 // Setup SO list
List<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles(); List<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles();
List<ConfigManager.SoFile> appSoFiles = configManager.getAppSoFiles(appInfo.getPackageName()); List<ConfigManager.SoFile> appSoFiles = configManager.getAppSoFiles(appInfo.getPackageName());
@@ -187,6 +216,16 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
} }
configManager.setAppInjectionMethod(appInfo.getPackageName(), selectedMethod); 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 // Save SO selection
if (soListRecyclerView.getAdapter() != null) { if (soListRecyclerView.getAdapter() != null) {
SoSelectionAdapter adapter = (SoSelectionAdapter) soListRecyclerView.getAdapter(); SoSelectionAdapter adapter = (SoSelectionAdapter) soListRecyclerView.getAdapter();

View File

@@ -262,6 +262,119 @@ public class ConfigManager {
saveConfig(); 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 // Copy SO files directly to app's data directory
private void deploySoFilesToApp(String packageName) { private void deploySoFilesToApp(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName); AppConfig appConfig = config.perAppConfig.get(packageName);
@@ -350,6 +463,11 @@ public class ConfigManager {
} }
Log.i(TAG, "Deployment complete for: " + packageName); 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 // 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); Log.i(TAG, "Cleanup complete for: " + packageName);
} }
@@ -420,6 +555,7 @@ public class ConfigManager {
public boolean enabled = false; public boolean enabled = false;
public List<SoFile> soFiles = new ArrayList<>(); public List<SoFile> soFiles = new ArrayList<>();
public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker" public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker"
public GadgetConfig gadgetConfig = null;
} }
public static class SoFile { public static class SoFile {
@@ -435,4 +571,12 @@ public class ConfigManager {
return false; 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";
}
} }

View File

@@ -81,13 +81,52 @@
android:textColor="?android:attr/textColorTertiary" android:textColor="?android:attr/textColorTertiary"
android:visibility="gone" /> android:visibility="gone" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<CheckBox
android:id="@+id/checkboxEnableGadget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="启用 Gadget 配置"
android:layout_marginEnd="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfigureGadget"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="配置"
android:textSize="12sp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:enabled="false" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Gadget可用于Frida调试勾选后可配置监听地址和端口"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="16dp" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="注入方式" android:text="注入方式"
android:textSize="14sp" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp" /> android:layout_marginBottom="8dp" />
<RadioGroup <RadioGroup

View File

@@ -92,11 +92,44 @@ namespace Config {
} }
} }
// Parse gadgetConfig if exists
size_t gadgetPos = appJson.find("\"gadgetConfig\"");
if (gadgetPos != std::string::npos) {
size_t gadgetObjStart = appJson.find("{", gadgetPos);
size_t gadgetObjEnd = appJson.find("}", gadgetObjStart);
if (gadgetObjStart != std::string::npos && gadgetObjEnd != std::string::npos) {
std::string gadgetObj = appJson.substr(gadgetObjStart, gadgetObjEnd - gadgetObjStart + 1);
GadgetConfig* gadgetConfig = new GadgetConfig();
std::string address = extractValue(gadgetObj, "address");
if (!address.empty()) gadgetConfig->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; g_config.perAppConfig[packageName] = appConfig;
const char* methodName = appConfig.injectionMethod == InjectionMethod::CUSTOM_LINKER ? "custom_linker" : const char* methodName = appConfig.injectionMethod == InjectionMethod::CUSTOM_LINKER ? "custom_linker" :
appConfig.injectionMethod == InjectionMethod::RIRU ? "riru" : "standard"; appConfig.injectionMethod == InjectionMethod::RIRU ? "riru" : "standard";
LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu", LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu, gadget: %s",
packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size()); packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size(),
appConfig.gadgetConfig ? "yes" : "no");
} }
ModuleConfig readConfig() { ModuleConfig readConfig() {

View File

@@ -19,10 +19,19 @@ namespace Config {
CUSTOM_LINKER = 2 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 { struct AppConfig {
bool enabled = false; bool enabled = false;
InjectionMethod injectionMethod = InjectionMethod::STANDARD; InjectionMethod injectionMethod = InjectionMethod::STANDARD;
std::vector<SoFile> soFiles; std::vector<SoFile> soFiles;
GadgetConfig* gadgetConfig = nullptr;
}; };
struct ModuleConfig { struct ModuleConfig {

View File

@@ -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 // Load each SO file using the configured method
for (const auto &soFile : soFiles) { 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()); LOGI("Loading SO: %s (stored as: %s)", soFile.name.c_str(), soFile.storedPath.c_str());
if (method == Config::InjectionMethod::CUSTOM_LINKER) { if (method == Config::InjectionMethod::CUSTOM_LINKER) {