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成功生效。 ![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) + ## 编译指南 ### 自动编译 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)