feat: 基于广播, 实现脚本指定 app 注入和测试
1. 广播实现命令行交互式指定 app 打开端口并自动配置; 2. 广播动态注册, 无法枚举广播接收器, 过滤 callingUid 只接收 shell 和 root 发出的广播; 3. 自动化脚本增加 check_and_set_selinux 方法设置 selinux 为宽容模式解决部分无法启动问题; 4. frida 官方 gadget.so 自动下载和缓存 scripts/.cache/; 5. 自动重启 apk & frida 命令测试;
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".ConfigApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -26,4 +27,4 @@
|
||||
android:parentActivityName=".MainActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.jiqiu.configapp;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Application class for dynamic receiver registration
|
||||
* 动态注册 BroadcastReceiver,避免被第三方 app 发现
|
||||
*/
|
||||
public class ConfigApplication extends Application {
|
||||
private static final String TAG = "ConfigApplication";
|
||||
private static final String ACTION_APPLY_CONFIG = "com.jiqiu.configapp.APPLY_CONFIG";
|
||||
|
||||
private ConfigApplyReceiver configReceiver;
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.d(TAG, "Application onCreate - registering receiver dynamically");
|
||||
|
||||
// 动态注册 ConfigApplyReceiver
|
||||
configReceiver = new ConfigApplyReceiver();
|
||||
IntentFilter filter = new IntentFilter(ACTION_APPLY_CONFIG);
|
||||
|
||||
// 使用 RECEIVER_NOT_EXPORTED 标志,明确表示不导出
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
registerReceiver(configReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
registerReceiver(configReceiver, filter);
|
||||
}
|
||||
Log.d(TAG, "Receiver registered dynamically (UID check: shell/root only)");
|
||||
Log.i(TAG, "ConfigApplyReceiver registered dynamically - invisible to third-party apps");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
|
||||
// 注销 receiver(注意:onTerminate 在真实设备上通常不会被调用,仅在模拟器中)
|
||||
if (configReceiver != null) {
|
||||
try {
|
||||
unregisterReceiver(configReceiver);
|
||||
Log.d(TAG, "Receiver unregistered");
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "Receiver was not registered or already unregistered");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.jiqiu.configapp;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
/**
|
||||
* BroadcastReceiver to apply configurations pushed from ADB
|
||||
* 接收来自 ADB shell 的广播以应用配置
|
||||
*
|
||||
* 安全机制:动态注册 + UID 权限检查,只允许 shell(2000) 或 root(0) 调用
|
||||
*/
|
||||
public class ConfigApplyReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "ConfigApplyReceiver";
|
||||
|
||||
// UID constants
|
||||
private static final int SHELL_UID = 2000; // ADB shell user
|
||||
private static final int ROOT_UID = 0; // Root user
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// 权限检查:只允许 shell 或 root 用户发送广播
|
||||
int callingUid = Binder.getCallingUid();
|
||||
if (callingUid != SHELL_UID && callingUid != ROOT_UID) {
|
||||
Log.w(TAG, "Unauthorized broadcast attempt from UID: " + callingUid);
|
||||
Log.w(TAG, "Only shell (2000) or root (0) can send this broadcast");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Received config apply broadcast from authorized UID: " + callingUid);
|
||||
|
||||
String action = intent.getAction();
|
||||
if (!"com.jiqiu.configapp.APPLY_CONFIG".equals(action)) {
|
||||
Log.w(TAG, "Unknown action: " + action);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取广播参数
|
||||
String packageName = intent.getStringExtra("package_name");
|
||||
String tmpConfigPath = intent.getStringExtra("tmp_config_path");
|
||||
String tmpGadgetConfigPath = intent.getStringExtra("tmp_gadget_config_path");
|
||||
boolean deployOnly = intent.getBooleanExtra("deploy_only", false);
|
||||
|
||||
Log.i(TAG, "Processing config for package: " + packageName);
|
||||
Log.i(TAG, "Config path: " + tmpConfigPath);
|
||||
Log.i(TAG, "Gadget config path: " + tmpGadgetConfigPath);
|
||||
Log.i(TAG, "Deploy only: " + deployOnly);
|
||||
|
||||
if (packageName == null || packageName.isEmpty()) {
|
||||
Log.e(TAG, "Package name is required");
|
||||
return;
|
||||
}
|
||||
|
||||
// 在后台线程处理,避免阻塞主线程
|
||||
new Thread(() -> {
|
||||
try {
|
||||
ConfigManager configManager = new ConfigManager(context);
|
||||
|
||||
// 确保目录存在
|
||||
configManager.ensureModuleDirectories();
|
||||
|
||||
// 如果提供了配置文件路径,复制到模块目录
|
||||
if (tmpConfigPath != null && !tmpConfigPath.isEmpty()) {
|
||||
Shell.Result checkResult = Shell.cmd("test -f \"" + tmpConfigPath + "\" && echo 'exists'").exec();
|
||||
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
|
||||
Log.i(TAG, "Copying main config: " + tmpConfigPath + " -> " + ConfigManager.CONFIG_FILE);
|
||||
Shell.Result copyResult = Shell.cmd(
|
||||
"cp \"" + tmpConfigPath + "\" \"" + ConfigManager.CONFIG_FILE + "\"",
|
||||
"chmod 644 \"" + ConfigManager.CONFIG_FILE + "\""
|
||||
).exec();
|
||||
|
||||
if (copyResult.isSuccess()) {
|
||||
Log.i(TAG, "Main config copied successfully");
|
||||
// 重新加载配置
|
||||
configManager.reloadConfig();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to copy main config: " + String.join("\n", copyResult.getErr()));
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Main config file not found at: " + tmpConfigPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果提供了 Gadget 配置文件,复制到应用数据目录
|
||||
if (tmpGadgetConfigPath != null && !tmpGadgetConfigPath.isEmpty()) {
|
||||
Shell.Result checkResult = Shell.cmd("test -f \"" + tmpGadgetConfigPath + "\" && echo 'exists'").exec();
|
||||
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
|
||||
String filesDir = "/data/data/" + packageName + "/files";
|
||||
|
||||
// 从路径中提取文件名
|
||||
String gadgetConfigFileName = tmpGadgetConfigPath.substring(tmpGadgetConfigPath.lastIndexOf('/') + 1);
|
||||
String targetPath = filesDir + "/" + gadgetConfigFileName;
|
||||
|
||||
Log.i(TAG, "Copying gadget config: " + tmpGadgetConfigPath + " -> " + targetPath);
|
||||
|
||||
// 创建目录
|
||||
Shell.cmd("mkdir -p \"" + filesDir + "\"").exec();
|
||||
|
||||
Shell.Result copyResult = Shell.cmd(
|
||||
"cp \"" + tmpGadgetConfigPath + "\" \"" + targetPath + "\"",
|
||||
"chmod 644 \"" + targetPath + "\""
|
||||
).exec();
|
||||
|
||||
if (copyResult.isSuccess()) {
|
||||
Log.i(TAG, "Gadget config copied successfully");
|
||||
|
||||
// 设置正确的所有权
|
||||
Shell.Result uidResult = Shell.cmd("stat -c %u /data/data/" + packageName).exec();
|
||||
if (uidResult.isSuccess() && !uidResult.getOut().isEmpty()) {
|
||||
String uid = uidResult.getOut().get(0).trim();
|
||||
Shell.cmd("chown " + uid + ":" + uid + " \"" + targetPath + "\"").exec();
|
||||
Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + targetPath + "\"").exec();
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Failed to copy gadget config: " + String.join("\n", copyResult.getErr()));
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Gadget config file not found at: " + tmpGadgetConfigPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是仅部署模式,或者没有提供配置文件,执行部署
|
||||
if (!deployOnly || (tmpConfigPath == null && tmpGadgetConfigPath == null)) {
|
||||
Log.i(TAG, "Deploying SO files for package: " + packageName);
|
||||
configManager.deployForPackage(packageName);
|
||||
Log.i(TAG, "Deployment completed for: " + packageName);
|
||||
} else {
|
||||
Log.i(TAG, "Config updated, skipping deployment (deploy_only=true)");
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
if (tmpConfigPath != null && !tmpConfigPath.isEmpty()) {
|
||||
Shell.cmd("rm -f \"" + tmpConfigPath + "\"").exec();
|
||||
}
|
||||
if (tmpGadgetConfigPath != null && !tmpGadgetConfigPath.isEmpty()) {
|
||||
Shell.cmd("rm -f \"" + tmpGadgetConfigPath + "\"").exec();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Config application completed successfully");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error applying config", e);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,15 @@ public class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to reload configuration from file
|
||||
* 从文件重新加载配置(用于外部更新配置后)
|
||||
*/
|
||||
public void reloadConfig() {
|
||||
loadConfig();
|
||||
Log.i(TAG, "Configuration reloaded");
|
||||
}
|
||||
|
||||
public void saveConfig() {
|
||||
String json = gson.toJson(config);
|
||||
// Write to temp file first
|
||||
@@ -651,6 +660,19 @@ public class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to deploy SO files for a specific package
|
||||
* 为指定包名部署 SO 文件(外部调用)
|
||||
* @param packageName Target package name
|
||||
*/
|
||||
public void deployForPackage(String packageName) {
|
||||
if (packageName == null || packageName.isEmpty()) {
|
||||
Log.e(TAG, "Package name cannot be null or empty");
|
||||
return;
|
||||
}
|
||||
deploySoFilesToApp(packageName);
|
||||
}
|
||||
|
||||
// Data classes
|
||||
public static class ModuleConfig {
|
||||
public boolean enabled = true;
|
||||
|
||||
Reference in New Issue
Block a user