15 Commits
v1.2.0 ... main

Author SHA1 Message Date
Ji qiu
8207e728dc Merge pull request #30 from vwww-droid/feature/enhance-cli-configure
feat: 基于广播, 实现脚本指定 app 注入和测试
2025-10-25 17:33:08 +08:00
vwvw
dd67f20b8d feat: 自动化脚本增强
1. 多端口适配;
2. 允许命令行指定端口;
3. 临时配置使用系统临时目录, 使用后删除;
2025-10-20 17:42:12 +08:00
vwvw
65e91d188f feat: 基于广播, 实现脚本指定 app 注入和测试
1. 广播实现命令行交互式指定 app 打开端口并自动配置;
2. 广播动态注册, 无法枚举广播接收器, 过滤 callingUid 只接收 shell 和 root 发出的广播;
3. 自动化脚本增加 check_and_set_selinux 方法设置 selinux 为宽容模式解决部分无法启动问题;
4. frida 官方 gadget.so 自动下载和缓存 scripts/.cache/;
5. 自动重启 apk & frida 命令测试;
2025-10-15 16:34:44 +08:00
Ji qiu
ec89b7bf2c Merge pull request #29 from vwww-droid/fix/improve-meta-inf-handling
feat: Fix META-INF handling in build script
2025-10-15 15:57:50 +08:00
vwvw
cc47334970 feat: Fix META-INF handling in build script
1. 修复因 META-INF 文件在 pixel 4 android 13 狐妖magisk 27001 无法识别为 magisk module 的问题
2025-10-15 11:16:13 +08:00
Ji qiu
8a23161e0f Merge pull request #22 from jiqiu2022/fix_bug
Fix bug
2025-06-28 21:21:49 +08:00
jiqiu2021
964c975cdd feat:bugfix 2025-06-28 21:18:43 +08:00
jiqiu2021
6110216556 feat:bugfix 2025-06-28 20:25:17 +08:00
jiqiu2021
e02a3df7fc feat:bugfix 2025-06-27 21:52:51 +08:00
Ji qiu
cb64bc7d48 Merge pull request #20 from jiqiu2022/fix_bug
紧急bug修复
2025-06-27 18:26:17 +08:00
jiqiu2021
fc24ab3455 feat:自定义linker静态编译,解决报错 2025-06-27 17:47:26 +08:00
jiqiu2021
7e7b38caf6 feat:gadget全局配置 2025-06-27 17:31:01 +08:00
Ji qiu
b8b8dafed0 Update README.md 2025-06-27 17:02:08 +08:00
Ji qiu
7b7389c0a0 Update README.md 2025-06-27 16:59:57 +08:00
Ji qiu
122878f8fa Update README.md 2025-06-27 16:59:13 +08:00
20 changed files with 1777 additions and 99 deletions

9
.gitignore vendored
View File

@@ -11,4 +11,11 @@
/captures
/out
.externalNativeBuild
.cxx
.cxx
# Frida Gadget temporary downloads
tmp_gadget/
*.so
# Frida Gadget local cache
scripts/.cache/

View File

