Files
Zygisk-MyInjector/scripts/auto_config.py
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

814 lines
28 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)