feat:增加gadget配置界面
This commit is contained in:
@@ -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<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles();
|
||||
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);
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -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<SoFile> 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";
|
||||
}
|
||||
}
|
||||
@@ -81,13 +81,52 @@
|
||||
android:textColor="?android:attr/textColorTertiary"
|
||||
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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="注入方式"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
|
||||
@@ -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;
|
||||
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() {
|
||||
|
||||
@@ -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<SoFile> soFiles;
|
||||
GadgetConfig* gadgetConfig = nullptr;
|
||||
};
|
||||
|
||||
struct ModuleConfig {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user