@@ -12,13 +12,15 @@
项目已完全开源包含面具模块、管理APP以及所有打包脚本并配置了GitHub CI自动构建。欢迎各位开发者贡献代码提交PR。
### 版本规划
### 版本规划&更新记录
版本规划:
- **v1.x**:专注功能添加,暂不考虑反检测
- **v2.x**实现各种检测绕过达到100%无痕注入
更新记录:
- **v1.2**: 增加gadget配置的自动生成支持脚本和server模式解决了若干bug增加了全局注入延迟设置
## 致谢
**项目地址**[https://github.com/jiqiu2022/Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
特别感谢以下项目和开发者(按时间顺序):
@@ -132,6 +134,30 @@ Riru Hide成功生效。
![CleanShot_2025_06_26_at_20_47_34](assets/CleanShot_2025_06_26_at_20_47_34-20250626204744-v9hdf53.png)
### 自动化注入
[脚本](./scripts/auto_config.py) 实现了针对 libgadget.so 的自动注入
```bash
# 1. 运行自动配置脚本(会自动检查并设置 SELinux
cd scripts
./auto_config.py
# 2. 按提示选择设备、应用和配置(全部使用默认值即可)
# 脚本会自动完成:
# - 生成配置文件
# - 推送到设备
# - 应用配置
# - 重启应用
# - 端口转发
# - 快速测试
# 3. 如果测试成功,直接使用 Frida 连接
frida -H 127.0.0.1:27042 Gadget -l your_script.js
```
见 [视频](assets/auto_config.record.mp4)
## 编译指南
### 自动编译

Binary file not shown.

View File

@@ -105,10 +105,14 @@ else
exit 1
fi
# 创建 META-INF 目录Magisk 需要)
mkdir -p $TEMP_DIR/META-INF/com/google/android
touch $TEMP_DIR/META-INF/com/google/android/update-binary
touch $TEMP_DIR/META-INF/com/google/android/updater-script
# 复制 META-INF 目录Magisk 需要)
if [ -d "template/magisk_module/META-INF" ]; then
cp -r template/magisk_module/META-INF $TEMP_DIR/
echo -e " ${GREEN}✓ 复制 META-INF${NC}"
else
echo -e " ${RED}✗ 未找到 META-INF 模板${NC}"
exit 1
fi
# 打包
echo -e "\n${YELLOW}[5/5] 打包模块...${NC}"

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

@@ -140,7 +140,11 @@ 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);
RadioGroup gadgetConfigGroup = dialogView.findViewById(R.id.gadgetConfigGroup);
RadioButton radioNoGadget = dialogView.findViewById(R.id.radioNoGadget);
RadioButton radioUseGlobalGadget = dialogView.findViewById(R.id.radioUseGlobalGadget);
RadioButton radioUseCustomGadget = dialogView.findViewById(R.id.radioUseCustomGadget);
TextView tvGlobalGadgetInfo = dialogView.findViewById(R.id.tvGlobalGadgetInfo);
com.google.android.material.button.MaterialButton btnConfigureGadget = dialogView.findViewById(R.id.btnConfigureGadget);
appIcon.setImageDrawable(appInfo.getAppIcon());
@@ -158,30 +162,71 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
}
// Load gadget config
ConfigManager.GadgetConfig gadgetConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
checkboxEnableGadget.setChecked(gadgetConfig != null);
btnConfigureGadget.setEnabled(gadgetConfig != null);
boolean useGlobalGadget = configManager.getAppUseGlobalGadget(appInfo.getPackageName());
ConfigManager.GadgetConfig appSpecificGadget = configManager.getAppGadgetConfig(appInfo.getPackageName());
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
// Setup gadget listeners
checkboxEnableGadget.setOnCheckedChangeListener((buttonView, isChecked) -> {
btnConfigureGadget.setEnabled(isChecked);
if (!isChecked) {
// Remove gadget config when unchecked
// Update global gadget info
if (globalGadget != null) {
String info = "全局: " + globalGadget.gadgetName;
if (globalGadget.mode.equals("server")) {
info += " (端口: " + globalGadget.port + ")";
}
tvGlobalGadgetInfo.setText(info);
} else {
tvGlobalGadgetInfo.setText("未配置全局Gadget");
}
// Set initial radio selection
if (!useGlobalGadget && appSpecificGadget != null) {
radioUseCustomGadget.setChecked(true);
btnConfigureGadget.setVisibility(View.VISIBLE);
btnConfigureGadget.setEnabled(true);
} else if (useGlobalGadget && globalGadget != null) {
radioUseGlobalGadget.setChecked(true);
btnConfigureGadget.setVisibility(View.GONE);
} else {
radioNoGadget.setChecked(true);
btnConfigureGadget.setVisibility(View.GONE);
}
// Setup gadget radio group listener
gadgetConfigGroup.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.radioNoGadget) {
btnConfigureGadget.setVisibility(View.GONE);
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
} else if (checkedId == R.id.radioUseGlobalGadget) {
btnConfigureGadget.setVisibility(View.GONE);
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), true);
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
} else if (checkedId == R.id.radioUseCustomGadget) {
btnConfigureGadget.setVisibility(View.VISIBLE);
btnConfigureGadget.setEnabled(true);
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
}
});
// Configure button listener
btnConfigureGadget.setOnClickListener(v -> {
ConfigManager.GadgetConfig currentConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
ConfigManager.GadgetConfig currentConfig = null;
if (!useGlobalGadget) {
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");
GadgetConfigDialog dialog = new GadgetConfigDialog(
getContext(),
"配置" + appInfo.getAppName() + "的Gadget",
currentConfig,
config -> {
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
configManager.setAppGadgetConfig(appInfo.getPackageName(), config);
}
);
dialog.show();
});
// Setup SO list
@@ -216,16 +261,6 @@ 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();

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
@@ -163,20 +172,44 @@ public class ConfigManager {
}
}
// Ensure SO storage directory exists
Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
Shell.cmd("chmod 755 " + SO_STORAGE_DIR).exec();
// Copy SO file to our storage
Log.i(TAG, "Copying SO file from: " + originalPath + " to: " + storedPath);
Shell.Result result = Shell.cmd("cp \"" + originalPath + "\" \"" + storedPath + "\"").exec();
if (result.isSuccess()) {
// Verify the file was actually copied
Shell.Result verifyResult = Shell.cmd("test -f \"" + storedPath + "\" && echo 'exists'").exec();
if (!verifyResult.isSuccess() || verifyResult.getOut().isEmpty()) {
Log.e(TAG, "File copy appeared successful but file not found at: " + storedPath);
return;
}
// Set proper permissions for SO file (readable and executable)
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + storedPath + "\"").exec();
if (!chmodResult.isSuccess()) {
Log.e(TAG, "Failed to set permissions on SO file: " + String.join("\n", chmodResult.getErr()));
}
SoFile soFile = new SoFile();
soFile.name = fileName;
soFile.storedPath = storedPath;
soFile.originalPath = originalPath;
config.globalSoFiles.add(soFile);
Log.i(TAG, "Successfully added SO file: " + fileName + " to storage");
if (deleteOriginal) {
Shell.cmd("rm \"" + originalPath + "\"").exec();
Log.i(TAG, "Deleted original file: " + originalPath);
}
saveConfig();
} else {
Log.e(TAG, "Failed to copy SO file: " + String.join("\n", result.getErr()));
}
}
@@ -265,11 +298,46 @@ public class ConfigManager {
public GadgetConfig getAppGadgetConfig(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
return null;
// If no app config, return global gadget config
return config.globalGadgetConfig;
}
// If app is set to use global gadget, return global config
if (appConfig.useGlobalGadget) {
return config.globalGadgetConfig;
}
// Otherwise return app-specific gadget config
return appConfig.gadgetConfig;
}
public GadgetConfig getGlobalGadgetConfig() {
return config.globalGadgetConfig;
}
public void setGlobalGadgetConfig(GadgetConfig gadgetConfig) {
config.globalGadgetConfig = gadgetConfig;
saveConfig();
}
public boolean getAppUseGlobalGadget(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
return true; // Default to use global
}
return appConfig.useGlobalGadget;
}
public void setAppUseGlobalGadget(String packageName, boolean useGlobal) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
appConfig = new AppConfig();
config.perAppConfig.put(packageName, appConfig);
}
appConfig.useGlobalGadget = useGlobal;
saveConfig();
}
public void setAppGadgetConfig(String packageName, GadgetConfig gadgetConfig) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
@@ -405,28 +473,36 @@ public class ConfigManager {
// 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();
Log.i(TAG, "Deploying SO files to: " + filesDir);
// Create directory without su -c for better compatibility
Shell.Result mkdirResult = Shell.cmd("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;
}
return;
}
// Set proper permissions and ownership
Shell.cmd("chmod 755 " + filesDir).exec();
// Set proper permissions and ownership for the files directory
Shell.cmd("chmod 771 " + filesDir).exec();
// Get UID for the package
// Get UID and GID 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);
// Set ownership of files directory to match app
Shell.Result chownDirResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + filesDir + "\"").exec();
if (!chownDirResult.isSuccess()) {
Log.e(TAG, "Failed to set directory ownership: " + String.join("\n", chownDirResult.getErr()));
}
// Set SELinux context for the directory
Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + filesDir + "\"").exec();
} else {
Log.e(TAG, "Failed to get package UID");
}
// Copy each SO file configured for this app
@@ -438,39 +514,66 @@ public class ConfigManager {
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);
// Log more details about the missing file
Shell.Result lsResult = Shell.cmd("ls -la \"" + SO_STORAGE_DIR + "\"").exec();
Log.e(TAG, "Contents of SO storage dir: " + String.join("\n", lsResult.getOut()));
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();
// First, ensure the destination directory exists and has proper permissions
Shell.cmd("mkdir -p \"" + filesDir + "\"").exec();
Shell.cmd("chmod 755 \"" + filesDir + "\"").exec();
// Copy file using cp with force flag
Shell.Result result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
if (!result.isSuccess()) {
Log.e(TAG, "Failed with cat, trying cp");
// Fallback to cp
result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
Log.e(TAG, "Failed with cp, trying cat method");
Log.e(TAG, "cp error: " + String.join("\n", result.getErr()));
// Fallback to cat method
result = Shell.cmd("cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"").exec();
if (!result.isSuccess()) {
Log.e(TAG, "Also failed with cat method");
Log.e(TAG, "cat error: " + String.join("\n", result.getErr()));
}
}
// Set permissions
Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
// Set permissions - SO files need to be readable and executable
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
if (!chmodResult.isSuccess()) {
Log.e(TAG, "Failed to set permissions: " + String.join("\n", chmodResult.getErr()));
}
// Set ownership if we have the UID
// Set ownership to match the app's UID
if (!uid.isEmpty()) {
Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
Shell.Result chownResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
if (!chownResult.isSuccess()) {
Log.e(TAG, "Failed to set ownership: " + String.join("\n", chownResult.getErr()));
// Try alternative method
Shell.cmd("chown " + uid + ".app_" + uid + " \"" + destPath + "\"").exec();
}
}
// Verify the file was copied
Shell.Result verifyResult = Shell.cmd("ls -la \"" + destPath + "\" 2>/dev/null").exec();
// Set SELinux context to match app's data files
Shell.Result contextResult = Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + destPath + "\"").exec();
if (!contextResult.isSuccess()) {
Log.w(TAG, "Failed to set SELinux context (this may be normal on some devices)");
}
// Verify the file was copied with correct permissions
Shell.Result verifyResult = Shell.cmd("ls -laZ \"" + 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");
// Fallback verification without SELinux context
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);
}
}
}
@@ -478,8 +581,9 @@ public class ConfigManager {
Log.i(TAG, "Deployment complete for: " + packageName);
// Deploy gadget config if configured
if (appConfig.gadgetConfig != null) {
deployGadgetConfigFile(packageName, appConfig.gadgetConfig);
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
if (gadgetToUse != null) {
deployGadgetConfigFile(packageName, gadgetToUse);
}
}
@@ -527,8 +631,9 @@ public class ConfigManager {
}
// Clean up gadget config file if exists
if (appConfig.gadgetConfig != null) {
String gadgetConfigName = appConfig.gadgetConfig.gadgetName.replace(".so", ".config.so");
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
if (gadgetToUse != null) {
String gadgetConfigName = gadgetToUse.gadgetName.replace(".so", ".config.so");
String configPath = filesDir + "/" + gadgetConfigName;
Shell.Result checkConfigResult = Shell.cmd("test -f \"" + configPath + "\" && echo 'exists'").exec();
@@ -555,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;
@@ -562,6 +680,7 @@ public class ConfigManager {
public int injectionDelay = 2; // Default 2 seconds
public List<SoFile> globalSoFiles = new ArrayList<>();
public Map<String, AppConfig> perAppConfig = new HashMap<>();
public GadgetConfig globalGadgetConfig = null; // Global gadget configuration
}
public static class AppConfig {
@@ -569,6 +688,7 @@ public class ConfigManager {
public List<SoFile> soFiles = new ArrayList<>();
public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker"
public GadgetConfig gadgetConfig = null;
public boolean useGlobalGadget = true; // Whether to use global gadget settings
}
public static class SoFile {

View File

@@ -115,18 +115,19 @@ public class FileUtils {
// Make file readable
tempFile.setReadable(true, false);
// Use root to copy to a more permanent location
String targetPath = "/data/local/tmp/" + fileName;
// First copy to /data/local/tmp as a temporary location
String tempTargetPath = "/data/local/tmp/" + fileName;
Shell.Result result = Shell.cmd(
"cp \"" + tempFile.getAbsolutePath() + "\" \"" + targetPath + "\"",
"chmod 644 \"" + targetPath + "\""
"cp \"" + tempFile.getAbsolutePath() + "\" \"" + tempTargetPath + "\"",
"chmod 644 \"" + tempTargetPath + "\""
).exec();
// Clean up temp file
tempFile.delete();
if (result.isSuccess()) {
return targetPath;
// Return the temporary path - it will be moved to the proper location by addGlobalSoFile
return tempTargetPath;
} else {
Log.e(TAG, "Failed to copy file to /data/local/tmp/");
return null;

View File

@@ -57,6 +57,7 @@ public class GadgetConfigDialog extends DialogFragment {
// Configuration data
private ConfigManager.GadgetConfig config;
private OnGadgetConfigListener listener;
private String customTitle;
// Flag to prevent recursive updates
private boolean isUpdatingUI = false;
@@ -75,6 +76,21 @@ public class GadgetConfigDialog extends DialogFragment {
return dialog;
}
// Constructor for non-fragment usage
public GadgetConfigDialog(Context context, String title, ConfigManager.GadgetConfig config, OnGadgetConfigListener listener) {
// This constructor is for compatibility with direct dialog creation
// The actual dialog will be created in show() method
this.savedContext = context;
this.customTitle = title;
this.config = config != null ? config : new ConfigManager.GadgetConfig();
this.listener = listener;
}
// Default constructor required for DialogFragment
public GadgetConfigDialog() {
// Empty constructor required
}
public void setOnGadgetConfigListener(OnGadgetConfigListener listener) {
this.listener = listener;
}
@@ -129,8 +145,10 @@ public class GadgetConfigDialog extends DialogFragment {
setupListeners();
updateJsonPreview();
String title = customTitle != null ? customTitle : "Gadget 配置";
return new MaterialAlertDialogBuilder(getContext())
.setTitle("Gadget 配置")
.setTitle(title)
.setView(view)
.setPositiveButton("保存", (dialog, which) -> saveConfig())
.setNegativeButton("取消", null)
@@ -567,6 +585,50 @@ public class GadgetConfigDialog extends DialogFragment {
.show();
}
// Show method for non-fragment usage
public void show() {
if (getContext() == null) {
throw new IllegalStateException("Context is required for non-fragment usage");
}
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_gadget_config, null);
initViews(view);
// Initialize config if null
if (config == null) {
config = new ConfigManager.GadgetConfig();
}
loadConfig();
setupListeners();
updateJsonPreview();
String title = customTitle != null ? customTitle : "Gadget 配置";
new MaterialAlertDialogBuilder(getContext())
.setTitle(title)
.setView(view)
.setPositiveButton("保存", (dialog, which) -> saveConfig())
.setNegativeButton("取消", null)
.show();
}
private Context savedContext;
@Override
public Context getContext() {
Context context = super.getContext();
if (context == null) {
return savedContext;
}
return context;
}
// Constructor for non-fragment usage needs to save context
public void setContext(Context context) {
this.savedContext = context;
}
private String getPathFromUri(Uri uri) {
String path = null;

View File

@@ -11,6 +11,8 @@ import android.widget.RadioGroup;
import android.widget.EditText;
import android.text.TextWatcher;
import android.text.Editable;
import android.widget.TextView;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -28,6 +30,8 @@ public class SettingsFragment extends Fragment {
private RadioButton radioShowAll;
private RadioButton radioHideSystem;
private EditText editInjectionDelay;
private TextView tvGlobalGadgetStatus;
private Button btnConfigureGlobalGadget;
private ConfigManager configManager;
private SharedPreferences sharedPreferences;
@@ -59,6 +63,8 @@ public class SettingsFragment extends Fragment {
radioShowAll = view.findViewById(R.id.radio_show_all);
radioHideSystem = view.findViewById(R.id.radio_hide_system);
editInjectionDelay = view.findViewById(R.id.editInjectionDelay);
tvGlobalGadgetStatus = view.findViewById(R.id.tvGlobalGadgetStatus);
btnConfigureGlobalGadget = view.findViewById(R.id.btnConfigureGlobalGadget);
configManager = new ConfigManager(getContext());
}
@@ -79,6 +85,9 @@ public class SettingsFragment extends Fragment {
// Load injection delay
int injectionDelay = configManager.getInjectionDelay();
editInjectionDelay.setText(String.valueOf(injectionDelay));
// Load global gadget status
updateGlobalGadgetStatus();
}
private void setupListeners() {
@@ -124,6 +133,11 @@ public class SettingsFragment extends Fragment {
}
}
});
// Global gadget configuration button
btnConfigureGlobalGadget.setOnClickListener(v -> {
showGlobalGadgetConfigDialog();
});
}
public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
@@ -133,4 +147,34 @@ public class SettingsFragment extends Fragment {
public boolean isHideSystemApps() {
return sharedPreferences.getBoolean(KEY_HIDE_SYSTEM_APPS, false);
}
private void updateGlobalGadgetStatus() {
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
if (globalGadget != null) {
String status = "已配置: " + globalGadget.gadgetName;
if (globalGadget.mode.equals("server")) {
status += " (Server模式, 端口: " + globalGadget.port + ")";
} else {
status += " (Script模式)";
}
tvGlobalGadgetStatus.setText(status);
} else {
tvGlobalGadgetStatus.setText("未配置");
}
}
private void showGlobalGadgetConfigDialog() {
// Use existing GadgetConfigDialog
GadgetConfigDialog dialog = new GadgetConfigDialog(
getContext(),
"全局Gadget配置",
configManager.getGlobalGadgetConfig(),
gadgetConfig -> {
// Save global gadget configuration
configManager.setGlobalGadgetConfig(gadgetConfig);
updateGlobalGadgetStatus();
}
);
dialog.show();
}
}

View File

@@ -88,35 +88,67 @@
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<LinearLayout
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gadget 配置"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<RadioGroup
android:id="@+id/gadgetConfigGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_marginBottom="8dp">
<CheckBox
android:id="@+id/checkboxEnableGadget"
<RadioButton
android:id="@+id/radioNoGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="不使用Gadget"
android:checked="true" />
<RadioButton
android:id="@+id/radioUseGlobalGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="使用全局Gadget配置" />
<TextView
android:id="@+id/tvGlobalGadgetInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="启用 Gadget 配置"
android:layout_marginEnd="8dp" />
android:layout_marginStart="32dp"
android:layout_marginBottom="4dp"
android:text="未配置全局Gadget"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<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" />
<RadioButton
android:id="@+id/radioUseCustomGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="自定义Gadget配置" />
</LinearLayout>
</RadioGroup>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfigureGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="配置Gadget"
android:textSize="14sp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:enabled="false"
android:visibility="gone"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Gadget可用于Frida调试勾选后可配置监听地址和端口"
android:text="Gadget可用于Frida调试可配置监听地址和端口"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="16dp" />

View File

@@ -143,6 +143,53 @@
</com.google.android.material.card.MaterialCardView>
<!-- 全局Gadget配置 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="全局Gadget配置"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="配置全局默认的Gadget设置应用可以选择使用或覆盖"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/tvGlobalGadgetStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未配置"
android:textSize="14sp"
android:layout_marginBottom="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfigureGlobalGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="配置全局Gadget"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 其他设置可以在这里添加 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"

View File

@@ -88,12 +88,8 @@ void load_so_file_custom_linker(const char *game_data_dir, const Config::SoFile
void hack_thread_func(const char *game_data_dir, const char *package_name, JavaVM *vm) {
LOGI("Hack thread started for package: %s", package_name);
// Get injection delay from config
int delay = Config::getInjectionDelay();
LOGI("Waiting %d seconds before injection", delay);
// Wait for app to initialize and files to be copied
sleep(delay);
// Note: Delay is now handled in main thread before this thread is created
LOGI("Starting injection immediately (delay already applied in main thread)");
// Get injection method for this app
Config::InjectionMethod method = Config::getAppInjectionMethod(package_name);
@@ -135,5 +131,5 @@ void hack_prepare(const char *game_data_dir, const char *package_name, void *dat
LOGI("hack_prepare called for package: %s, dir: %s", package_name, game_data_dir);
std::thread hack_thread(hack_thread_func, game_data_dir, package_name, vm);
hack_thread.detach();
hack_thread.join();
}

View File

@@ -45,6 +45,13 @@ public:
// Get JavaVM
JavaVM *vm = nullptr;
if (env->GetJavaVM(&vm) == JNI_OK) {
// Get injection delay from config
int delay = Config::getInjectionDelay();
LOGI("Main thread blocking for %d seconds before injection", delay);
// Block main thread for the delay period
sleep(delay);
// Then start hack thread with JavaVM
std::thread hack_thread(hack_prepare, _data_dir, _package_name, data, length, vm);
hack_thread.detach();

View File

@@ -17,8 +17,8 @@ set(SOURCES
find_library(log-lib log)
# Build as shared library
add_library(mylinker SHARED ${SOURCES})
# Build as static library to be linked into main module
add_library(mylinker STATIC ${SOURCES})
target_link_libraries(mylinker ${log-lib})

4
scripts/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Temporary config files
.tmp/
*.pyc
__pycache__/

246
scripts/README.md Normal file
View File

@@ -0,0 +1,246 @@
# Auto Config Tool for Zygisk-MyInjector
自动配置工具,通过交互式命令行快速配置应用注入。
## 功能特性
- 🎯 交互式设备选择(支持多设备)
- 🔍 Tab 自动补全选择应用包名(支持模糊搜索)
- ⚙️ 交互式 Gadget 配置Server/Script 模式)
- 📦 自动下载 Frida Gadget 16.0.7(自动检测设备架构)
- 📦 自动生成配置文件
- 🚀 一键推送并应用配置
## 安装依赖
```bash
pip install prompt_toolkit
```
## 使用方法
### 基本用法
```bash
./auto_config.py
```
### 工作流程
1. **设备选择**
- 自动检测连接的设备
- 单设备时自动选择
- 多设备时交互式选择
2. **应用选择**
- 可选是否包含系统应用(默认仅第三方应用)
- 使用 Tab 键自动补全包名
- 支持模糊搜索过滤
3. **Gadget 配置**
- 指定 Gadget 名称(默认 libgadget.so
- **Server 模式**:监听端口等待 Frida 连接
- 监听地址(默认 0.0.0.0
- 监听端口(默认 27042
- 端口冲突处理fail/ignore/close默认 fail
- 加载行为resume/wait默认 resume
- **Script 模式**:执行本地脚本
- 脚本路径(默认 /data/local/tmp/script.js
4. **Frida Gadget 检查与安装**
- 自动检查设备上是否已安装 Gadget
- 如未安装,自动下载 Frida Gadget 16.0.7
- 根据设备架构选择正确版本arm64/arm/x86/x86_64
- 自动解压并安装到模块 SO 库
5. **配置部署**
- 自动生成 config.json 和 gadget 配置文件
- 推送到设备 /data/local/tmp
- 发送广播触发应用配置
## 示例会话
```
============================================================
Zygisk-MyInjector Auto Config Tool
============================================================
Using device: 192.168.1.100:5555 (OnePlus)
=== Loading app packages ===
Include system apps? (y/N): n
Found 156 packages
=== Select Target App ===
Tip: Use Tab for auto-completion, type to filter
Package name: com.example.app
Selected: com.example.app
=== Gadget Configuration ===
Gadget SO name (default: libgadget.so):
libgadget.so
Select mode:
1. Server mode (listen for connections)
2. Script mode (execute script)
Mode (1/2, default: 1): 1
Listen address (default: 0.0.0.0):
Listen port (default: 27042):
Port conflict behavior:
1. fail - Exit if port is in use
2. ignore - Continue anyway
3. close - Close existing connection
On port conflict (1/2/3, default: 1): 1
On load behavior:
1. resume - Continue immediately (recommended)
2. wait - Wait for connection (for debugging)
On load (1/2, default: 1): 1
=== Generating Configuration Files ===
...
✓ Configuration applied successfully!
The app 'com.example.app' has been configured.
You can now use Frida to connect to the app:
frida -H 0.0.0.0:27042 -n <process-name>
```
## 生成的文件
脚本会生成以下文件:
1. **config.json**
- 模块主配置文件
- 存储位置:`/data/adb/modules/zygisk-myinjector/config.json`
2. **gadget 配置文件**
- 格式:`libgadget.config.so`
- 存储位置:`/data/data/<package>/files/libgadget.config.so`
## 广播接收器
配置通过广播接收器应用:
**注意**ConfigApplyReceiver 现在使用**动态注册**方式,第三方 app 无法通过 PackageManager 发现其存在。
同时增加了 UID 权限检查,只允许 shell (2000) 或 root (0) 发送广播。
```bash
# 手动发送广播示例
adb shell am broadcast \
-n com.jiqiu.configapp/.ConfigApplyReceiver \
-a com.jiqiu.configapp.APPLY_CONFIG \
--es package_name "com.example.app" \
--es tmp_config_path "/data/local/tmp/zygisk_config.json" \
--es tmp_gadget_config_path "/data/local/tmp/libgadget.config.so"
```
## 调试
查看日志:
```bash
adb logcat -s ConfigApplyReceiver:* ConfigManager:*
```
## 故障排除
### prompt_toolkit 未安装
```
Error: prompt_toolkit is required. Install it with:
pip install prompt_toolkit
```
**解决方案**:运行 `pip install prompt_toolkit`
### 没有设备连接
```
Error: No devices found. Please connect a device and enable USB debugging.
```
**解决方案**
1. 通过 USB 或 WiFi 连接设备
2. 确保已启用 USB 调试
3. 运行 `adb devices` 确认设备已连接
### 广播发送失败
**解决方案**
1. 确保 configapp 已安装
2. 确保设备已 root
3. 检查 logcat 日志获取详细错误信息
## 高级用法
### 仅生成配置不部署
修改广播命令添加 `deploy_only` 参数:
```bash
adb shell am broadcast \
-n com.jiqiu.configapp/.ConfigApplyReceiver \
-a com.jiqiu.configapp.APPLY_CONFIG \
--es package_name "com.example.app" \
--es tmp_config_path "/data/local/tmp/zygisk_config.json" \
--ez deploy_only true
```
## 注意事项
1. ⚠️ 设备必须已 root
2. ⚠️ configapp 必须已安装
3.**SELinux 会自动检查并设置为 Permissive 模式**(脚本会自动处理)
4. ⚠️ 首次使用会自动下载 Frida Gadget 16.0.7(需要网络连接)
5. ⚠️ 配置完成后需要重启目标应用才能生效
6. ⚠️ 需要安装 `xz` 工具用于解压macOS 通过 `brew install xz` 安装)
### SELinux 问题
Zygisk 模块需要读取 `/data/adb/modules/zygisk-myinjector/config.json`,但 SELinux 默认会阻止 zygote 进程访问。
**自动处理**
`auto_config.py` 脚本会自动检查 SELinux 状态,如果处于 Enforcing 模式会提示设置为 Permissive。
**手动设置**(重启后需重新设置):
```bash
adb shell "su -c 'setenforce 0'"
```
**永久解决方案**(需要 Magisk 模块):
创建 SELinux 策略或修改模块实现方式。
## 完整工作流程
```bash
# 1. 运行自动配置脚本(会自动检查并设置 SELinux
cd scripts
./auto_config.py
# 2. 按提示选择设备、应用和配置(全部使用默认值即可)
# 脚本会自动完成:
# - 生成配置文件
# - 推送到设备
# - 应用配置
# - 重启应用
# - 端口转发
# - 快速测试
# 3. 如果测试成功,直接使用 Frida 连接
frida -H 127.0.0.1:27042 Gadget -l your_script.js
```
### 快速测试示例
```bash
# 测试连接
frida -H 127.0.0.1:27042 Gadget -e "console.log('Connected! PID:', Process.id)"
# 列举模块
frida -H 127.0.0.1:27042 Gadget -e "Process.enumerateModules().forEach(m => console.log(m.name))"
```

841
scripts/auto_config.py Executable file
View File

@@ -0,0 +1,841 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Auto Config Script for Zygisk-MyInjector
通过交互式命令行快速配置应用注入
完整工作流程:
1. 运行自动配置脚本(会自动检查并设置 SELinux
cd scripts
./auto_config.py
2. 按提示选择设备、应用和配置(全部使用默认值即可)
脚本会自动完成:
- 生成配置文件
- 推送到设备
- 应用配置
- 重启应用
- 端口转发
- 快速测试
3. 如果测试成功,直接使用 Frida 连接
frida -H 127.0.0.1:27042 Gadget -l your_script.js
快速测试:
# 测试连接
frida -H 127.0.0.1:27042 Gadget -e "console.log('Connected! PID:', Process.id)"
# 列举模块
frida -H 127.0.0.1:27042 Gadget -e "Process.enumerateModules().forEach(m => console.log(m.name))"
"""
import subprocess
import json
import sys
import os
import tempfile
import shutil
import argparse
from typing import List, Dict, Optional
from pathlib import Path
try:
from prompt_toolkit import prompt
from prompt_toolkit.completion import FuzzyWordCompleter
from prompt_toolkit.styles import Style
except ImportError:
print("Error: prompt_toolkit is required. Install it with:")
print(" pip install prompt_toolkit")
sys.exit(1)
# Define style for prompts
style = Style.from_dict({
'prompt': 'ansicyan bold',
})
# Frida Gadget version
FRIDA_VERSION = "16.0.7"
MODULE_PATH = "/data/adb/modules/zygisk-myinjector"
SO_STORAGE_DIR = f"{MODULE_PATH}/so_files"
# Default ports
DEFAULT_PORTS = [27042, 65320]
# Local cache directory for downloaded gadgets
SCRIPT_DIR = Path(__file__).parent
CACHE_DIR = SCRIPT_DIR / '.cache' / 'frida-gadgets'
class ADBHelper:
"""ADB helper class for device and package operations"""
def __init__(self, device_id: Optional[str] = None):
self.device_id = device_id
self.base_cmd = ['adb']
if device_id:
self.base_cmd.extend(['-s', device_id])
def run(self, args: List[str], check=True) -> subprocess.CompletedProcess:
"""Run adb command"""
cmd = self.base_cmd + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=check)
return result
except subprocess.CalledProcessError as e:
print(f"Error running command: {' '.join(cmd)}")
print(f"Error: {e.stderr}")
if check:
raise
return e
@staticmethod
def get_devices() -> List[Dict[str, str]]:
"""Get list of connected devices"""
result = subprocess.run(['adb', 'devices', '-l'], capture_output=True, text=True)
lines = result.stdout.strip().split('\n')[1:] # Skip header
devices = []
for line in lines:
if not line.strip():
continue
parts = line.split()
if len(parts) >= 2:
device_id = parts[0]
status = parts[1]
# Parse device info
model = ''
product = ''
for part in parts[2:]:
if part.startswith('model:'):
model = part.split(':', 1)[1]
elif part.startswith('product:'):
product = part.split(':', 1)[1]
devices.append({
'id': device_id,
'status': status,
'model': model,
'product': product
})
return devices
def get_packages(self, include_system: bool = False) -> List[str]:
"""Get list of installed packages"""
args = ['shell', 'pm', 'list', 'packages']
if not include_system:
args.append('-3') # Third-party apps only
result = self.run(args)
packages = []
for line in result.stdout.strip().split('\n'):
if line.startswith('package:'):
pkg = line.split(':', 1)[1].strip()
packages.append(pkg)
return sorted(packages)
def push_file(self, local_path: str, remote_path: str) -> bool:
"""Push file to device"""
result = self.run(['push', local_path, remote_path], check=False)
return result.returncode == 0
def send_broadcast(self, action: str, component: str, extras: Dict[str, str]) -> bool:
"""Send broadcast with extras"""
args = ['shell', 'am', 'broadcast', '-n', component, '-a', action]
for key, value in extras.items():
args.extend(['--es', key, value])
result = self.run(args, check=False)
if result.returncode == 0:
print(f"Broadcast sent successfully")
print(result.stdout)
return True
else:
print(f"Failed to send broadcast")
print(result.stderr)
return False
def get_arch(self) -> str:
"""Get device CPU architecture"""
result = self.run(['shell', 'getprop', 'ro.product.cpu.abi'])
arch = result.stdout.strip()
return arch
def file_exists(self, path: str) -> bool:
"""Check if file exists on device"""
result = self.run(['shell', f'su -c "test -f {path} && echo exists"'], check=False)
return 'exists' in result.stdout
def select_device() -> Optional[str]:
"""Interactive device selection"""
devices = ADBHelper.get_devices()
if not devices:
print("Error: No devices found. Please connect a device and enable USB debugging.")
return None
if len(devices) == 1:
device = devices[0]
print(f"Using device: {device['id']} ({device['model'] or device['product']})")
return device['id']
print("\n=== Connected Devices ===")
for idx, device in enumerate(devices, 1):
model_info = device['model'] or device['product'] or 'Unknown'
print(f"{idx}. {device['id']} - {model_info} [{device['status']}]")
while True:
try:
choice = input(f"\nSelect device (1-{len(devices)}): ").strip()
idx = int(choice) - 1
if 0 <= idx < len(devices):
selected = devices[idx]
print(f"Selected: {selected['id']} ({selected['model'] or selected['product']})")
return selected['id']
else:
print(f"Invalid choice. Please enter 1-{len(devices)}")
except (ValueError, KeyboardInterrupt):
print("\nDevice selection cancelled")
return None
def select_package(adb: ADBHelper) -> Optional[str]:
"""Interactive package selection with fuzzy completion"""
print("\n=== Loading app packages ===")
# Ask if include system apps
while True:
choice = input("Include system apps? (y/N): ").strip().lower()
if choice in ['', 'n', 'no']:
include_system = False
break
elif choice in ['y', 'yes']:
include_system = True
break
else:
print("Please enter 'y' or 'n'")
packages = adb.get_packages(include_system=include_system)
if not packages:
print("Error: No packages found")
return None
print(f"Found {len(packages)} packages")
# Create fuzzy completer
completer = FuzzyWordCompleter(packages)
print("\n=== Select Target App ===")
print("Tip: Use Tab for auto-completion, type to filter")
try:
package = prompt(
'Package name: ',
completer=completer,
style=style
).strip()
if package and package in packages:
print(f"Selected: {package}")
return package
elif package:
print(f"Warning: '{package}' not found in package list, using anyway")
return package
else:
print("No package selected")
return None
except KeyboardInterrupt:
print("\nSelection cancelled")
return None
def configure_gadget(preset_port: Optional[int] = None) -> Optional[Dict]:
"""Interactive gadget configuration"""
print("\n=== Gadget Configuration ===")
gadget_config = {}
# Gadget name
gadget_name = input("Gadget SO name (default: libgadget.so): ").strip()
gadget_config['gadgetName'] = gadget_name or 'libgadget.so'
# Mode selection
print("\nSelect mode:")
print("1. Server mode (listen for connections)")
print("2. Script mode (execute script)")
while True:
choice = input("Mode (1/2, default: 1): ").strip()
if choice in ['', '1']:
gadget_config['mode'] = 'server'
break
elif choice == '2':
gadget_config['mode'] = 'script'
break
else:
print("Invalid choice. Please enter 1 or 2")
if gadget_config['mode'] == 'server':
# Server mode configuration
address = input("Listen address (default: 0.0.0.0): ").strip()
gadget_config['address'] = address or '0.0.0.0'
# Use preset port if provided
if preset_port:
gadget_config['port'] = preset_port
print(f"\nUsing preset port: {preset_port}")
else:
print(f"\nAvailable ports: {', '.join(map(str, DEFAULT_PORTS))}")
port = input(f"Listen port (default: {DEFAULT_PORTS[0]}): ").strip()
try:
gadget_config['port'] = int(port) if port else DEFAULT_PORTS[0]
except ValueError:
print(f"Invalid port, using default {DEFAULT_PORTS[0]}")
gadget_config['port'] = DEFAULT_PORTS[0]
print("\nPort conflict behavior:")
print("1. fail - Exit if port is in use")
print("2. ignore - Continue anyway")
print("3. close - Close existing connection")
conflict = input("On port conflict (1/2/3, default: 1): ").strip()
conflict_map = {'1': 'fail', '2': 'ignore', '3': 'close', '': 'fail'}
gadget_config['onPortConflict'] = conflict_map.get(conflict, 'fail')
print("\nOn load behavior:")
print("1. resume - Continue immediately (recommended)")
print("2. wait - Wait for connection (for debugging)")
load = input("On load (1/2, default: 1): ").strip()
load_map = {'1': 'resume', '2': 'wait', '': 'resume'}
gadget_config['onLoad'] = load_map.get(load, 'resume')
else:
# Script mode configuration
script_path = input("Script path (default: /data/local/tmp/script.js): ").strip()
gadget_config['scriptPath'] = script_path or '/data/local/tmp/script.js'
return gadget_config
def download_frida_gadget(arch: str) -> Optional[str]:
"""Download Frida Gadget for specified architecture (with local caching)"""
# Map Android arch to Frida naming
arch_map = {
'arm64-v8a': 'arm64',
'armeabi-v7a': 'arm',
'x86': 'x86',
'x86_64': 'x86_64'
}
frida_arch = arch_map.get(arch)
if not frida_arch:
print(f"Unsupported architecture: {arch}")
return None
# Check local cache first
CACHE_DIR.mkdir(parents=True, exist_ok=True)
cached_file = CACHE_DIR / f"frida-gadget-{FRIDA_VERSION}-android-{frida_arch}.so"
if cached_file.exists():
print(f"\n✓ Using cached Frida Gadget {FRIDA_VERSION} for {arch}")
print(f" Cache: {cached_file}")
# Copy to temp location for consistent behavior
temp_dir = tempfile.mkdtemp(prefix='frida_gadget_')
temp_file = os.path.join(temp_dir, 'libgadget.so')
shutil.copy2(str(cached_file), temp_file)
return temp_file
# Download if not in cache
url = f"https://github.com/frida/frida/releases/download/{FRIDA_VERSION}/frida-gadget-{FRIDA_VERSION}-android-{frida_arch}.so.xz"
print(f"\nDownloading Frida Gadget {FRIDA_VERSION} for {arch}...")
print(f"URL: {url}")
# Create temp directory
temp_dir = tempfile.mkdtemp(prefix='frida_gadget_')
xz_file = os.path.join(temp_dir, f'frida-gadget.so.xz')
so_file = os.path.join(temp_dir, 'libgadget.so')
try:
# Download
result = subprocess.run(['curl', '-L', '-o', xz_file, url],
capture_output=True, text=True, check=False)
if result.returncode != 0:
print(f"Failed to download: {result.stderr}")
shutil.rmtree(temp_dir)
return None
print("✓ Downloaded")
# Decompress
print("Decompressing...")
result = subprocess.run(['xz', '-d', xz_file],
capture_output=True, text=True, check=False)
if result.returncode != 0:
print(f"Failed to decompress: {result.stderr}")
shutil.rmtree(temp_dir)
return None
# Rename
decompressed = xz_file.replace('.xz', '')
os.rename(decompressed, so_file)
print("✓ Decompressed")
# Save to cache for future use
try:
shutil.copy2(so_file, str(cached_file))
print(f"✓ Cached for future use: {cached_file}")
except Exception as e:
print(f"Warning: Failed to cache gadget: {e}")
return so_file
except Exception as e:
print(f"Error downloading Frida Gadget: {e}")
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
return None
def ensure_gadget_installed(adb: ADBHelper, gadget_name: str = 'libgadget.so') -> bool:
"""Ensure Frida Gadget is installed on device"""
gadget_path = f"{SO_STORAGE_DIR}/{gadget_name}"
print(f"\n=== Checking Frida Gadget ===")
# Check if gadget already exists
if adb.file_exists(gadget_path):
print(f"✓ Gadget found: {gadget_path}")
return True
print(f"Gadget not found in: {gadget_path}")
print("Need to download and install Frida Gadget")
# Get device architecture
arch = adb.get_arch()
print(f"Device architecture: {arch}")
# Download gadget
local_gadget = download_frida_gadget(arch)
if not local_gadget:
print("Failed to download Frida Gadget")
return False
try:
# Push to device temp location
print("\nPushing to device...")
if not adb.push_file(local_gadget, '/data/local/tmp/libgadget.so'):
print("Failed to push gadget to device")
return False
print("✓ Pushed to device")
# Copy to SO storage with root
print(f"Installing to {gadget_path}...")
result = adb.run(['shell', f'su -c "mkdir -p {SO_STORAGE_DIR}"'], check=False)
result = adb.run(['shell',
f'su -c "cp /data/local/tmp/libgadget.so {gadget_path}"'],
check=False)
if result.returncode != 0:
print(f"Failed to install gadget: {result.stderr}")
return False
# Set permissions
adb.run(['shell', f'su -c "chmod 755 {gadget_path}"'], check=False)
# Verify
if adb.file_exists(gadget_path):
print(f"✓ Gadget installed successfully: {gadget_path}")
# Clean up temp file on device
adb.run(['shell', 'rm -f /data/local/tmp/libgadget.so'], check=False)
return True
else:
print("Failed to verify gadget installation")
return False
finally:
# Clean up local temp file
temp_dir = os.path.dirname(local_gadget)
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
def generate_config_files(package_name: str, gadget_config: Dict) -> tuple:
"""Generate config.json and gadget config content"""
# Prepare SO file reference for gadget
gadget_name = gadget_config['gadgetName']
gadget_so_ref = {
"name": gadget_name,
"storedPath": f"{SO_STORAGE_DIR}/{gadget_name}",
"originalPath": f"{SO_STORAGE_DIR}/{gadget_name}"
}
# Generate main module config.json
module_config = {
"enabled": True,
"hideInjection": False,
"injectionDelay": 2,
"globalSoFiles": [],
"perAppConfig": {
package_name: {
"enabled": True,
"soFiles": [gadget_so_ref], # Include gadget SO file
"injectionMethod": "standard",
"gadgetConfig": gadget_config,
"useGlobalGadget": False
}
},
"globalGadgetConfig": None
}
# Generate gadget config content based on mode
if gadget_config['mode'] == 'server':
gadget_config_content = {
"interaction": {
"type": "listen",
"address": gadget_config['address'],
"port": gadget_config['port'],
"on_port_conflict": gadget_config['onPortConflict'],
"on_load": gadget_config['onLoad']
}
}
else: # script mode
gadget_config_content = {
"interaction": {
"type": "script",
"path": gadget_config['scriptPath']
}
}
return (
json.dumps(module_config, indent=2, ensure_ascii=False),
json.dumps(gadget_config_content, indent=2, ensure_ascii=False)
)
def quick_test(adb: ADBHelper, port: int = 27042):
"""Quick test connectivity after configuration"""
print("\n=== Quick Test ===")
print("Testing Frida connectivity...\n")
# Check if frida is installed
result = subprocess.run(['which', 'frida'], capture_output=True, text=True)
if result.returncode != 0:
print("⚠️ Frida CLI not found. Please install it with:")
print(" pip install frida-tools")
return False
# Test 1: Basic connection test
print("Test 1: Basic connection...")
# 使用 stdin 输入命令和 exit避免交互式 REPL 导致超时
test_input = "console.log('Connected! PID:', Process.id)\nexit\n"
test_cmd = ['frida', '-H', f'127.0.0.1:{port}', 'Gadget']
result = subprocess.run(test_cmd, input=test_input, capture_output=True, text=True, timeout=10)
if result.returncode == 0 and 'Connected!' in result.stdout:
print("✓ Connection successful!")
# 提取并显示 PID
for line in result.stdout.split('\n'):
if 'Connected! PID:' in line:
print(f" {line.strip()}")
break
else:
print("✗ Connection failed")
print(f" Error: {result.stderr.strip() if result.stderr else 'Unknown error'}")
print("\nTroubleshooting:")
print(" 1. Check if the target app is running")
print(" 2. Verify port forwarding: adb forward tcp:{} tcp:{}".format(port, port))
print(" 3. Check logcat for errors: adb logcat -s Gadget:*")
return False
# Test 2: Enumerate modules
print("\nTest 2: Enumerate loaded modules...")
test_input = "Process.enumerateModules().slice(0, 5).forEach(m => console.log(' -', m.name))\nexit\n"
test_cmd = ['frida', '-H', f'127.0.0.1:{port}', 'Gadget']
result = subprocess.run(test_cmd, input=test_input, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
print("✓ Modules enumerated:")
# 提取并显示模块列表
in_output = False
for line in result.stdout.split('\n'):
if ' -' in line:
in_output = True
print(line)
elif in_output and line.strip() == '':
break
else:
print("✗ Failed to enumerate modules")
print(f" Error: {result.stderr.strip()}")
return True
def setup_port_forward(adb: ADBHelper, port: int) -> bool:
"""Setup ADB port forwarding"""
print(f"\n=== Setting up port forwarding ===")
print(f"Forwarding local port {port} to device port {port}...")
result = adb.run(['forward', f'tcp:{port}', f'tcp:{port}'], check=False)
if result.returncode == 0:
print(f"✓ Port forwarding established: tcp:{port} -> tcp:{port}")
return True
else:
print(f"✗ Failed to setup port forwarding")
print(f" Error: {result.stderr}")
return False
def check_and_set_selinux(adb: ADBHelper) -> bool:
"""Check SELinux status and set to Permissive if needed"""
print("\n=== Checking SELinux Status ===")
# Check current SELinux status
result = adb.run(['shell', 'getenforce'], check=False)
if result.returncode != 0:
print("⚠️ Failed to check SELinux status")
return True # Continue anyway
status = result.stdout.strip()
print(f"Current SELinux mode: {status}")
if status == 'Permissive':
print("✓ SELinux is already in Permissive mode")
return True
elif status == 'Enforcing':
print("\n⚠️ SELinux is in Enforcing mode")
print(" Zygisk modules cannot read config files when SELinux is Enforcing.")
print(" We need to set it to Permissive mode.")
# Ask user for confirmation
while True:
choice = input("\nSet SELinux to Permissive? (Y/n): ").strip().lower()
if choice in ['', 'y', 'yes']:
break
elif choice in ['n', 'no']:
print("\nWarning: Continuing with SELinux Enforcing may cause injection to fail.")
print("You can manually set it later with: adb shell \"su -c 'setenforce 0'\"")
return True
else:
print("Please enter 'y' or 'n'")
# Set SELinux to Permissive
print("\nSetting SELinux to Permissive...")
result = adb.run(['shell', 'su', '-c', 'setenforce 0'], check=False)
if result.returncode == 0:
print("✓ SELinux set to Permissive mode")
print(" Note: This setting will reset after reboot")
return True
else:
print("✗ Failed to set SELinux to Permissive")
print(f" Error: {result.stderr.strip()}")
print("\nPlease manually run: adb shell \"su -c 'setenforce 0'\"")
return False
else:
print(f"Unknown SELinux status: {status}")
return True
def restart_app(adb: ADBHelper, package_name: str):
"""Restart the target application"""
print(f"\n=== Restarting Application ===")
# Force stop
print(f"Stopping {package_name}...")
result = adb.run(['shell', 'am', 'force-stop', package_name], check=False)
if result.returncode == 0:
print("✓ App stopped")
else:
print("⚠️ Failed to stop app")
# Get main activity
print(f"\nGetting launch activity...")
result = adb.run(['shell', 'pm', 'dump', package_name, '|', 'grep', '-A', '1', 'android.intent.action.MAIN'], check=False)
# Try to start the app
print(f"Starting {package_name}...")
result = adb.run(['shell', 'monkey', '-p', package_name, '-c', 'android.intent.category.LAUNCHER', '1'], check=False)
if result.returncode == 0:
print("✓ App started")
print(" Note: The app should now load with Frida Gadget injected")
return True
else:
print("⚠️ Failed to start app automatically")
print(" Please start the app manually from the device")
return False
def main():
"""Main entry point"""
# Parse command line arguments
parser = argparse.ArgumentParser(
description='Zygisk-MyInjector Auto Config Tool',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
'-p', '--port',
type=int,
choices=DEFAULT_PORTS,
help=f'Preset Gadget port (choices: {", ".join(map(str, DEFAULT_PORTS))})'
)
args = parser.parse_args()
print("=" * 60)
print("Zygisk-MyInjector Auto Config Tool")
print("=" * 60)
# Step 1: Select device
device_id = select_device()
if not device_id:
sys.exit(1)
adb = ADBHelper(device_id)
# Step 2: Check and set SELinux
if not check_and_set_selinux(adb):
print("\nError: Failed to configure SELinux")
print("The injection may fail. Please fix SELinux manually and try again.")
sys.exit(1)
# Step 3: Select package
package_name = select_package(adb)
if not package_name:
sys.exit(1)
# Step 4: Configure gadget
gadget_config = configure_gadget(preset_port=args.port)
if not gadget_config:
sys.exit(1)
# Step 5: Ensure Frida Gadget is installed
if not ensure_gadget_installed(adb, gadget_config['gadgetName']):
print("\nError: Failed to install Frida Gadget")
print("Please manually download and install libgadget.so")
sys.exit(1)
# Step 6: Generate config files
print("\n=== Generating Configuration Files ===")
config_json, gadget_config_json = generate_config_files(package_name, gadget_config)
print("\nGenerated config.json:")
print(config_json)
print("\nGenerated gadget config:")
print(gadget_config_json)
# Step 7: Save to temp files
temp_dir = tempfile.mkdtemp(prefix='frida_gadget_config_')
config_file = os.path.join(temp_dir, 'config.json')
gadget_name = gadget_config['gadgetName'].replace('.so', '.config.so')
gadget_config_file = os.path.join(temp_dir, gadget_name)
with open(config_file, 'w', encoding='utf-8') as f:
f.write(config_json)
with open(gadget_config_file, 'w', encoding='utf-8') as f:
f.write(gadget_config_json)
print(f"\nConfig files saved to: {temp_dir}")
# Step 8: Push to device
print("\n=== Pushing Files to Device ===")
remote_config = '/data/local/tmp/zygisk_config.json'
remote_gadget_config = f'/data/local/tmp/{gadget_name}'
if not adb.push_file(config_file, remote_config):
print("Error: Failed to push config.json")
sys.exit(1)
print(f"✓ Pushed config.json -> {remote_config}")
if not adb.push_file(gadget_config_file, remote_gadget_config):
print("Error: Failed to push gadget config")
sys.exit(1)
print(f"✓ Pushed gadget config -> {remote_gadget_config}")
# Step 9: Send broadcast
print("\n=== Sending Broadcast to Apply Config ===")
success = adb.send_broadcast(
action='com.jiqiu.configapp.APPLY_CONFIG',
component='com.jiqiu.configapp/.ConfigApplyReceiver',
extras={
'package_name': package_name,
'tmp_config_path': remote_config,
'tmp_gadget_config_path': remote_gadget_config
}
)
if success:
print("\n✓ Configuration applied successfully!")
print(f"\nThe app '{package_name}' has been configured.")
# 自动完成工作流程
print("\n=== Completing Workflow ===")
# Step 1: Restart app
restart_app(adb, package_name)
# Step 2: Setup port forwarding
port = gadget_config.get('port', 27042)
if setup_port_forward(adb, port):
# Step 3: Quick test
import time
print("\nWaiting 3 seconds for app to initialize...")
time.sleep(3)
try:
test_success = quick_test(adb, port)
if test_success:
# 彩色打印 frida 命令
print("\n" + "="*60)
print("\033[1;32m✓ All tests passed!\033[0m\n")
print("\033[1;36mYou can now use Frida with the following commands:\033[0m\n")
# Interactive mode
print("\033[1;33m# Interactive REPL:\033[0m")
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget\033[0m\n")
# Execute script
print("\033[1;33m# Execute JavaScript code:\033[0m")
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget -e 'YOUR_CODE_HERE'\033[0m\n")
# Load script file
print("\033[1;33m# Load script file:\033[0m")
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget -l your_script.js\033[0m\n")
print("="*60)
except Exception as e:
print(f"\n⚠️ Test failed: {e}")
print("\nYou can manually test with:")
if gadget_config['mode'] == 'server':
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget -l your_script.js\033[0m")
else:
print("\n✗ Failed to apply configuration")
print("Please check logcat for details:")
print(f" adb -s {device_id} logcat -s ConfigApplyReceiver:* ConfigManager:*")
sys.exit(1)
# Clean up temp directory
try:
shutil.rmtree(temp_dir)
except Exception:
pass # Ignore cleanup errors
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\n\nOperation cancelled by user")
sys.exit(130)