diff --git a/.gitignore b/.gitignore
index fc00840..c63cb9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,11 @@
/captures
/out
.externalNativeBuild
-.cxx
\ No newline at end of file
+.cxx
+
+# Frida Gadget temporary downloads
+tmp_gadget/
+*.so
+
+# Frida Gadget local cache
+scripts/.cache/
diff --git a/README.md b/README.md
index 19eea22..c989085 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,30 @@ Riru Hide成功生效。

+### 自动化注入
+
+[脚本](./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)
+
## 编译指南
### 自动编译
diff --git a/assets/auto_config.record.mp4 b/assets/auto_config.record.mp4
new file mode 100644
index 0000000..fec8f16
Binary files /dev/null and b/assets/auto_config.record.mp4 differ
diff --git a/configapp/src/main/AndroidManifest.xml b/configapp/src/main/AndroidManifest.xml
index 0ddd386..96a1764 100644
--- a/configapp/src/main/AndroidManifest.xml
+++ b/configapp/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
-
\ No newline at end of file
+
diff --git a/configapp/src/main/java/com/jiqiu/configapp/ConfigApplication.java b/configapp/src/main/java/com/jiqiu/configapp/ConfigApplication.java
new file mode 100644
index 0000000..f816dd6
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/ConfigApplication.java
@@ -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");
+ }
+ }
+ }
+}
diff --git a/configapp/src/main/java/com/jiqiu/configapp/ConfigApplyReceiver.java b/configapp/src/main/java/com/jiqiu/configapp/ConfigApplyReceiver.java
new file mode 100644
index 0000000..6390ac4
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/ConfigApplyReceiver.java
@@ -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();
+ }
+}
diff --git a/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java b/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java
index 8cb5f73..459bb77 100644
--- a/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java
+++ b/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java
@@ -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;
diff --git a/scripts/.gitignore b/scripts/.gitignore
new file mode 100644
index 0000000..6222101
--- /dev/null
+++ b/scripts/.gitignore
@@ -0,0 +1,4 @@
+# Temporary config files
+.tmp/
+*.pyc
+__pycache__/
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..91ab8bc
--- /dev/null
+++ b/scripts/README.md
@@ -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
+```
+
+## 生成的文件
+
+脚本会生成以下文件:
+
+1. **config.json**
+ - 模块主配置文件
+ - 存储位置:`/data/adb/modules/zygisk-myinjector/config.json`
+
+2. **gadget 配置文件**
+ - 格式:`libgadget.config.so`
+ - 存储位置:`/data/data//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))"
+```
diff --git a/scripts/auto_config.py b/scripts/auto_config.py
new file mode 100755
index 0000000..5799e7f
--- /dev/null
+++ b/scripts/auto_config.py
@@ -0,0 +1,813 @@
+#!/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
+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"
+
+# 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() -> 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'
+
+ port = input("Listen port (default: 27042): ").strip()
+ try:
+ gadget_config['port'] = int(port) if port else 27042
+ except ValueError:
+ print("Invalid port, using default 27042")
+ gadget_config['port'] = 27042
+
+ 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"""
+ 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()
+ 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 = os.path.join(os.path.dirname(__file__), '.tmp')
+ os.makedirs(temp_dir, exist_ok=True)
+
+ 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)
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except KeyboardInterrupt:
+ print("\n\nOperation cancelled by user")
+ sys.exit(130)