|
|
|
|
@@ -27,13 +27,17 @@ public class ConfigManager {
|
|
|
|
|
// Configure Shell to use root
|
|
|
|
|
Shell.enableVerboseLogging = BuildConfig.DEBUG;
|
|
|
|
|
Shell.setDefaultBuilder(Shell.Builder.create()
|
|
|
|
|
.setFlags(Shell.FLAG_REDIRECT_STDERR)
|
|
|
|
|
.setTimeout(10));
|
|
|
|
|
.setFlags(Shell.FLAG_REDIRECT_STDERR | Shell.FLAG_MOUNT_MASTER)
|
|
|
|
|
.setTimeout(30));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ConfigManager(Context context) {
|
|
|
|
|
this.context = context;
|
|
|
|
|
this.gson = new GsonBuilder().setPrettyPrinting().create();
|
|
|
|
|
|
|
|
|
|
// Ensure we get root shell on creation
|
|
|
|
|
Shell.getShell();
|
|
|
|
|
|
|
|
|
|
loadConfig();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -42,8 +46,32 @@ public class ConfigManager {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void ensureModuleDirectories() {
|
|
|
|
|
Shell.cmd("mkdir -p " + MODULE_PATH).exec();
|
|
|
|
|
Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
|
|
|
|
|
// Check root access first
|
|
|
|
|
if (!isRootAvailable()) {
|
|
|
|
|
Log.e(TAG, "Root access not available!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create module directories
|
|
|
|
|
Shell.Result result1 = Shell.cmd("mkdir -p " + MODULE_PATH).exec();
|
|
|
|
|
if (!result1.isSuccess()) {
|
|
|
|
|
Log.e(TAG, "Failed to create module directory: " + MODULE_PATH);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Shell.Result result2 = Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
|
|
|
|
|
if (!result2.isSuccess()) {
|
|
|
|
|
Log.e(TAG, "Failed to create SO storage directory: " + SO_STORAGE_DIR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set permissions
|
|
|
|
|
Shell.cmd("chmod 755 " + MODULE_PATH).exec();
|
|
|
|
|
Shell.cmd("chmod 755 " + SO_STORAGE_DIR).exec();
|
|
|
|
|
|
|
|
|
|
// Verify directories exist
|
|
|
|
|
Shell.Result verify = Shell.cmd("ls -la " + MODULE_PATH).exec();
|
|
|
|
|
if (verify.isSuccess()) {
|
|
|
|
|
Log.i(TAG, "Module directory ready: " + String.join("\n", verify.getOut()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void loadConfig() {
|
|
|
|
|
@@ -94,6 +122,13 @@ public class ConfigManager {
|
|
|
|
|
}
|
|
|
|
|
appConfig.enabled = enabled;
|
|
|
|
|
saveConfig();
|
|
|
|
|
|
|
|
|
|
// 自动部署或清理 SO 文件
|
|
|
|
|
if (enabled) {
|
|
|
|
|
deploySoFilesToApp(packageName);
|
|
|
|
|
} else {
|
|
|
|
|
cleanupAppSoFiles(packageName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<SoFile> getAppSoFiles(String packageName) {
|
|
|
|
|
@@ -163,6 +198,11 @@ public class ConfigManager {
|
|
|
|
|
// Add reference to the global SO file
|
|
|
|
|
appConfig.soFiles.add(globalSoFile);
|
|
|
|
|
saveConfig();
|
|
|
|
|
|
|
|
|
|
// If app is enabled, deploy the new SO file
|
|
|
|
|
if (appConfig.enabled) {
|
|
|
|
|
deploySoFilesToApp(packageName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void removeSoFileFromApp(String packageName, SoFile soFile) {
|
|
|
|
|
@@ -171,6 +211,11 @@ public class ConfigManager {
|
|
|
|
|
|
|
|
|
|
appConfig.soFiles.removeIf(s -> s.storedPath.equals(soFile.storedPath));
|
|
|
|
|
saveConfig();
|
|
|
|
|
|
|
|
|
|
// If app is enabled, re-deploy to update SO files
|
|
|
|
|
if (appConfig.enabled) {
|
|
|
|
|
deploySoFilesToApp(packageName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean getHideInjection() {
|
|
|
|
|
@@ -182,6 +227,152 @@ public class ConfigManager {
|
|
|
|
|
saveConfig();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy SO files directly to app's data directory
|
|
|
|
|
private void deploySoFilesToApp(String packageName) {
|
|
|
|
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
|
|
|
|
if (appConfig == null || appConfig.soFiles.isEmpty()) {
|
|
|
|
|
Log.w(TAG, "No SO files to deploy for: " + packageName);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// First check if we have root access
|
|
|
|
|
if (!Shell.getShell().isRoot()) {
|
|
|
|
|
Log.e(TAG, "No root access available!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create files directory in app's data dir
|
|
|
|
|
String filesDir = "/data/data/" + packageName + "/files";
|
|
|
|
|
|
|
|
|
|
// Use su -c for better compatibility
|
|
|
|
|
Shell.Result mkdirResult = Shell.cmd("su -c 'mkdir -p " + filesDir + "'").exec();
|
|
|
|
|
if (!mkdirResult.isSuccess()) {
|
|
|
|
|
Log.e(TAG, "Failed to create directory: " + filesDir);
|
|
|
|
|
Log.e(TAG, "Error: " + String.join("\n", mkdirResult.getErr()));
|
|
|
|
|
// Try without su -c
|
|
|
|
|
mkdirResult = Shell.cmd("mkdir -p " + filesDir).exec();
|
|
|
|
|
if (!mkdirResult.isSuccess()) {
|
|
|
|
|
Log.e(TAG, "Also failed without su -c");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set proper permissions and ownership
|
|
|
|
|
Shell.cmd("chmod 755 " + filesDir).exec();
|
|
|
|
|
|
|
|
|
|
// Get UID for the package
|
|
|
|
|
Shell.Result uidResult = Shell.cmd("stat -c %u /data/data/" + packageName).exec();
|
|
|
|
|
String uid = "";
|
|
|
|
|
if (uidResult.isSuccess() && !uidResult.getOut().isEmpty()) {
|
|
|
|
|
uid = uidResult.getOut().get(0).trim();
|
|
|
|
|
Log.i(TAG, "Package UID: " + uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy each SO file configured for this app
|
|
|
|
|
for (SoFile soFile : appConfig.soFiles) {
|
|
|
|
|
// Extract mapped filename
|
|
|
|
|
String mappedName = new File(soFile.storedPath).getName();
|
|
|
|
|
String destPath = filesDir + "/" + mappedName;
|
|
|
|
|
|
|
|
|
|
// Check if source file exists
|
|
|
|
|
Shell.Result checkResult = Shell.cmd("test -f \"" + soFile.storedPath + "\" && echo 'exists'").exec();
|
|
|
|
|
if (!checkResult.isSuccess() || checkResult.getOut().isEmpty()) {
|
|
|
|
|
Log.e(TAG, "Source SO file not found: " + soFile.storedPath);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "Copying: " + soFile.storedPath + " to " + destPath);
|
|
|
|
|
|
|
|
|
|
// Copy file using cat to avoid permission issues
|
|
|
|
|
String copyCmd = "cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"";
|
|
|
|
|
Shell.Result result = Shell.cmd(copyCmd).exec();
|
|
|
|
|
|
|
|
|
|
if (!result.isSuccess()) {
|
|
|
|
|
Log.e(TAG, "Failed with cat, trying cp");
|
|
|
|
|
// Fallback to cp
|
|
|
|
|
result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set permissions
|
|
|
|
|
Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
|
|
|
|
|
|
|
|
|
|
// Set ownership if we have the UID
|
|
|
|
|
if (!uid.isEmpty()) {
|
|
|
|
|
Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify the file was copied
|
|
|
|
|
Shell.Result verifyResult = Shell.cmd("ls -la \"" + destPath + "\" 2>/dev/null").exec();
|
|
|
|
|
if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
|
|
|
|
|
Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
|
|
|
|
|
} else {
|
|
|
|
|
Log.e(TAG, "Failed to verify SO file copy: " + destPath);
|
|
|
|
|
// Try another verification method
|
|
|
|
|
Shell.Result sizeResult = Shell.cmd("stat -c %s \"" + destPath + "\" 2>/dev/null").exec();
|
|
|
|
|
if (sizeResult.isSuccess() && !sizeResult.getOut().isEmpty()) {
|
|
|
|
|
Log.i(TAG, "File exists with size: " + sizeResult.getOut().get(0) + " bytes");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "Deployment complete for: " + packageName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clean up deployed SO files when app is disabled
|
|
|
|
|
private void cleanupAppSoFiles(String packageName) {
|
|
|
|
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
|
|
|
|
if (appConfig == null || appConfig.soFiles.isEmpty()) {
|
|
|
|
|
Log.w(TAG, "No SO files to clean up for: " + packageName);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// First check if we have root access
|
|
|
|
|
if (!Shell.getShell().isRoot()) {
|
|
|
|
|
Log.e(TAG, "No root access available!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String filesDir = "/data/data/" + packageName + "/files";
|
|
|
|
|
|
|
|
|
|
// Only delete the SO files we deployed, not the entire directory
|
|
|
|
|
for (SoFile soFile : appConfig.soFiles) {
|
|
|
|
|
String mappedName = new File(soFile.storedPath).getName();
|
|
|
|
|
String filePath = filesDir + "/" + mappedName;
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "Cleaning up: " + filePath);
|
|
|
|
|
|
|
|
|
|
// Check if file exists before trying to delete
|
|
|
|
|
Shell.Result checkResult = Shell.cmd("test -f \"" + filePath + "\" && echo 'exists'").exec();
|
|
|
|
|
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
|
|
|
|
|
// Try to remove the file
|
|
|
|
|
Shell.Result result = Shell.cmd("rm -f \"" + filePath + "\"").exec();
|
|
|
|
|
|
|
|
|
|
// Verify deletion
|
|
|
|
|
Shell.Result verifyResult = Shell.cmd("test -f \"" + filePath + "\" && echo 'still_exists'").exec();
|
|
|
|
|
if (!verifyResult.isSuccess() || verifyResult.getOut().isEmpty()) {
|
|
|
|
|
Log.i(TAG, "Successfully deleted SO file: " + filePath);
|
|
|
|
|
} else {
|
|
|
|
|
Log.e(TAG, "Failed to delete SO file: " + filePath);
|
|
|
|
|
// Try with su -c
|
|
|
|
|
Shell.cmd("su -c 'rm -f \"" + filePath + "\"'").exec();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Log.w(TAG, "SO file not found for cleanup: " + filePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "Cleanup complete for: " + packageName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Deploy SO files for all enabled apps
|
|
|
|
|
public void deployAllSoFiles() {
|
|
|
|
|
for (Map.Entry<String, AppConfig> entry : config.perAppConfig.entrySet()) {
|
|
|
|
|
if (entry.getValue().enabled) {
|
|
|
|
|
deploySoFilesToApp(entry.getKey());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Data classes
|
|
|
|
|
public static class ModuleConfig {
|
|
|
|
|
public boolean enabled = true;
|
|
|
|
|
|