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:
vwvw
2025-10-15 12:28:47 +08:00
parent ec89b7bf2c
commit 65e91d188f
10 changed files with 1324 additions and 2 deletions

View File

@@ -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>

View File

@@ -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");
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;