Compare commits
27 Commits
linker_loa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8207e728dc | ||
|
|
dd67f20b8d | ||
|
|
65e91d188f | ||
|
|
ec89b7bf2c | ||
|
|
cc47334970 | ||
|
|
8a23161e0f | ||
|
|
964c975cdd | ||
|
|
6110216556 | ||
|
|
e02a3df7fc | ||
|
|
cb64bc7d48 | ||
|
|
fc24ab3455 | ||
|
|
7e7b38caf6 | ||
|
|
b8b8dafed0 | ||
|
|
7b7389c0a0 | ||
|
|
122878f8fa | ||
|
|
13ee77e96f | ||
|
|
e4ca55d6cd | ||
|
|
f955835df5 | ||
|
|
7e5a96cd78 | ||
|
|
cc4fb60b7b | ||
|
|
0c713a26da | ||
|
|
fee5600129 | ||
|
|
cec1db61fb | ||
|
|
d201c72e22 | ||
|
|
92a640a3c7 | ||
|
|
da73a3f9bd | ||
|
|
aa6709b9d9 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -11,4 +11,11 @@
|
||||
/captures
|
||||
/out
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
.cxx
|
||||
|
||||
# Frida Gadget temporary downloads
|
||||
tmp_gadget/
|
||||
*.so
|
||||
|
||||
# Frida Gadget local cache
|
||||
scripts/.cache/
|
||||
|
||||
36
README.md
36
README.md
@@ -12,13 +12,15 @@
|
||||
|
||||
项目已完全开源,包含面具模块、管理APP以及所有打包脚本,并配置了GitHub CI自动构建。欢迎各位开发者贡献代码,提交PR。
|
||||
|
||||
### 版本规划
|
||||
|
||||
### 版本规划&更新记录
|
||||
版本规划:
|
||||
- **v1.x**:专注功能添加,暂不考虑反检测
|
||||
- **v2.x**:实现各种检测绕过,达到100%无痕注入
|
||||
|
||||
更新记录:
|
||||
- **v1.2**: 增加gadget配置的自动生成,支持脚本和server模式,解决了若干bug,增加了全局注入延迟设置
|
||||
|
||||
## 致谢
|
||||
|
||||
**项目地址**:[https://github.com/jiqiu2022/Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
|
||||
|
||||
特别感谢以下项目和开发者(按时间顺序):
|
||||
@@ -132,6 +134,30 @@ Riru Hide成功生效。
|
||||
|
||||

|
||||
|
||||
### 自动化注入
|
||||
|
||||
[脚本](./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)
|
||||
|
||||
## 编译指南
|
||||
|
||||
### 自动编译
|
||||
@@ -176,7 +202,7 @@ Aborted
|
||||
|
||||
## 后续更新计划
|
||||
|
||||
- **深度隐藏**:联动面具模块,提供maps等更深层次的隐藏功能
|
||||
- **深度隐藏**:联动内核模块,提供maps等更深层次的隐藏功能
|
||||
- **用户体验优化**:
|
||||
- 增加gadget配置一键生成功能
|
||||
- 提供更友好的界面交互
|
||||
@@ -184,4 +210,4 @@ Aborted
|
||||
|
||||
---
|
||||
|
||||
欢迎关注项目进展,期待您的贡献!
|
||||
欢迎关注项目进展,期待您的贡献!
|
||||
|
||||
BIN
assets/auto_config.record.mp4
Normal file
BIN
assets/auto_config.record.mp4
Normal file
Binary file not shown.
12
build_all.sh
12
build_all.sh
@@ -105,10 +105,14 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建 META-INF 目录(Magisk 需要)
|
||||
mkdir -p $TEMP_DIR/META-INF/com/google/android
|
||||
touch $TEMP_DIR/META-INF/com/google/android/update-binary
|
||||
touch $TEMP_DIR/META-INF/com/google/android/updater-script
|
||||
# 复制 META-INF 目录(Magisk 需要)
|
||||
if [ -d "template/magisk_module/META-INF" ]; then
|
||||
cp -r template/magisk_module/META-INF $TEMP_DIR/
|
||||
echo -e " ${GREEN}✓ 复制 META-INF${NC}"
|
||||
else
|
||||
echo -e " ${RED}✗ 未找到 META-INF 模板${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 打包
|
||||
echo -e "\n${YELLOW}[5/5] 打包模块...${NC}"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".ConfigApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -26,4 +27,4 @@
|
||||
android:parentActivityName=".MainActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -140,6 +140,12 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
||||
RadioButton radioStandardInjection = dialogView.findViewById(R.id.radioStandardInjection);
|
||||
RadioButton radioRiruInjection = dialogView.findViewById(R.id.radioRiruInjection);
|
||||
RadioButton radioCustomLinkerInjection = dialogView.findViewById(R.id.radioCustomLinkerInjection);
|
||||
RadioGroup gadgetConfigGroup = dialogView.findViewById(R.id.gadgetConfigGroup);
|
||||
RadioButton radioNoGadget = dialogView.findViewById(R.id.radioNoGadget);
|
||||
RadioButton radioUseGlobalGadget = dialogView.findViewById(R.id.radioUseGlobalGadget);
|
||||
RadioButton radioUseCustomGadget = dialogView.findViewById(R.id.radioUseCustomGadget);
|
||||
TextView tvGlobalGadgetInfo = dialogView.findViewById(R.id.tvGlobalGadgetInfo);
|
||||
com.google.android.material.button.MaterialButton btnConfigureGadget = dialogView.findViewById(R.id.btnConfigureGadget);
|
||||
|
||||
appIcon.setImageDrawable(appInfo.getAppIcon());
|
||||
appName.setText(appInfo.getAppName());
|
||||
@@ -155,6 +161,74 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
||||
radioStandardInjection.setChecked(true);
|
||||
}
|
||||
|
||||
// Load gadget config
|
||||
boolean useGlobalGadget = configManager.getAppUseGlobalGadget(appInfo.getPackageName());
|
||||
ConfigManager.GadgetConfig appSpecificGadget = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
|
||||
|
||||
// Update global gadget info
|
||||
if (globalGadget != null) {
|
||||
String info = "全局: " + globalGadget.gadgetName;
|
||||
if (globalGadget.mode.equals("server")) {
|
||||
info += " (端口: " + globalGadget.port + ")";
|
||||
}
|
||||
tvGlobalGadgetInfo.setText(info);
|
||||
} else {
|
||||
tvGlobalGadgetInfo.setText("未配置全局Gadget");
|
||||
}
|
||||
|
||||
// Set initial radio selection
|
||||
if (!useGlobalGadget && appSpecificGadget != null) {
|
||||
radioUseCustomGadget.setChecked(true);
|
||||
btnConfigureGadget.setVisibility(View.VISIBLE);
|
||||
btnConfigureGadget.setEnabled(true);
|
||||
} else if (useGlobalGadget && globalGadget != null) {
|
||||
radioUseGlobalGadget.setChecked(true);
|
||||
btnConfigureGadget.setVisibility(View.GONE);
|
||||
} else {
|
||||
radioNoGadget.setChecked(true);
|
||||
btnConfigureGadget.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Setup gadget radio group listener
|
||||
gadgetConfigGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
if (checkedId == R.id.radioNoGadget) {
|
||||
btnConfigureGadget.setVisibility(View.GONE);
|
||||
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
|
||||
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
|
||||
} else if (checkedId == R.id.radioUseGlobalGadget) {
|
||||
btnConfigureGadget.setVisibility(View.GONE);
|
||||
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), true);
|
||||
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
|
||||
} else if (checkedId == R.id.radioUseCustomGadget) {
|
||||
btnConfigureGadget.setVisibility(View.VISIBLE);
|
||||
btnConfigureGadget.setEnabled(true);
|
||||
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
|
||||
}
|
||||
});
|
||||
|
||||
// Configure button listener
|
||||
btnConfigureGadget.setOnClickListener(v -> {
|
||||
ConfigManager.GadgetConfig currentConfig = null;
|
||||
if (!useGlobalGadget) {
|
||||
currentConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||
}
|
||||
if (currentConfig == null) {
|
||||
currentConfig = new ConfigManager.GadgetConfig();
|
||||
}
|
||||
|
||||
GadgetConfigDialog dialog = new GadgetConfigDialog(
|
||||
getContext(),
|
||||
"配置" + appInfo.getAppName() + "的Gadget",
|
||||
currentConfig,
|
||||
config -> {
|
||||
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
|
||||
configManager.setAppGadgetConfig(appInfo.getPackageName(), config);
|
||||
}
|
||||
);
|
||||
dialog.show();
|
||||
});
|
||||
|
||||
// Setup SO list
|
||||
List<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles();
|
||||
List<ConfigManager.SoFile> appSoFiles = configManager.getAppSoFiles(appInfo.getPackageName());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,15 @@ public class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to reload configuration from file
|
||||
* 从文件重新加载配置(用于外部更新配置后)
|
||||
*/
|
||||
public void reloadConfig() {
|
||||
loadConfig();
|
||||
Log.i(TAG, "Configuration reloaded");
|
||||
}
|
||||
|
||||
public void saveConfig() {
|
||||
String json = gson.toJson(config);
|
||||
// Write to temp file first
|
||||
@@ -163,20 +172,44 @@ public class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure SO storage directory exists
|
||||
Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
|
||||
Shell.cmd("chmod 755 " + SO_STORAGE_DIR).exec();
|
||||
|
||||
// Copy SO file to our storage
|
||||
Log.i(TAG, "Copying SO file from: " + originalPath + " to: " + storedPath);
|
||||
Shell.Result result = Shell.cmd("cp \"" + originalPath + "\" \"" + storedPath + "\"").exec();
|
||||
|
||||
if (result.isSuccess()) {
|
||||
// Verify the file was actually copied
|
||||
Shell.Result verifyResult = Shell.cmd("test -f \"" + storedPath + "\" && echo 'exists'").exec();
|
||||
if (!verifyResult.isSuccess() || verifyResult.getOut().isEmpty()) {
|
||||
Log.e(TAG, "File copy appeared successful but file not found at: " + storedPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set proper permissions for SO file (readable and executable)
|
||||
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + storedPath + "\"").exec();
|
||||
if (!chmodResult.isSuccess()) {
|
||||
Log.e(TAG, "Failed to set permissions on SO file: " + String.join("\n", chmodResult.getErr()));
|
||||
}
|
||||
|
||||
SoFile soFile = new SoFile();
|
||||
soFile.name = fileName;
|
||||
soFile.storedPath = storedPath;
|
||||
soFile.originalPath = originalPath;
|
||||
config.globalSoFiles.add(soFile);
|
||||
|
||||
Log.i(TAG, "Successfully added SO file: " + fileName + " to storage");
|
||||
|
||||
if (deleteOriginal) {
|
||||
Shell.cmd("rm \"" + originalPath + "\"").exec();
|
||||
Log.i(TAG, "Deleted original file: " + originalPath);
|
||||
}
|
||||
|
||||
saveConfig();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to copy SO file: " + String.join("\n", result.getErr()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +286,176 @@ public class ConfigManager {
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public int getInjectionDelay() {
|
||||
return config.injectionDelay;
|
||||
}
|
||||
|
||||
public void setInjectionDelay(int delay) {
|
||||
config.injectionDelay = delay;
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public GadgetConfig getAppGadgetConfig(String packageName) {
|
||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||
if (appConfig == null) {
|
||||
// If no app config, return global gadget config
|
||||
return config.globalGadgetConfig;
|
||||
}
|
||||
|
||||
// If app is set to use global gadget, return global config
|
||||
if (appConfig.useGlobalGadget) {
|
||||
return config.globalGadgetConfig;
|
||||
}
|
||||
|
||||
// Otherwise return app-specific gadget config
|
||||
return appConfig.gadgetConfig;
|
||||
}
|
||||
|
||||
public GadgetConfig getGlobalGadgetConfig() {
|
||||
return config.globalGadgetConfig;
|
||||
}
|
||||
|
||||
public void setGlobalGadgetConfig(GadgetConfig gadgetConfig) {
|
||||
config.globalGadgetConfig = gadgetConfig;
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public boolean getAppUseGlobalGadget(String packageName) {
|
||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||
if (appConfig == null) {
|
||||
return true; // Default to use global
|
||||
}
|
||||
return appConfig.useGlobalGadget;
|
||||
}
|
||||
|
||||
public void setAppUseGlobalGadget(String packageName, boolean useGlobal) {
|
||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||
if (appConfig == null) {
|
||||
appConfig = new AppConfig();
|
||||
config.perAppConfig.put(packageName, appConfig);
|
||||
}
|
||||
appConfig.useGlobalGadget = useGlobal;
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public void setAppGadgetConfig(String packageName, GadgetConfig gadgetConfig) {
|
||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||
if (appConfig == null) {
|
||||
appConfig = new AppConfig();
|
||||
config.perAppConfig.put(packageName, appConfig);
|
||||
}
|
||||
|
||||
// Remove old gadget from SO list if exists
|
||||
if (appConfig.gadgetConfig != null) {
|
||||
String oldGadgetName = appConfig.gadgetConfig.gadgetName;
|
||||
appConfig.soFiles.removeIf(soFile -> soFile.name.equals(oldGadgetName));
|
||||
}
|
||||
|
||||
appConfig.gadgetConfig = gadgetConfig;
|
||||
|
||||
// Add new gadget to SO list if configured
|
||||
if (gadgetConfig != null) {
|
||||
// Check if gadget SO file exists in global storage
|
||||
String gadgetPath = SO_STORAGE_DIR + "/" + gadgetConfig.gadgetName;
|
||||
Shell.Result checkResult = Shell.cmd("test -f \"" + gadgetPath + "\" && echo 'exists'").exec();
|
||||
|
||||
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
|
||||
// Add gadget as a SO file
|
||||
SoFile gadgetSoFile = new SoFile();
|
||||
gadgetSoFile.name = gadgetConfig.gadgetName;
|
||||
gadgetSoFile.storedPath = gadgetPath;
|
||||
gadgetSoFile.originalPath = gadgetPath;
|
||||
|
||||
// Check if already in list
|
||||
boolean alreadyExists = false;
|
||||
for (SoFile soFile : appConfig.soFiles) {
|
||||
if (soFile.name.equals(gadgetSoFile.name)) {
|
||||
alreadyExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyExists) {
|
||||
appConfig.soFiles.add(gadgetSoFile);
|
||||
Log.i(TAG, "Added gadget SO to app's SO list: " + gadgetSoFile.name);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Gadget SO file not found in storage: " + gadgetPath);
|
||||
Log.w(TAG, "Please ensure " + gadgetConfig.gadgetName + " is added to SO library");
|
||||
}
|
||||
}
|
||||
|
||||
saveConfig();
|
||||
|
||||
// If app is enabled, deploy both gadget SO and config file
|
||||
if (appConfig.enabled) {
|
||||
if (gadgetConfig != null) {
|
||||
deployGadgetConfigFile(packageName, gadgetConfig);
|
||||
}
|
||||
// Re-deploy all SO files including gadget
|
||||
deploySoFilesToApp(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
private void deployGadgetConfigFile(String packageName, GadgetConfig gadgetConfig) {
|
||||
try {
|
||||
// Create gadget config JSON
|
||||
String configJson;
|
||||
if ("script".equals(gadgetConfig.mode)) {
|
||||
configJson = String.format(
|
||||
"{\n" +
|
||||
" \"interaction\": {\n" +
|
||||
" \"type\": \"script\",\n" +
|
||||
" \"path\": \"%s\"\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
gadgetConfig.scriptPath
|
||||
);
|
||||
} else {
|
||||
configJson = String.format(
|
||||
"{\n" +
|
||||
" \"interaction\": {\n" +
|
||||
" \"type\": \"listen\",\n" +
|
||||
" \"address\": \"%s\",\n" +
|
||||
" \"port\": %d,\n" +
|
||||
" \"on_port_conflict\": \"%s\",\n" +
|
||||
" \"on_load\": \"%s\"\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
gadgetConfig.address,
|
||||
gadgetConfig.port,
|
||||
gadgetConfig.onPortConflict,
|
||||
gadgetConfig.onLoad
|
||||
);
|
||||
}
|
||||
|
||||
// Write to temp file
|
||||
String tempFile = context.getCacheDir() + "/" + gadgetConfig.gadgetName + ".config";
|
||||
java.io.FileWriter writer = new java.io.FileWriter(tempFile);
|
||||
writer.write(configJson);
|
||||
writer.close();
|
||||
|
||||
// Copy to app's files directory
|
||||
String filesDir = "/data/data/" + packageName + "/files";
|
||||
String gadgetConfigName = gadgetConfig.gadgetName.replace(".so", ".config.so");
|
||||
String targetPath = filesDir + "/" + gadgetConfigName;
|
||||
|
||||
Shell.Result copyResult = Shell.cmd("cp " + tempFile + " " + targetPath).exec();
|
||||
if (copyResult.isSuccess()) {
|
||||
// Set permissions
|
||||
Shell.cmd("chmod 644 " + targetPath).exec();
|
||||
Log.i(TAG, "Deployed gadget config to: " + targetPath);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to deploy gadget config: " + String.join("\n", copyResult.getErr()));
|
||||
}
|
||||
|
||||
// Clean up temp file
|
||||
new java.io.File(tempFile).delete();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to create gadget config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy SO files directly to app's data directory
|
||||
private void deploySoFilesToApp(String packageName) {
|
||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||
@@ -270,28 +473,36 @@ public class ConfigManager {
|
||||
// Create files directory in app's data dir
|
||||
String filesDir = "/data/data/" + packageName + "/files";
|
||||
|
||||
// Use su -c for better compatibility
|
||||
Shell.Result mkdirResult = Shell.cmd("su -c 'mkdir -p " + filesDir + "'").exec();
|
||||
Log.i(TAG, "Deploying SO files to: " + filesDir);
|
||||
|
||||
// Create directory without su -c for better compatibility
|
||||
Shell.Result mkdirResult = Shell.cmd("mkdir -p " + filesDir).exec();
|
||||
if (!mkdirResult.isSuccess()) {
|
||||
Log.e(TAG, "Failed to create directory: " + filesDir);
|
||||
Log.e(TAG, "Error: " + String.join("\n", mkdirResult.getErr()));
|
||||
// Try without su -c
|
||||
mkdirResult = Shell.cmd("mkdir -p " + filesDir).exec();
|
||||
if (!mkdirResult.isSuccess()) {
|
||||
Log.e(TAG, "Also failed without su -c");
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Set proper permissions and ownership
|
||||
Shell.cmd("chmod 755 " + filesDir).exec();
|
||||
// Set proper permissions and ownership for the files directory
|
||||
Shell.cmd("chmod 771 " + filesDir).exec();
|
||||
|
||||
// Get UID for the package
|
||||
// Get UID and GID for the package
|
||||
Shell.Result uidResult = Shell.cmd("stat -c %u /data/data/" + packageName).exec();
|
||||
String uid = "";
|
||||
if (uidResult.isSuccess() && !uidResult.getOut().isEmpty()) {
|
||||
uid = uidResult.getOut().get(0).trim();
|
||||
Log.i(TAG, "Package UID: " + uid);
|
||||
|
||||
// Set ownership of files directory to match app
|
||||
Shell.Result chownDirResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + filesDir + "\"").exec();
|
||||
if (!chownDirResult.isSuccess()) {
|
||||
Log.e(TAG, "Failed to set directory ownership: " + String.join("\n", chownDirResult.getErr()));
|
||||
}
|
||||
|
||||
// Set SELinux context for the directory
|
||||
Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + filesDir + "\"").exec();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to get package UID");
|
||||
}
|
||||
|
||||
// Copy each SO file configured for this app
|
||||
@@ -303,44 +514,77 @@ public class ConfigManager {
|
||||
Shell.Result checkResult = Shell.cmd("test -f \"" + soFile.storedPath + "\" && echo 'exists'").exec();
|
||||
if (!checkResult.isSuccess() || checkResult.getOut().isEmpty()) {
|
||||
Log.e(TAG, "Source SO file not found: " + soFile.storedPath);
|
||||
// Log more details about the missing file
|
||||
Shell.Result lsResult = Shell.cmd("ls -la \"" + SO_STORAGE_DIR + "\"").exec();
|
||||
Log.e(TAG, "Contents of SO storage dir: " + String.join("\n", lsResult.getOut()));
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Copying: " + soFile.storedPath + " to " + destPath);
|
||||
|
||||
// Copy file using cat to avoid permission issues
|
||||
String copyCmd = "cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"";
|
||||
Shell.Result result = Shell.cmd(copyCmd).exec();
|
||||
// First, ensure the destination directory exists and has proper permissions
|
||||
Shell.cmd("mkdir -p \"" + filesDir + "\"").exec();
|
||||
Shell.cmd("chmod 755 \"" + filesDir + "\"").exec();
|
||||
|
||||
// Copy file using cp with force flag
|
||||
Shell.Result result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
Log.e(TAG, "Failed with cat, trying cp");
|
||||
// Fallback to cp
|
||||
result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
|
||||
Log.e(TAG, "Failed with cp, trying cat method");
|
||||
Log.e(TAG, "cp error: " + String.join("\n", result.getErr()));
|
||||
// Fallback to cat method
|
||||
result = Shell.cmd("cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"").exec();
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
Log.e(TAG, "Also failed with cat method");
|
||||
Log.e(TAG, "cat error: " + String.join("\n", result.getErr()));
|
||||
}
|
||||
}
|
||||
|
||||
// Set permissions
|
||||
Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
|
||||
// Set permissions - SO files need to be readable and executable
|
||||
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
|
||||
if (!chmodResult.isSuccess()) {
|
||||
Log.e(TAG, "Failed to set permissions: " + String.join("\n", chmodResult.getErr()));
|
||||
}
|
||||
|
||||
// Set ownership if we have the UID
|
||||
// Set ownership to match the app's UID
|
||||
if (!uid.isEmpty()) {
|
||||
Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
|
||||
Shell.Result chownResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
|
||||
if (!chownResult.isSuccess()) {
|
||||
Log.e(TAG, "Failed to set ownership: " + String.join("\n", chownResult.getErr()));
|
||||
// Try alternative method
|
||||
Shell.cmd("chown " + uid + ".app_" + uid + " \"" + destPath + "\"").exec();
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the file was copied
|
||||
Shell.Result verifyResult = Shell.cmd("ls -la \"" + destPath + "\" 2>/dev/null").exec();
|
||||
// Set SELinux context to match app's data files
|
||||
Shell.Result contextResult = Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + destPath + "\"").exec();
|
||||
if (!contextResult.isSuccess()) {
|
||||
Log.w(TAG, "Failed to set SELinux context (this may be normal on some devices)");
|
||||
}
|
||||
|
||||
// Verify the file was copied with correct permissions
|
||||
Shell.Result verifyResult = Shell.cmd("ls -laZ \"" + destPath + "\" 2>/dev/null").exec();
|
||||
if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
|
||||
Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
|
||||
} else {
|
||||
Log.e(TAG, "Failed to verify SO file copy: " + destPath);
|
||||
// Try another verification method
|
||||
Shell.Result sizeResult = Shell.cmd("stat -c %s \"" + destPath + "\" 2>/dev/null").exec();
|
||||
if (sizeResult.isSuccess() && !sizeResult.getOut().isEmpty()) {
|
||||
Log.i(TAG, "File exists with size: " + sizeResult.getOut().get(0) + " bytes");
|
||||
// Fallback verification without SELinux context
|
||||
verifyResult = Shell.cmd("ls -la \"" + destPath + "\" 2>/dev/null").exec();
|
||||
if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
|
||||
Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
|
||||
} else {
|
||||
Log.e(TAG, "Failed to verify SO file copy: " + destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Deployment complete for: " + packageName);
|
||||
|
||||
// Deploy gadget config if configured
|
||||
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
|
||||
if (gadgetToUse != null) {
|
||||
deployGadgetConfigFile(packageName, gadgetToUse);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up deployed SO files when app is disabled
|
||||
@@ -386,6 +630,24 @@ public class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up gadget config file if exists
|
||||
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
|
||||
if (gadgetToUse != null) {
|
||||
String gadgetConfigName = gadgetToUse.gadgetName.replace(".so", ".config.so");
|
||||
String configPath = filesDir + "/" + gadgetConfigName;
|
||||
|
||||
Shell.Result checkConfigResult = Shell.cmd("test -f \"" + configPath + "\" && echo 'exists'").exec();
|
||||
if (checkConfigResult.isSuccess() && !checkConfigResult.getOut().isEmpty()) {
|
||||
Shell.Result deleteResult = Shell.cmd("rm -f \"" + configPath + "\"").exec();
|
||||
if (deleteResult.isSuccess()) {
|
||||
Log.i(TAG, "Deleted gadget config: " + configPath);
|
||||
} else {
|
||||
// Try with su -c
|
||||
Shell.cmd("su -c 'rm -f \"" + configPath + "\"'").exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Cleanup complete for: " + packageName);
|
||||
}
|
||||
|
||||
@@ -398,18 +660,35 @@ 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;
|
||||
public boolean hideInjection = false;
|
||||
public int injectionDelay = 2; // Default 2 seconds
|
||||
public List<SoFile> globalSoFiles = new ArrayList<>();
|
||||
public Map<String, AppConfig> perAppConfig = new HashMap<>();
|
||||
public GadgetConfig globalGadgetConfig = null; // Global gadget configuration
|
||||
}
|
||||
|
||||
public static class AppConfig {
|
||||
public boolean enabled = false;
|
||||
public List<SoFile> soFiles = new ArrayList<>();
|
||||
public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker"
|
||||
public GadgetConfig gadgetConfig = null;
|
||||
public boolean useGlobalGadget = true; // Whether to use global gadget settings
|
||||
}
|
||||
|
||||
public static class SoFile {
|
||||
@@ -425,4 +704,17 @@ public class ConfigManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GadgetConfig {
|
||||
public String mode = "server"; // "server" or "script"
|
||||
// Server mode config
|
||||
public String address = "0.0.0.0";
|
||||
public int port = 27042;
|
||||
public String onPortConflict = "fail";
|
||||
public String onLoad = "wait";
|
||||
// Script mode config
|
||||
public String scriptPath = "/data/local/tmp/script.js";
|
||||
// Common config
|
||||
public String gadgetName = "libgadget.so";
|
||||
}
|
||||
}
|
||||
171
configapp/src/main/java/com/jiqiu/configapp/FileUtils.java
Normal file
171
configapp/src/main/java/com/jiqiu/configapp/FileUtils.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package com.jiqiu.configapp;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class FileUtils {
|
||||
private static final String TAG = "FileUtils";
|
||||
|
||||
/**
|
||||
* Get real file path from URI, handling both file:// and content:// URIs
|
||||
* @param context Context
|
||||
* @param uri The URI to resolve
|
||||
* @return The real file path, or null if unable to resolve
|
||||
*/
|
||||
public static String getRealPathFromUri(Context context, Uri uri) {
|
||||
if (uri == null) return null;
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null) return null;
|
||||
|
||||
// Handle file:// URIs
|
||||
if ("file".equals(scheme)) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
// Handle content:// URIs
|
||||
if ("content".equals(scheme)) {
|
||||
// For content URIs, we need to copy the file to a temporary location
|
||||
return copyFileFromContentUri(context, uri);
|
||||
}
|
||||
|
||||
// Try direct path extraction as fallback
|
||||
String path = uri.getPath();
|
||||
if (path != null) {
|
||||
// Some file managers return paths like /external_files/...
|
||||
// Try to resolve these to actual paths
|
||||
if (path.contains(":")) {
|
||||
String[] parts = path.split(":");
|
||||
if (parts.length == 2) {
|
||||
String type = parts[0];
|
||||
String relativePath = parts[1];
|
||||
|
||||
// Common storage locations
|
||||
if (type.endsWith("/primary")) {
|
||||
return "/storage/emulated/0/" + relativePath;
|
||||
} else if (type.contains("external")) {
|
||||
return "/storage/emulated/0/" + relativePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any file:// prefix
|
||||
if (path.startsWith("file://")) {
|
||||
path = path.substring(7);
|
||||
}
|
||||
|
||||
// Check if the path exists
|
||||
Shell.Result result = Shell.cmd("test -f \"" + path + "\" && echo 'exists'").exec();
|
||||
if (result.isSuccess() && !result.getOut().isEmpty()) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file from content URI to temporary location
|
||||
* @param context Context
|
||||
* @param uri Content URI
|
||||
* @return Path to copied file, or null on failure
|
||||
*/
|
||||
private static String copyFileFromContentUri(Context context, Uri uri) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
String fileName = getFileName(context, uri);
|
||||
|
||||
if (fileName == null || !fileName.endsWith(".so")) {
|
||||
fileName = "temp_" + System.currentTimeMillis() + ".so";
|
||||
}
|
||||
|
||||
// Create temp directory
|
||||
File tempDir = new File(context.getCacheDir(), "so_temp");
|
||||
if (!tempDir.exists()) {
|
||||
tempDir.mkdirs();
|
||||
}
|
||||
|
||||
File tempFile = new File(tempDir, fileName);
|
||||
|
||||
try (InputStream inputStream = resolver.openInputStream(uri);
|
||||
OutputStream outputStream = new FileOutputStream(tempFile)) {
|
||||
|
||||
if (inputStream == null) {
|
||||
Log.e(TAG, "Unable to open input stream for URI: " + uri);
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
// Make file readable
|
||||
tempFile.setReadable(true, false);
|
||||
|
||||
// First copy to /data/local/tmp as a temporary location
|
||||
String tempTargetPath = "/data/local/tmp/" + fileName;
|
||||
Shell.Result result = Shell.cmd(
|
||||
"cp \"" + tempFile.getAbsolutePath() + "\" \"" + tempTargetPath + "\"",
|
||||
"chmod 644 \"" + tempTargetPath + "\""
|
||||
).exec();
|
||||
|
||||
// Clean up temp file
|
||||
tempFile.delete();
|
||||
|
||||
if (result.isSuccess()) {
|
||||
// Return the temporary path - it will be moved to the proper location by addGlobalSoFile
|
||||
return tempTargetPath;
|
||||
} else {
|
||||
Log.e(TAG, "Failed to copy file to /data/local/tmp/");
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error copying file from content URI", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file name from URI
|
||||
* @param context Context
|
||||
* @param uri URI to get name from
|
||||
* @return File name or null
|
||||
*/
|
||||
private static String getFileName(Context context, Uri uri) {
|
||||
String fileName = null;
|
||||
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
try (Cursor cursor = context.getContentResolver().query(
|
||||
uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (nameIndex >= 0) {
|
||||
fileName = cursor.getString(nameIndex);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error getting file name from URI", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileName == null) {
|
||||
fileName = uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
package com.jiqiu.configapp;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class GadgetConfigDialog extends DialogFragment {
|
||||
|
||||
// UI elements
|
||||
private RadioGroup modeRadioGroup;
|
||||
private RadioButton radioModeServer;
|
||||
private RadioButton radioModeScript;
|
||||
private LinearLayout serverModeLayout;
|
||||
private LinearLayout scriptModeLayout;
|
||||
private RadioGroup addressRadioGroup;
|
||||
private RadioButton radioAddressAll;
|
||||
private RadioButton radioAddressLocal;
|
||||
private RadioButton radioAddressCustom;
|
||||
private EditText editCustomAddress;
|
||||
private EditText editPort;
|
||||
private RadioGroup portConflictRadioGroup;
|
||||
private RadioButton radioConflictFail;
|
||||
private RadioButton radioConflictPickNext;
|
||||
private RadioGroup onLoadRadioGroup;
|
||||
private RadioButton radioLoadWait;
|
||||
private RadioButton radioLoadResume;
|
||||
private EditText editScriptPath;
|
||||
private EditText editGadgetName;
|
||||
private EditText editJsonPreview;
|
||||
|
||||
// Configuration data
|
||||
private ConfigManager.GadgetConfig config;
|
||||
private OnGadgetConfigListener listener;
|
||||
private String customTitle;
|
||||
|
||||
// Flag to prevent recursive updates
|
||||
private boolean isUpdatingUI = false;
|
||||
|
||||
// Activity result launchers
|
||||
private ActivityResultLauncher<Intent> fileBrowserLauncher;
|
||||
private ActivityResultLauncher<Intent> filePickerLauncher;
|
||||
|
||||
public interface OnGadgetConfigListener {
|
||||
void onGadgetConfigSaved(ConfigManager.GadgetConfig config);
|
||||
}
|
||||
|
||||
public static GadgetConfigDialog newInstance(ConfigManager.GadgetConfig config) {
|
||||
GadgetConfigDialog dialog = new GadgetConfigDialog();
|
||||
dialog.config = config != null ? config : new ConfigManager.GadgetConfig();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
// Constructor for non-fragment usage
|
||||
public GadgetConfigDialog(Context context, String title, ConfigManager.GadgetConfig config, OnGadgetConfigListener listener) {
|
||||
// This constructor is for compatibility with direct dialog creation
|
||||
// The actual dialog will be created in show() method
|
||||
this.savedContext = context;
|
||||
this.customTitle = title;
|
||||
this.config = config != null ? config : new ConfigManager.GadgetConfig();
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
// Default constructor required for DialogFragment
|
||||
public GadgetConfigDialog() {
|
||||
// Empty constructor required
|
||||
}
|
||||
|
||||
public void setOnGadgetConfigListener(OnGadgetConfigListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Initialize file browser launcher
|
||||
fileBrowserLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == android.app.Activity.RESULT_OK && result.getData() != null) {
|
||||
String selectedPath = result.getData().getStringExtra("selected_path");
|
||||
if (selectedPath != null) {
|
||||
editScriptPath.setText(selectedPath);
|
||||
config.scriptPath = selectedPath;
|
||||
updateJsonPreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Initialize file picker launcher
|
||||
filePickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == android.app.Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri uri = result.getData().getData();
|
||||
if (uri != null) {
|
||||
String path = getPathFromUri(uri);
|
||||
if (path != null) {
|
||||
editScriptPath.setText(path);
|
||||
config.scriptPath = path;
|
||||
updateJsonPreview();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "无法获取文件路径", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_gadget_config, null);
|
||||
|
||||
initViews(view);
|
||||
loadConfig();
|
||||
setupListeners();
|
||||
updateJsonPreview();
|
||||
|
||||
String title = customTitle != null ? customTitle : "Gadget 配置";
|
||||
|
||||
return new MaterialAlertDialogBuilder(getContext())
|
||||
.setTitle(title)
|
||||
.setView(view)
|
||||
.setPositiveButton("保存", (dialog, which) -> saveConfig())
|
||||
.setNegativeButton("取消", null)
|
||||
.create();
|
||||
}
|
||||
|
||||
private void initViews(View view) {
|
||||
modeRadioGroup = view.findViewById(R.id.modeRadioGroup);
|
||||
radioModeServer = view.findViewById(R.id.radioModeServer);
|
||||
radioModeScript = view.findViewById(R.id.radioModeScript);
|
||||
serverModeLayout = view.findViewById(R.id.serverModeLayout);
|
||||
scriptModeLayout = view.findViewById(R.id.scriptModeLayout);
|
||||
addressRadioGroup = view.findViewById(R.id.addressRadioGroup);
|
||||
radioAddressAll = view.findViewById(R.id.radioAddressAll);
|
||||
radioAddressLocal = view.findViewById(R.id.radioAddressLocal);
|
||||
radioAddressCustom = view.findViewById(R.id.radioAddressCustom);
|
||||
editCustomAddress = view.findViewById(R.id.editCustomAddress);
|
||||
editPort = view.findViewById(R.id.editPort);
|
||||
portConflictRadioGroup = view.findViewById(R.id.portConflictRadioGroup);
|
||||
radioConflictFail = view.findViewById(R.id.radioConflictFail);
|
||||
radioConflictPickNext = view.findViewById(R.id.radioConflictPickNext);
|
||||
onLoadRadioGroup = view.findViewById(R.id.onLoadRadioGroup);
|
||||
radioLoadWait = view.findViewById(R.id.radioLoadWait);
|
||||
radioLoadResume = view.findViewById(R.id.radioLoadResume);
|
||||
editScriptPath = view.findViewById(R.id.editScriptPath);
|
||||
editGadgetName = view.findViewById(R.id.editGadgetName);
|
||||
editJsonPreview = view.findViewById(R.id.editJsonPreview);
|
||||
|
||||
// File select button
|
||||
View btnSelectScript = view.findViewById(R.id.btnSelectScript);
|
||||
if (btnSelectScript != null) {
|
||||
btnSelectScript.setOnClickListener(v -> selectScriptFile());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadConfig() {
|
||||
isUpdatingUI = true;
|
||||
|
||||
// Load mode
|
||||
if ("script".equals(config.mode)) {
|
||||
radioModeScript.setChecked(true);
|
||||
serverModeLayout.setVisibility(View.GONE);
|
||||
scriptModeLayout.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
radioModeServer.setChecked(true);
|
||||
serverModeLayout.setVisibility(View.VISIBLE);
|
||||
scriptModeLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Load address
|
||||
if ("127.0.0.1".equals(config.address)) {
|
||||
radioAddressLocal.setChecked(true);
|
||||
} else if ("0.0.0.0".equals(config.address)) {
|
||||
radioAddressAll.setChecked(true);
|
||||
} else {
|
||||
radioAddressCustom.setChecked(true);
|
||||
editCustomAddress.setText(config.address);
|
||||
editCustomAddress.setEnabled(true);
|
||||
}
|
||||
|
||||
// Load port
|
||||
editPort.setText(String.valueOf(config.port));
|
||||
|
||||
// Load port conflict handling
|
||||
if ("pick-next".equals(config.onPortConflict)) {
|
||||
radioConflictPickNext.setChecked(true);
|
||||
} else {
|
||||
radioConflictFail.setChecked(true);
|
||||
}
|
||||
|
||||
// Load on load handling
|
||||
if ("resume".equals(config.onLoad)) {
|
||||
radioLoadResume.setChecked(true);
|
||||
} else {
|
||||
radioLoadWait.setChecked(true);
|
||||
}
|
||||
|
||||
// Load script path
|
||||
editScriptPath.setText(config.scriptPath);
|
||||
|
||||
// Load gadget name
|
||||
editGadgetName.setText(config.gadgetName);
|
||||
|
||||
isUpdatingUI = false;
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
// Mode radio group listener
|
||||
modeRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
if (!isUpdatingUI) {
|
||||
if (checkedId == R.id.radioModeScript) {
|
||||
config.mode = "script";
|
||||
serverModeLayout.setVisibility(View.GONE);
|
||||
scriptModeLayout.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
config.mode = "server";
|
||||
serverModeLayout.setVisibility(View.VISIBLE);
|
||||
scriptModeLayout.setVisibility(View.GONE);
|
||||
}
|
||||
updateJsonPreview();
|
||||
}
|
||||
});
|
||||
|
||||
// Address radio group listener
|
||||
addressRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
if (!isUpdatingUI) {
|
||||
if (checkedId == R.id.radioAddressCustom) {
|
||||
editCustomAddress.setEnabled(true);
|
||||
editCustomAddress.requestFocus();
|
||||
} else {
|
||||
editCustomAddress.setEnabled(false);
|
||||
if (checkedId == R.id.radioAddressAll) {
|
||||
config.address = "0.0.0.0";
|
||||
} else if (checkedId == R.id.radioAddressLocal) {
|
||||
config.address = "127.0.0.1";
|
||||
}
|
||||
updateJsonPreview();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Custom address text watcher
|
||||
editCustomAddress.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (!isUpdatingUI && radioAddressCustom.isChecked()) {
|
||||
config.address = s.toString().trim();
|
||||
updateJsonPreview();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Port text watcher
|
||||
editPort.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (!isUpdatingUI) {
|
||||
try {
|
||||
int port = Integer.parseInt(s.toString());
|
||||
if (port >= 1 && port <= 65535) {
|
||||
config.port = port;
|
||||
updateJsonPreview();
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore invalid input
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Port conflict radio group listener
|
||||
portConflictRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
if (!isUpdatingUI) {
|
||||
config.onPortConflict = (checkedId == R.id.radioConflictPickNext) ? "pick-next" : "fail";
|
||||
updateJsonPreview();
|
||||
}
|
||||
});
|
||||
|
||||
// On load radio group listener
|
||||
onLoadRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
if (!isUpdatingUI) {
|
||||
config.onLoad = (checkedId == R.id.radioLoadResume) ? "resume" : "wait";
|
||||
updateJsonPreview();
|
||||
}
|
||||
});
|
||||
|
||||
// Script path text watcher
|
||||
editScriptPath.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (!isUpdatingUI) {
|
||||
config.scriptPath = s.toString().trim();
|
||||
updateJsonPreview();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Gadget name text watcher
|
||||
editGadgetName.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (!isUpdatingUI) {
|
||||
config.gadgetName = s.toString().trim();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// JSON preview text watcher
|
||||
editJsonPreview.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (!isUpdatingUI) {
|
||||
parseJsonAndUpdateUI(s.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateJsonPreview() {
|
||||
if (isUpdatingUI) return;
|
||||
|
||||
try {
|
||||
JSONObject root = new JSONObject();
|
||||
JSONObject interaction = new JSONObject();
|
||||
|
||||
if ("script".equals(config.mode)) {
|
||||
interaction.put("type", "script");
|
||||
interaction.put("path", config.scriptPath);
|
||||
} else {
|
||||
interaction.put("type", "listen");
|
||||
interaction.put("address", config.address);
|
||||
interaction.put("port", config.port);
|
||||
interaction.put("on_port_conflict", config.onPortConflict);
|
||||
interaction.put("on_load", config.onLoad);
|
||||
}
|
||||
|
||||
root.put("interaction", interaction);
|
||||
|
||||
isUpdatingUI = true;
|
||||
editJsonPreview.setText(root.toString(2));
|
||||
isUpdatingUI = false;
|
||||
} catch (JSONException e) {
|
||||
// Should not happen
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void parseJsonAndUpdateUI(String json) {
|
||||
try {
|
||||
JSONObject root = new JSONObject(json);
|
||||
JSONObject interaction = root.getJSONObject("interaction");
|
||||
|
||||
isUpdatingUI = true;
|
||||
|
||||
// Update mode
|
||||
String type = interaction.getString("type");
|
||||
if ("script".equals(type)) {
|
||||
config.mode = "script";
|
||||
radioModeScript.setChecked(true);
|
||||
serverModeLayout.setVisibility(View.GONE);
|
||||
scriptModeLayout.setVisibility(View.VISIBLE);
|
||||
|
||||
// Update script path
|
||||
if (interaction.has("path")) {
|
||||
config.scriptPath = interaction.getString("path");
|
||||
editScriptPath.setText(config.scriptPath);
|
||||
}
|
||||
} else {
|
||||
config.mode = "server";
|
||||
radioModeServer.setChecked(true);
|
||||
serverModeLayout.setVisibility(View.VISIBLE);
|
||||
scriptModeLayout.setVisibility(View.GONE);
|
||||
|
||||
// Update address
|
||||
String address = interaction.getString("address");
|
||||
config.address = address;
|
||||
if ("0.0.0.0".equals(address)) {
|
||||
radioAddressAll.setChecked(true);
|
||||
editCustomAddress.setEnabled(false);
|
||||
} else if ("127.0.0.1".equals(address)) {
|
||||
radioAddressLocal.setChecked(true);
|
||||
editCustomAddress.setEnabled(false);
|
||||
} else {
|
||||
radioAddressCustom.setChecked(true);
|
||||
editCustomAddress.setText(address);
|
||||
editCustomAddress.setEnabled(true);
|
||||
}
|
||||
|
||||
// Update port
|
||||
config.port = interaction.getInt("port");
|
||||
editPort.setText(String.valueOf(config.port));
|
||||
|
||||
// Update port conflict
|
||||
String onPortConflict = interaction.getString("on_port_conflict");
|
||||
config.onPortConflict = onPortConflict;
|
||||
if ("pick-next".equals(onPortConflict)) {
|
||||
radioConflictPickNext.setChecked(true);
|
||||
} else {
|
||||
radioConflictFail.setChecked(true);
|
||||
}
|
||||
|
||||
// Update on load
|
||||
String onLoad = interaction.getString("on_load");
|
||||
config.onLoad = onLoad;
|
||||
if ("resume".equals(onLoad)) {
|
||||
radioLoadResume.setChecked(true);
|
||||
} else {
|
||||
radioLoadWait.setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
isUpdatingUI = false;
|
||||
} catch (JSONException e) {
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void saveConfig() {
|
||||
if (listener != null) {
|
||||
// Ensure gadget name is not empty
|
||||
if (config.gadgetName == null || config.gadgetName.trim().isEmpty()) {
|
||||
config.gadgetName = "libgadget.so";
|
||||
}
|
||||
listener.onGadgetConfigSaved(config);
|
||||
}
|
||||
}
|
||||
|
||||
private void selectScriptFile() {
|
||||
String[] options = {"浏览文件系统", "从外部文件管理器选择", "手动输入路径"};
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("选择 Script 文件")
|
||||
.setItems(options, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
openFileBrowser();
|
||||
} else if (which == 1) {
|
||||
openFilePicker();
|
||||
} else {
|
||||
showPathInputDialog();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void openFileBrowser() {
|
||||
// Show path selection dialog first
|
||||
String[] paths = {
|
||||
"/data/local/tmp",
|
||||
"/sdcard",
|
||||
"/sdcard/Download",
|
||||
"/storage/emulated/0",
|
||||
"自定义路径..."
|
||||
};
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("选择起始目录")
|
||||
.setItems(paths, (dialog, which) -> {
|
||||
if (which == paths.length - 1) {
|
||||
// Custom path
|
||||
showCustomPathDialog();
|
||||
} else {
|
||||
Intent intent = new Intent(getContext(), FileBrowserActivity.class);
|
||||
intent.putExtra(FileBrowserActivity.EXTRA_START_PATH, paths[which]);
|
||||
intent.putExtra(FileBrowserActivity.EXTRA_FILE_FILTER, ".js");
|
||||
fileBrowserLauncher.launch(intent);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showCustomPathDialog() {
|
||||
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
|
||||
android.widget.EditText editText = view.findViewById(android.R.id.edit);
|
||||
editText.setText("/");
|
||||
editText.setHint("输入起始路径");
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("自定义起始路径")
|
||||
.setView(view)
|
||||
.setPositiveButton("确定", (dialog, which) -> {
|
||||
String path = editText.getText().toString().trim();
|
||||
if (!path.isEmpty()) {
|
||||
Intent intent = new Intent(getContext(), FileBrowserActivity.class);
|
||||
intent.putExtra(FileBrowserActivity.EXTRA_START_PATH, path);
|
||||
intent.putExtra(FileBrowserActivity.EXTRA_FILE_FILTER, ".js");
|
||||
fileBrowserLauncher.launch(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void openFilePicker() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
// Add MIME types that might help filter JS files
|
||||
String[] mimeTypes = {"text/javascript", "application/javascript", "text/plain", "*/*"};
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||
// Suggest starting location
|
||||
intent.putExtra("android.provider.extra.INITIAL_URI",
|
||||
android.net.Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload"));
|
||||
filePickerLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void showPathInputDialog() {
|
||||
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
|
||||
android.widget.EditText editText = view.findViewById(android.R.id.edit);
|
||||
editText.setText("/data/local/tmp/");
|
||||
editText.setHint("/data/local/tmp/script.js");
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("输入 Script 文件路径")
|
||||
.setView(view)
|
||||
.setPositiveButton("确定", (dialog, which) -> {
|
||||
String path = editText.getText().toString().trim();
|
||||
if (!path.isEmpty()) {
|
||||
editScriptPath.setText(path);
|
||||
config.scriptPath = path;
|
||||
updateJsonPreview();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
// Show method for non-fragment usage
|
||||
public void show() {
|
||||
if (getContext() == null) {
|
||||
throw new IllegalStateException("Context is required for non-fragment usage");
|
||||
}
|
||||
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_gadget_config, null);
|
||||
initViews(view);
|
||||
|
||||
// Initialize config if null
|
||||
if (config == null) {
|
||||
config = new ConfigManager.GadgetConfig();
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
setupListeners();
|
||||
updateJsonPreview();
|
||||
|
||||
String title = customTitle != null ? customTitle : "Gadget 配置";
|
||||
|
||||
new MaterialAlertDialogBuilder(getContext())
|
||||
.setTitle(title)
|
||||
.setView(view)
|
||||
.setPositiveButton("保存", (dialog, which) -> saveConfig())
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private Context savedContext;
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
Context context = super.getContext();
|
||||
if (context == null) {
|
||||
return savedContext;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// Constructor for non-fragment usage needs to save context
|
||||
public void setContext(Context context) {
|
||||
this.savedContext = context;
|
||||
}
|
||||
|
||||
private String getPathFromUri(Uri uri) {
|
||||
String path = null;
|
||||
|
||||
// Try to get path from MediaStore
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
try {
|
||||
ContentResolver resolver = getContext().getContentResolver();
|
||||
try (Cursor cursor = resolver.query(uri, new String[]{"_data"}, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int columnIndex = cursor.getColumnIndex("_data");
|
||||
if (columnIndex != -1) {
|
||||
path = cursor.getString(columnIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Try DocumentsContract if MediaStore fails
|
||||
if (path == null && DocumentsContract.isDocumentUri(getContext(), uri)) {
|
||||
try {
|
||||
String docId = DocumentsContract.getDocumentId(uri);
|
||||
if (uri.getAuthority().equals("com.android.externalstorage.documents")) {
|
||||
String[] split = docId.split(":");
|
||||
if (split.length >= 2) {
|
||||
String type = split[0];
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
path = "/storage/emulated/0/" + split[1];
|
||||
} else {
|
||||
path = "/storage/" + type + "/" + split[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
} else if ("file".equals(uri.getScheme())) {
|
||||
path = uri.getPath();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,11 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.EditText;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.Editable;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -24,6 +29,10 @@ public class SettingsFragment extends Fragment {
|
||||
private RadioGroup radioGroupFilter;
|
||||
private RadioButton radioShowAll;
|
||||
private RadioButton radioHideSystem;
|
||||
private EditText editInjectionDelay;
|
||||
private TextView tvGlobalGadgetStatus;
|
||||
private Button btnConfigureGlobalGadget;
|
||||
private ConfigManager configManager;
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
private OnSettingsChangeListener settingsChangeListener;
|
||||
@@ -53,6 +62,11 @@ public class SettingsFragment extends Fragment {
|
||||
radioGroupFilter = view.findViewById(R.id.radio_group_filter);
|
||||
radioShowAll = view.findViewById(R.id.radio_show_all);
|
||||
radioHideSystem = view.findViewById(R.id.radio_hide_system);
|
||||
editInjectionDelay = view.findViewById(R.id.editInjectionDelay);
|
||||
tvGlobalGadgetStatus = view.findViewById(R.id.tvGlobalGadgetStatus);
|
||||
btnConfigureGlobalGadget = view.findViewById(R.id.btnConfigureGlobalGadget);
|
||||
|
||||
configManager = new ConfigManager(getContext());
|
||||
}
|
||||
|
||||
private void initSharedPreferences() {
|
||||
@@ -67,6 +81,13 @@ public class SettingsFragment extends Fragment {
|
||||
} else {
|
||||
radioShowAll.setChecked(true);
|
||||
}
|
||||
|
||||
// Load injection delay
|
||||
int injectionDelay = configManager.getInjectionDelay();
|
||||
editInjectionDelay.setText(String.valueOf(injectionDelay));
|
||||
|
||||
// Load global gadget status
|
||||
updateGlobalGadgetStatus();
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
@@ -86,6 +107,37 @@ public class SettingsFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Injection delay listener
|
||||
editInjectionDelay.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
String text = s.toString().trim();
|
||||
if (!text.isEmpty()) {
|
||||
try {
|
||||
int delay = Integer.parseInt(text);
|
||||
// Limit delay between 0 and 60 seconds
|
||||
if (delay < 0) delay = 0;
|
||||
if (delay > 60) delay = 60;
|
||||
|
||||
configManager.setInjectionDelay(delay);
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore invalid input
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Global gadget configuration button
|
||||
btnConfigureGlobalGadget.setOnClickListener(v -> {
|
||||
showGlobalGadgetConfigDialog();
|
||||
});
|
||||
}
|
||||
|
||||
public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
|
||||
@@ -95,4 +147,34 @@ public class SettingsFragment extends Fragment {
|
||||
public boolean isHideSystemApps() {
|
||||
return sharedPreferences.getBoolean(KEY_HIDE_SYSTEM_APPS, false);
|
||||
}
|
||||
|
||||
private void updateGlobalGadgetStatus() {
|
||||
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
|
||||
if (globalGadget != null) {
|
||||
String status = "已配置: " + globalGadget.gadgetName;
|
||||
if (globalGadget.mode.equals("server")) {
|
||||
status += " (Server模式, 端口: " + globalGadget.port + ")";
|
||||
} else {
|
||||
status += " (Script模式)";
|
||||
}
|
||||
tvGlobalGadgetStatus.setText(status);
|
||||
} else {
|
||||
tvGlobalGadgetStatus.setText("未配置");
|
||||
}
|
||||
}
|
||||
|
||||
private void showGlobalGadgetConfigDialog() {
|
||||
// Use existing GadgetConfigDialog
|
||||
GadgetConfigDialog dialog = new GadgetConfigDialog(
|
||||
getContext(),
|
||||
"全局Gadget配置",
|
||||
configManager.getGlobalGadgetConfig(),
|
||||
gadgetConfig -> {
|
||||
// Save global gadget configuration
|
||||
configManager.setGlobalGadgetConfig(gadgetConfig);
|
||||
updateGlobalGadgetStatus();
|
||||
}
|
||||
);
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +196,12 @@ public class SoManagerFragment extends Fragment {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
// Add MIME types that might help filter SO files
|
||||
String[] mimeTypes = {"application/octet-stream", "application/x-sharedlib", "*/*"};
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||
// Suggest starting location
|
||||
intent.putExtra("android.provider.extra.INITIAL_URI",
|
||||
android.net.Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload"));
|
||||
filePickerLauncher.launch(intent);
|
||||
}
|
||||
|
||||
@@ -219,14 +225,12 @@ public class SoManagerFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void handleFileSelection(Uri uri) {
|
||||
// Get real path from URI
|
||||
String path = uri.getPath();
|
||||
// Get real path from URI using proper URI handling
|
||||
String path = FileUtils.getRealPathFromUri(requireContext(), uri);
|
||||
if (path != null) {
|
||||
// Remove the file:// prefix if present
|
||||
if (path.startsWith("file://")) {
|
||||
path = path.substring(7);
|
||||
}
|
||||
showDeleteOriginalDialog(path);
|
||||
} else {
|
||||
Toast.makeText(getContext(), "无法获取文件路径,请尝试其他方式", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -64,7 +68,7 @@
|
||||
android:id="@+id/soListRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxHeight="200dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
@@ -77,13 +81,84 @@
|
||||
android:textColor="?android:attr/textColorTertiary"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gadget 配置"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/gadgetConfigGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioNoGadget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="不使用Gadget"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioUseGlobalGadget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="使用全局Gadget配置" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvGlobalGadgetInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="未配置全局Gadget"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioUseCustomGadget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="自定义Gadget配置" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnConfigureGadget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="配置Gadget"
|
||||
android:textSize="14sp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:enabled="false"
|
||||
android:visibility="gone"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gadget可用于Frida调试,可配置监听地址和端口"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="注入方式"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
@@ -138,5 +213,7 @@
|
||||
android:layout_marginStart="32dp" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
299
configapp/src/main/res/layout/dialog_gadget_config.xml
Normal file
299
configapp/src/main/res/layout/dialog_gadget_config.xml
Normal file
@@ -0,0 +1,299 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gadget 配置"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="提示:请确保对应的 gadget SO 文件已添加到 SO 库中"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- 模式选择 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gadget 模式"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/modeRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioModeServer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Server 模式"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioModeScript"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Script 模式" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<!-- Server 模式配置区域 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/serverModeLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 监听地址配置 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="监听地址"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/addressRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioAddressAll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0.0.0.0 (监听所有接口)"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioAddressLocal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="127.0.0.1 (仅本地)" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioAddressCustom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="自定义" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editCustomAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="输入自定义地址"
|
||||
android:inputType="text"
|
||||
android:enabled="false"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- 端口配置 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="监听端口"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editPort"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="27042"
|
||||
android:hint="1-65535"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- 端口冲突处理 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="端口冲突处理"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/portConflictRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioConflictFail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="fail (启动失败)"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioConflictPickNext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="pick-next (尝试下一个端口)" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<!-- 加载处理方式 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="加载时处理方式"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/onLoadRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioLoadWait"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="wait (等待连接)"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioLoadResume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="resume (立即继续)" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Script 模式配置区域 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/scriptModeLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Script 文件路径"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editScriptPath"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="/data/local/tmp/script.js"
|
||||
android:inputType="text"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSelectScript"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="选择"
|
||||
android:textSize="12sp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="提示:Script 模式下,Gadget 会在程序入口点执行前自动加载并运行指定的脚本文件"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Gadget 名称 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gadget 文件名"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editGadgetName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="libgadget.so"
|
||||
android:hint="例如: libgadget.so"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- JSON 预览区域 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="配置预览 (可直接编辑)"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardElevation="2dp"
|
||||
app:cardCornerRadius="4dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editJsonPreview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="150dp"
|
||||
android:gravity="top"
|
||||
android:padding="12dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="12sp"
|
||||
android:inputType="textMultiLine"
|
||||
android:scrollbars="vertical"
|
||||
android:overScrollMode="always" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@@ -72,6 +72,124 @@
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 注入延迟时间设置 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="注入延迟时间"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="设置SO文件注入前的等待时间,以确保应用完全初始化"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="延迟时间(秒):"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editInjectionDelay"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="2"
|
||||
android:textAlignment="center"
|
||||
android:hint="0-60"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="秒"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="建议值:2-5秒。某些应用可能需要更长时间。"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 全局Gadget配置 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="全局Gadget配置"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="配置全局默认的Gadget设置,应用可以选择使用或覆盖"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvGlobalGadgetStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="未配置"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnConfigureGlobalGadget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="配置全局Gadget"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 其他设置可以在这里添加 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -31,6 +31,13 @@ namespace Config {
|
||||
} else if (json[valueStart] == 't' || json[valueStart] == 'f') {
|
||||
// Boolean value
|
||||
return (json.substr(valueStart, 4) == "true") ? "true" : "false";
|
||||
} else {
|
||||
// Number value
|
||||
size_t valueEnd = json.find_first_of(",} \t\n", valueStart);
|
||||
if (valueEnd == std::string::npos) {
|
||||
return json.substr(valueStart);
|
||||
}
|
||||
return json.substr(valueStart, valueEnd - valueStart);
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -85,11 +92,44 @@ namespace Config {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse gadgetConfig if exists
|
||||
size_t gadgetPos = appJson.find("\"gadgetConfig\"");
|
||||
if (gadgetPos != std::string::npos) {
|
||||
size_t gadgetObjStart = appJson.find("{", gadgetPos);
|
||||
size_t gadgetObjEnd = appJson.find("}", gadgetObjStart);
|
||||
|
||||
if (gadgetObjStart != std::string::npos && gadgetObjEnd != std::string::npos) {
|
||||
std::string gadgetObj = appJson.substr(gadgetObjStart, gadgetObjEnd - gadgetObjStart + 1);
|
||||
|
||||
GadgetConfig* gadgetConfig = new GadgetConfig();
|
||||
|
||||
std::string address = extractValue(gadgetObj, "address");
|
||||
if (!address.empty()) gadgetConfig->address = address;
|
||||
|
||||
std::string portStr = extractValue(gadgetObj, "port");
|
||||
if (!portStr.empty()) gadgetConfig->port = std::stoi(portStr);
|
||||
|
||||
std::string onPortConflict = extractValue(gadgetObj, "onPortConflict");
|
||||
if (!onPortConflict.empty()) gadgetConfig->onPortConflict = onPortConflict;
|
||||
|
||||
std::string onLoad = extractValue(gadgetObj, "onLoad");
|
||||
if (!onLoad.empty()) gadgetConfig->onLoad = onLoad;
|
||||
|
||||
std::string gadgetName = extractValue(gadgetObj, "gadgetName");
|
||||
if (!gadgetName.empty()) gadgetConfig->gadgetName = gadgetName;
|
||||
|
||||
appConfig.gadgetConfig = gadgetConfig;
|
||||
LOGD("Loaded gadget config: %s:%d, name: %s",
|
||||
gadgetConfig->address.c_str(), gadgetConfig->port, gadgetConfig->gadgetName.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
g_config.perAppConfig[packageName] = appConfig;
|
||||
const char* methodName = appConfig.injectionMethod == InjectionMethod::CUSTOM_LINKER ? "custom_linker" :
|
||||
appConfig.injectionMethod == InjectionMethod::RIRU ? "riru" : "standard";
|
||||
LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu",
|
||||
packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size());
|
||||
LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu, gadget: %s",
|
||||
packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size(),
|
||||
appConfig.gadgetConfig ? "yes" : "no");
|
||||
}
|
||||
|
||||
ModuleConfig readConfig() {
|
||||
@@ -118,7 +158,13 @@ namespace Config {
|
||||
std::string hideStr = extractValue(json, "hideInjection");
|
||||
g_config.hideInjection = (hideStr == "true");
|
||||
|
||||
LOGD("Module enabled: %d, hide injection: %d", g_config.enabled, g_config.hideInjection);
|
||||
std::string delayStr = extractValue(json, "injectionDelay");
|
||||
if (!delayStr.empty()) {
|
||||
g_config.injectionDelay = std::stoi(delayStr);
|
||||
}
|
||||
|
||||
LOGD("Module enabled: %d, hide injection: %d, injection delay: %d",
|
||||
g_config.enabled, g_config.hideInjection, g_config.injectionDelay);
|
||||
|
||||
// Parse perAppConfig
|
||||
size_t perAppPos = json.find("\"perAppConfig\"");
|
||||
@@ -212,4 +258,11 @@ namespace Config {
|
||||
}
|
||||
return InjectionMethod::STANDARD;
|
||||
}
|
||||
|
||||
int getInjectionDelay() {
|
||||
if (!g_configLoaded) {
|
||||
readConfig();
|
||||
}
|
||||
return g_config.injectionDelay;
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,25 @@ namespace Config {
|
||||
CUSTOM_LINKER = 2
|
||||
};
|
||||
|
||||
struct GadgetConfig {
|
||||
std::string address = "0.0.0.0";
|
||||
int port = 27042;
|
||||
std::string onPortConflict = "fail";
|
||||
std::string onLoad = "wait";
|
||||
std::string gadgetName = "libgadget.so";
|
||||
};
|
||||
|
||||
struct AppConfig {
|
||||
bool enabled = false;
|
||||
InjectionMethod injectionMethod = InjectionMethod::STANDARD;
|
||||
std::vector<SoFile> soFiles;
|
||||
GadgetConfig* gadgetConfig = nullptr;
|
||||
};
|
||||
|
||||
struct ModuleConfig {
|
||||
bool enabled = true;
|
||||
bool hideInjection = false;
|
||||
int injectionDelay = 2; // Default 2 seconds
|
||||
std::unordered_map<std::string, AppConfig> perAppConfig;
|
||||
};
|
||||
|
||||
@@ -45,6 +55,9 @@ namespace Config {
|
||||
|
||||
// Get injection method for specific app
|
||||
InjectionMethod getAppInjectionMethod(const std::string& packageName);
|
||||
|
||||
// Get injection delay in seconds
|
||||
int getInjectionDelay();
|
||||
}
|
||||
|
||||
#endif // CONFIG_H
|
||||
@@ -88,8 +88,8 @@ void load_so_file_custom_linker(const char *game_data_dir, const Config::SoFile
|
||||
void hack_thread_func(const char *game_data_dir, const char *package_name, JavaVM *vm) {
|
||||
LOGI("Hack thread started for package: %s", package_name);
|
||||
|
||||
// Wait a bit for app to initialize and files to be copied
|
||||
sleep(2);
|
||||
// Note: Delay is now handled in main thread before this thread is created
|
||||
LOGI("Starting injection immediately (delay already applied in main thread)");
|
||||
|
||||
// Get injection method for this app
|
||||
Config::InjectionMethod method = Config::getAppInjectionMethod(package_name);
|
||||
@@ -103,6 +103,12 @@ void hack_thread_func(const char *game_data_dir, const char *package_name, JavaV
|
||||
|
||||
// Load each SO file using the configured method
|
||||
for (const auto &soFile : soFiles) {
|
||||
// Skip config files
|
||||
if (soFile.name.find(".config.so") != std::string::npos) {
|
||||
LOGI("Skipping config file: %s", soFile.name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGI("Loading SO: %s (stored as: %s)", soFile.name.c_str(), soFile.storedPath.c_str());
|
||||
|
||||
if (method == Config::InjectionMethod::CUSTOM_LINKER) {
|
||||
@@ -125,5 +131,5 @@ void hack_prepare(const char *game_data_dir, const char *package_name, void *dat
|
||||
LOGI("hack_prepare called for package: %s, dir: %s", package_name, game_data_dir);
|
||||
|
||||
std::thread hack_thread(hack_thread_func, game_data_dir, package_name, vm);
|
||||
hack_thread.detach();
|
||||
hack_thread.join();
|
||||
}
|
||||
@@ -45,6 +45,13 @@ public:
|
||||
// Get JavaVM
|
||||
JavaVM *vm = nullptr;
|
||||
if (env->GetJavaVM(&vm) == JNI_OK) {
|
||||
// Get injection delay from config
|
||||
int delay = Config::getInjectionDelay();
|
||||
LOGI("Main thread blocking for %d seconds before injection", delay);
|
||||
|
||||
// Block main thread for the delay period
|
||||
sleep(delay);
|
||||
|
||||
// Then start hack thread with JavaVM
|
||||
std::thread hack_thread(hack_prepare, _data_dir, _package_name, data, length, vm);
|
||||
hack_thread.detach();
|
||||
|
||||
@@ -17,8 +17,8 @@ set(SOURCES
|
||||
|
||||
find_library(log-lib log)
|
||||
|
||||
# Build as shared library
|
||||
add_library(mylinker SHARED ${SOURCES})
|
||||
# Build as static library to be linked into main module
|
||||
add_library(mylinker STATIC ${SOURCES})
|
||||
|
||||
target_link_libraries(mylinker ${log-lib})
|
||||
|
||||
|
||||
4
scripts/.gitignore
vendored
Normal file
4
scripts/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Temporary config files
|
||||
.tmp/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
246
scripts/README.md
Normal file
246
scripts/README.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# Auto Config Tool for Zygisk-MyInjector
|
||||
|
||||
自动配置工具,通过交互式命令行快速配置应用注入。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🎯 交互式设备选择(支持多设备)
|
||||
- 🔍 Tab 自动补全选择应用包名(支持模糊搜索)
|
||||
- ⚙️ 交互式 Gadget 配置(Server/Script 模式)
|
||||
- 📦 自动下载 Frida Gadget 16.0.7(自动检测设备架构)
|
||||
- 📦 自动生成配置文件
|
||||
- 🚀 一键推送并应用配置
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
pip install prompt_toolkit
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本用法
|
||||
|
||||
```bash
|
||||
./auto_config.py
|
||||
```
|
||||
|
||||
### 工作流程
|
||||
|
||||
1. **设备选择**
|
||||
- 自动检测连接的设备
|
||||
- 单设备时自动选择
|
||||
- 多设备时交互式选择
|
||||
|
||||
2. **应用选择**
|
||||
- 可选是否包含系统应用(默认仅第三方应用)
|
||||
- 使用 Tab 键自动补全包名
|
||||
- 支持模糊搜索过滤
|
||||
|
||||
3. **Gadget 配置**
|
||||
- 指定 Gadget 名称(默认 libgadget.so)
|
||||
- **Server 模式**:监听端口等待 Frida 连接
|
||||
- 监听地址(默认 0.0.0.0)
|
||||
- 监听端口(默认 27042)
|
||||
- 端口冲突处理(fail/ignore/close,默认 fail)
|
||||
- 加载行为(resume/wait,默认 resume)
|
||||
|
||||
- **Script 模式**:执行本地脚本
|
||||
- 脚本路径(默认 /data/local/tmp/script.js)
|
||||
|
||||
4. **Frida Gadget 检查与安装**
|
||||
- 自动检查设备上是否已安装 Gadget
|
||||
- 如未安装,自动下载 Frida Gadget 16.0.7
|
||||
- 根据设备架构选择正确版本(arm64/arm/x86/x86_64)
|
||||
- 自动解压并安装到模块 SO 库
|
||||
|
||||
5. **配置部署**
|
||||
- 自动生成 config.json 和 gadget 配置文件
|
||||
- 推送到设备 /data/local/tmp
|
||||
- 发送广播触发应用配置
|
||||
|
||||
## 示例会话
|
||||
|
||||
```
|
||||
============================================================
|
||||
Zygisk-MyInjector Auto Config Tool
|
||||
============================================================
|
||||
|
||||
Using device: 192.168.1.100:5555 (OnePlus)
|
||||
|
||||
=== Loading app packages ===
|
||||
Include system apps? (y/N): n
|
||||
Found 156 packages
|
||||
|
||||
=== Select Target App ===
|
||||
Tip: Use Tab for auto-completion, type to filter
|
||||
Package name: com.example.app
|
||||
Selected: com.example.app
|
||||
|
||||
=== Gadget Configuration ===
|
||||
Gadget SO name (default: libgadget.so):
|
||||
libgadget.so
|
||||
|
||||
Select mode:
|
||||
1. Server mode (listen for connections)
|
||||
2. Script mode (execute script)
|
||||
Mode (1/2, default: 1): 1
|
||||
|
||||
Listen address (default: 0.0.0.0):
|
||||
Listen port (default: 27042):
|
||||
|
||||
Port conflict behavior:
|
||||
1. fail - Exit if port is in use
|
||||
2. ignore - Continue anyway
|
||||
3. close - Close existing connection
|
||||
On port conflict (1/2/3, default: 1): 1
|
||||
|
||||
On load behavior:
|
||||
1. resume - Continue immediately (recommended)
|
||||
2. wait - Wait for connection (for debugging)
|
||||
On load (1/2, default: 1): 1
|
||||
|
||||
=== Generating Configuration Files ===
|
||||
...
|
||||
|
||||
✓ Configuration applied successfully!
|
||||
|
||||
The app 'com.example.app' has been configured.
|
||||
You can now use Frida to connect to the app:
|
||||
frida -H 0.0.0.0:27042 -n <process-name>
|
||||
```
|
||||
|
||||
## 生成的文件
|
||||
|
||||
脚本会生成以下文件:
|
||||
|
||||
1. **config.json**
|
||||
- 模块主配置文件
|
||||
- 存储位置:`/data/adb/modules/zygisk-myinjector/config.json`
|
||||
|
||||
2. **gadget 配置文件**
|
||||
- 格式:`libgadget.config.so`
|
||||
- 存储位置:`/data/data/<package>/files/libgadget.config.so`
|
||||
|
||||
## 广播接收器
|
||||
|
||||
配置通过广播接收器应用:
|
||||
|
||||
**注意**:ConfigApplyReceiver 现在使用**动态注册**方式,第三方 app 无法通过 PackageManager 发现其存在。
|
||||
同时增加了 UID 权限检查,只允许 shell (2000) 或 root (0) 发送广播。
|
||||
|
||||
```bash
|
||||
# 手动发送广播示例
|
||||
adb shell am broadcast \
|
||||
-n com.jiqiu.configapp/.ConfigApplyReceiver \
|
||||
-a com.jiqiu.configapp.APPLY_CONFIG \
|
||||
--es package_name "com.example.app" \
|
||||
--es tmp_config_path "/data/local/tmp/zygisk_config.json" \
|
||||
--es tmp_gadget_config_path "/data/local/tmp/libgadget.config.so"
|
||||
```
|
||||
|
||||
## 调试
|
||||
|
||||
查看日志:
|
||||
|
||||
```bash
|
||||
adb logcat -s ConfigApplyReceiver:* ConfigManager:*
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### prompt_toolkit 未安装
|
||||
|
||||
```
|
||||
Error: prompt_toolkit is required. Install it with:
|
||||
pip install prompt_toolkit
|
||||
```
|
||||
|
||||
**解决方案**:运行 `pip install prompt_toolkit`
|
||||
|
||||
### 没有设备连接
|
||||
|
||||
```
|
||||
Error: No devices found. Please connect a device and enable USB debugging.
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
1. 通过 USB 或 WiFi 连接设备
|
||||
2. 确保已启用 USB 调试
|
||||
3. 运行 `adb devices` 确认设备已连接
|
||||
|
||||
### 广播发送失败
|
||||
|
||||
**解决方案**:
|
||||
1. 确保 configapp 已安装
|
||||
2. 确保设备已 root
|
||||
3. 检查 logcat 日志获取详细错误信息
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 仅生成配置不部署
|
||||
|
||||
修改广播命令添加 `deploy_only` 参数:
|
||||
|
||||
```bash
|
||||
adb shell am broadcast \
|
||||
-n com.jiqiu.configapp/.ConfigApplyReceiver \
|
||||
-a com.jiqiu.configapp.APPLY_CONFIG \
|
||||
--es package_name "com.example.app" \
|
||||
--es tmp_config_path "/data/local/tmp/zygisk_config.json" \
|
||||
--ez deploy_only true
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. ⚠️ 设备必须已 root
|
||||
2. ⚠️ configapp 必须已安装
|
||||
3. ✓ **SELinux 会自动检查并设置为 Permissive 模式**(脚本会自动处理)
|
||||
4. ⚠️ 首次使用会自动下载 Frida Gadget 16.0.7(需要网络连接)
|
||||
5. ⚠️ 配置完成后需要重启目标应用才能生效
|
||||
6. ⚠️ 需要安装 `xz` 工具用于解压(macOS 通过 `brew install xz` 安装)
|
||||
|
||||
### SELinux 问题
|
||||
|
||||
Zygisk 模块需要读取 `/data/adb/modules/zygisk-myinjector/config.json`,但 SELinux 默认会阻止 zygote 进程访问。
|
||||
|
||||
**自动处理**:
|
||||
`auto_config.py` 脚本会自动检查 SELinux 状态,如果处于 Enforcing 模式会提示设置为 Permissive。
|
||||
|
||||
**手动设置**(重启后需重新设置):
|
||||
```bash
|
||||
adb shell "su -c 'setenforce 0'"
|
||||
```
|
||||
|
||||
**永久解决方案**(需要 Magisk 模块):
|
||||
创建 SELinux 策略或修改模块实现方式。
|
||||
|
||||
## 完整工作流程
|
||||
|
||||
```bash
|
||||
# 1. 运行自动配置脚本(会自动检查并设置 SELinux)
|
||||
cd scripts
|
||||
./auto_config.py
|
||||
|
||||
# 2. 按提示选择设备、应用和配置(全部使用默认值即可)
|
||||
# 脚本会自动完成:
|
||||
# - 生成配置文件
|
||||
# - 推送到设备
|
||||
# - 应用配置
|
||||
# - 重启应用
|
||||
# - 端口转发
|
||||
# - 快速测试
|
||||
|
||||
# 3. 如果测试成功,直接使用 Frida 连接
|
||||
frida -H 127.0.0.1:27042 Gadget -l your_script.js
|
||||
```
|
||||
|
||||
### 快速测试示例
|
||||
|
||||
```bash
|
||||
# 测试连接
|
||||
frida -H 127.0.0.1:27042 Gadget -e "console.log('Connected! PID:', Process.id)"
|
||||
|
||||
# 列举模块
|
||||
frida -H 127.0.0.1:27042 Gadget -e "Process.enumerateModules().forEach(m => console.log(m.name))"
|
||||
```
|
||||
841
scripts/auto_config.py
Executable file
841
scripts/auto_config.py
Executable file
@@ -0,0 +1,841 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Auto Config Script for Zygisk-MyInjector
|
||||
通过交互式命令行快速配置应用注入
|
||||
|
||||
完整工作流程:
|
||||
1. 运行自动配置脚本(会自动检查并设置 SELinux)
|
||||
cd scripts
|
||||
./auto_config.py
|
||||
|
||||
2. 按提示选择设备、应用和配置(全部使用默认值即可)
|
||||
脚本会自动完成:
|
||||
- 生成配置文件
|
||||
- 推送到设备
|
||||
- 应用配置
|
||||
- 重启应用
|
||||
- 端口转发
|
||||
- 快速测试
|
||||
|
||||
3. 如果测试成功,直接使用 Frida 连接
|
||||
frida -H 127.0.0.1:27042 Gadget -l your_script.js
|
||||
|
||||
快速测试:
|
||||
# 测试连接
|
||||
frida -H 127.0.0.1:27042 Gadget -e "console.log('Connected! PID:', Process.id)"
|
||||
|
||||
# 列举模块
|
||||
frida -H 127.0.0.1:27042 Gadget -e "Process.enumerateModules().forEach(m => console.log(m.name))"
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import argparse
|
||||
from typing import List, Dict, Optional
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from prompt_toolkit import prompt
|
||||
from prompt_toolkit.completion import FuzzyWordCompleter
|
||||
from prompt_toolkit.styles import Style
|
||||
except ImportError:
|
||||
print("Error: prompt_toolkit is required. Install it with:")
|
||||
print(" pip install prompt_toolkit")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Define style for prompts
|
||||
style = Style.from_dict({
|
||||
'prompt': 'ansicyan bold',
|
||||
})
|
||||
|
||||
# Frida Gadget version
|
||||
FRIDA_VERSION = "16.0.7"
|
||||
MODULE_PATH = "/data/adb/modules/zygisk-myinjector"
|
||||
SO_STORAGE_DIR = f"{MODULE_PATH}/so_files"
|
||||
|
||||
# Default ports
|
||||
DEFAULT_PORTS = [27042, 65320]
|
||||
|
||||
# Local cache directory for downloaded gadgets
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
CACHE_DIR = SCRIPT_DIR / '.cache' / 'frida-gadgets'
|
||||
|
||||
|
||||
class ADBHelper:
|
||||
"""ADB helper class for device and package operations"""
|
||||
|
||||
def __init__(self, device_id: Optional[str] = None):
|
||||
self.device_id = device_id
|
||||
self.base_cmd = ['adb']
|
||||
if device_id:
|
||||
self.base_cmd.extend(['-s', device_id])
|
||||
|
||||
def run(self, args: List[str], check=True) -> subprocess.CompletedProcess:
|
||||
"""Run adb command"""
|
||||
cmd = self.base_cmd + args
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=check)
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error running command: {' '.join(cmd)}")
|
||||
print(f"Error: {e.stderr}")
|
||||
if check:
|
||||
raise
|
||||
return e
|
||||
|
||||
@staticmethod
|
||||
def get_devices() -> List[Dict[str, str]]:
|
||||
"""Get list of connected devices"""
|
||||
result = subprocess.run(['adb', 'devices', '-l'], capture_output=True, text=True)
|
||||
lines = result.stdout.strip().split('\n')[1:] # Skip header
|
||||
devices = []
|
||||
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
device_id = parts[0]
|
||||
status = parts[1]
|
||||
|
||||
# Parse device info
|
||||
model = ''
|
||||
product = ''
|
||||
for part in parts[2:]:
|
||||
if part.startswith('model:'):
|
||||
model = part.split(':', 1)[1]
|
||||
elif part.startswith('product:'):
|
||||
product = part.split(':', 1)[1]
|
||||
|
||||
devices.append({
|
||||
'id': device_id,
|
||||
'status': status,
|
||||
'model': model,
|
||||
'product': product
|
||||
})
|
||||
|
||||
return devices
|
||||
|
||||
def get_packages(self, include_system: bool = False) -> List[str]:
|
||||
"""Get list of installed packages"""
|
||||
args = ['shell', 'pm', 'list', 'packages']
|
||||
if not include_system:
|
||||
args.append('-3') # Third-party apps only
|
||||
|
||||
result = self.run(args)
|
||||
packages = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line.startswith('package:'):
|
||||
pkg = line.split(':', 1)[1].strip()
|
||||
packages.append(pkg)
|
||||
|
||||
return sorted(packages)
|
||||
|
||||
def push_file(self, local_path: str, remote_path: str) -> bool:
|
||||
"""Push file to device"""
|
||||
result = self.run(['push', local_path, remote_path], check=False)
|
||||
return result.returncode == 0
|
||||
|
||||
def send_broadcast(self, action: str, component: str, extras: Dict[str, str]) -> bool:
|
||||
"""Send broadcast with extras"""
|
||||
args = ['shell', 'am', 'broadcast', '-n', component, '-a', action]
|
||||
|
||||
for key, value in extras.items():
|
||||
args.extend(['--es', key, value])
|
||||
|
||||
result = self.run(args, check=False)
|
||||
if result.returncode == 0:
|
||||
print(f"Broadcast sent successfully")
|
||||
print(result.stdout)
|
||||
return True
|
||||
else:
|
||||
print(f"Failed to send broadcast")
|
||||
print(result.stderr)
|
||||
return False
|
||||
|
||||
def get_arch(self) -> str:
|
||||
"""Get device CPU architecture"""
|
||||
result = self.run(['shell', 'getprop', 'ro.product.cpu.abi'])
|
||||
arch = result.stdout.strip()
|
||||
return arch
|
||||
|
||||
def file_exists(self, path: str) -> bool:
|
||||
"""Check if file exists on device"""
|
||||
result = self.run(['shell', f'su -c "test -f {path} && echo exists"'], check=False)
|
||||
return 'exists' in result.stdout
|
||||
|
||||
|
||||
def select_device() -> Optional[str]:
|
||||
"""Interactive device selection"""
|
||||
devices = ADBHelper.get_devices()
|
||||
|
||||
if not devices:
|
||||
print("Error: No devices found. Please connect a device and enable USB debugging.")
|
||||
return None
|
||||
|
||||
if len(devices) == 1:
|
||||
device = devices[0]
|
||||
print(f"Using device: {device['id']} ({device['model'] or device['product']})")
|
||||
return device['id']
|
||||
|
||||
print("\n=== Connected Devices ===")
|
||||
for idx, device in enumerate(devices, 1):
|
||||
model_info = device['model'] or device['product'] or 'Unknown'
|
||||
print(f"{idx}. {device['id']} - {model_info} [{device['status']}]")
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = input(f"\nSelect device (1-{len(devices)}): ").strip()
|
||||
idx = int(choice) - 1
|
||||
if 0 <= idx < len(devices):
|
||||
selected = devices[idx]
|
||||
print(f"Selected: {selected['id']} ({selected['model'] or selected['product']})")
|
||||
return selected['id']
|
||||
else:
|
||||
print(f"Invalid choice. Please enter 1-{len(devices)}")
|
||||
except (ValueError, KeyboardInterrupt):
|
||||
print("\nDevice selection cancelled")
|
||||
return None
|
||||
|
||||
|
||||
def select_package(adb: ADBHelper) -> Optional[str]:
|
||||
"""Interactive package selection with fuzzy completion"""
|
||||
print("\n=== Loading app packages ===")
|
||||
|
||||
# Ask if include system apps
|
||||
while True:
|
||||
choice = input("Include system apps? (y/N): ").strip().lower()
|
||||
if choice in ['', 'n', 'no']:
|
||||
include_system = False
|
||||
break
|
||||
elif choice in ['y', 'yes']:
|
||||
include_system = True
|
||||
break
|
||||
else:
|
||||
print("Please enter 'y' or 'n'")
|
||||
|
||||
packages = adb.get_packages(include_system=include_system)
|
||||
|
||||
if not packages:
|
||||
print("Error: No packages found")
|
||||
return None
|
||||
|
||||
print(f"Found {len(packages)} packages")
|
||||
|
||||
# Create fuzzy completer
|
||||
completer = FuzzyWordCompleter(packages)
|
||||
|
||||
print("\n=== Select Target App ===")
|
||||
print("Tip: Use Tab for auto-completion, type to filter")
|
||||
|
||||
try:
|
||||
package = prompt(
|
||||
'Package name: ',
|
||||
completer=completer,
|
||||
style=style
|
||||
).strip()
|
||||
|
||||
if package and package in packages:
|
||||
print(f"Selected: {package}")
|
||||
return package
|
||||
elif package:
|
||||
print(f"Warning: '{package}' not found in package list, using anyway")
|
||||
return package
|
||||
else:
|
||||
print("No package selected")
|
||||
return None
|
||||
except KeyboardInterrupt:
|
||||
print("\nSelection cancelled")
|
||||
return None
|
||||
|
||||
|
||||
def configure_gadget(preset_port: Optional[int] = None) -> Optional[Dict]:
|
||||
"""Interactive gadget configuration"""
|
||||
print("\n=== Gadget Configuration ===")
|
||||
|
||||
gadget_config = {}
|
||||
|
||||
# Gadget name
|
||||
gadget_name = input("Gadget SO name (default: libgadget.so): ").strip()
|
||||
gadget_config['gadgetName'] = gadget_name or 'libgadget.so'
|
||||
|
||||
# Mode selection
|
||||
print("\nSelect mode:")
|
||||
print("1. Server mode (listen for connections)")
|
||||
print("2. Script mode (execute script)")
|
||||
|
||||
while True:
|
||||
choice = input("Mode (1/2, default: 1): ").strip()
|
||||
if choice in ['', '1']:
|
||||
gadget_config['mode'] = 'server'
|
||||
break
|
||||
elif choice == '2':
|
||||
gadget_config['mode'] = 'script'
|
||||
break
|
||||
else:
|
||||
print("Invalid choice. Please enter 1 or 2")
|
||||
|
||||
if gadget_config['mode'] == 'server':
|
||||
# Server mode configuration
|
||||
address = input("Listen address (default: 0.0.0.0): ").strip()
|
||||
gadget_config['address'] = address or '0.0.0.0'
|
||||
|
||||
# Use preset port if provided
|
||||
if preset_port:
|
||||
gadget_config['port'] = preset_port
|
||||
print(f"\nUsing preset port: {preset_port}")
|
||||
else:
|
||||
print(f"\nAvailable ports: {', '.join(map(str, DEFAULT_PORTS))}")
|
||||
port = input(f"Listen port (default: {DEFAULT_PORTS[0]}): ").strip()
|
||||
try:
|
||||
gadget_config['port'] = int(port) if port else DEFAULT_PORTS[0]
|
||||
except ValueError:
|
||||
print(f"Invalid port, using default {DEFAULT_PORTS[0]}")
|
||||
gadget_config['port'] = DEFAULT_PORTS[0]
|
||||
|
||||
print("\nPort conflict behavior:")
|
||||
print("1. fail - Exit if port is in use")
|
||||
print("2. ignore - Continue anyway")
|
||||
print("3. close - Close existing connection")
|
||||
conflict = input("On port conflict (1/2/3, default: 1): ").strip()
|
||||
conflict_map = {'1': 'fail', '2': 'ignore', '3': 'close', '': 'fail'}
|
||||
gadget_config['onPortConflict'] = conflict_map.get(conflict, 'fail')
|
||||
|
||||
print("\nOn load behavior:")
|
||||
print("1. resume - Continue immediately (recommended)")
|
||||
print("2. wait - Wait for connection (for debugging)")
|
||||
load = input("On load (1/2, default: 1): ").strip()
|
||||
load_map = {'1': 'resume', '2': 'wait', '': 'resume'}
|
||||
gadget_config['onLoad'] = load_map.get(load, 'resume')
|
||||
|
||||
else:
|
||||
# Script mode configuration
|
||||
script_path = input("Script path (default: /data/local/tmp/script.js): ").strip()
|
||||
gadget_config['scriptPath'] = script_path or '/data/local/tmp/script.js'
|
||||
|
||||
return gadget_config
|
||||
|
||||
|
||||
def download_frida_gadget(arch: str) -> Optional[str]:
|
||||
"""Download Frida Gadget for specified architecture (with local caching)"""
|
||||
# Map Android arch to Frida naming
|
||||
arch_map = {
|
||||
'arm64-v8a': 'arm64',
|
||||
'armeabi-v7a': 'arm',
|
||||
'x86': 'x86',
|
||||
'x86_64': 'x86_64'
|
||||
}
|
||||
|
||||
frida_arch = arch_map.get(arch)
|
||||
if not frida_arch:
|
||||
print(f"Unsupported architecture: {arch}")
|
||||
return None
|
||||
|
||||
# Check local cache first
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
cached_file = CACHE_DIR / f"frida-gadget-{FRIDA_VERSION}-android-{frida_arch}.so"
|
||||
|
||||
if cached_file.exists():
|
||||
print(f"\n✓ Using cached Frida Gadget {FRIDA_VERSION} for {arch}")
|
||||
print(f" Cache: {cached_file}")
|
||||
# Copy to temp location for consistent behavior
|
||||
temp_dir = tempfile.mkdtemp(prefix='frida_gadget_')
|
||||
temp_file = os.path.join(temp_dir, 'libgadget.so')
|
||||
shutil.copy2(str(cached_file), temp_file)
|
||||
return temp_file
|
||||
|
||||
# Download if not in cache
|
||||
url = f"https://github.com/frida/frida/releases/download/{FRIDA_VERSION}/frida-gadget-{FRIDA_VERSION}-android-{frida_arch}.so.xz"
|
||||
|
||||
print(f"\nDownloading Frida Gadget {FRIDA_VERSION} for {arch}...")
|
||||
print(f"URL: {url}")
|
||||
|
||||
# Create temp directory
|
||||
temp_dir = tempfile.mkdtemp(prefix='frida_gadget_')
|
||||
xz_file = os.path.join(temp_dir, f'frida-gadget.so.xz')
|
||||
so_file = os.path.join(temp_dir, 'libgadget.so')
|
||||
|
||||
try:
|
||||
# Download
|
||||
result = subprocess.run(['curl', '-L', '-o', xz_file, url],
|
||||
capture_output=True, text=True, check=False)
|
||||
if result.returncode != 0:
|
||||
print(f"Failed to download: {result.stderr}")
|
||||
shutil.rmtree(temp_dir)
|
||||
return None
|
||||
|
||||
print("✓ Downloaded")
|
||||
|
||||
# Decompress
|
||||
print("Decompressing...")
|
||||
result = subprocess.run(['xz', '-d', xz_file],
|
||||
capture_output=True, text=True, check=False)
|
||||
if result.returncode != 0:
|
||||
print(f"Failed to decompress: {result.stderr}")
|
||||
shutil.rmtree(temp_dir)
|
||||
return None
|
||||
|
||||
# Rename
|
||||
decompressed = xz_file.replace('.xz', '')
|
||||
os.rename(decompressed, so_file)
|
||||
|
||||
print("✓ Decompressed")
|
||||
|
||||
# Save to cache for future use
|
||||
try:
|
||||
shutil.copy2(so_file, str(cached_file))
|
||||
print(f"✓ Cached for future use: {cached_file}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to cache gadget: {e}")
|
||||
|
||||
return so_file
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error downloading Frida Gadget: {e}")
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
||||
return None
|
||||
|
||||
|
||||
def ensure_gadget_installed(adb: ADBHelper, gadget_name: str = 'libgadget.so') -> bool:
|
||||
"""Ensure Frida Gadget is installed on device"""
|
||||
gadget_path = f"{SO_STORAGE_DIR}/{gadget_name}"
|
||||
|
||||
print(f"\n=== Checking Frida Gadget ===")
|
||||
|
||||
# Check if gadget already exists
|
||||
if adb.file_exists(gadget_path):
|
||||
print(f"✓ Gadget found: {gadget_path}")
|
||||
return True
|
||||
|
||||
print(f"Gadget not found in: {gadget_path}")
|
||||
print("Need to download and install Frida Gadget")
|
||||
|
||||
# Get device architecture
|
||||
arch = adb.get_arch()
|
||||
print(f"Device architecture: {arch}")
|
||||
|
||||
# Download gadget
|
||||
local_gadget = download_frida_gadget(arch)
|
||||
if not local_gadget:
|
||||
print("Failed to download Frida Gadget")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Push to device temp location
|
||||
print("\nPushing to device...")
|
||||
if not adb.push_file(local_gadget, '/data/local/tmp/libgadget.so'):
|
||||
print("Failed to push gadget to device")
|
||||
return False
|
||||
|
||||
print("✓ Pushed to device")
|
||||
|
||||
# Copy to SO storage with root
|
||||
print(f"Installing to {gadget_path}...")
|
||||
result = adb.run(['shell', f'su -c "mkdir -p {SO_STORAGE_DIR}"'], check=False)
|
||||
result = adb.run(['shell',
|
||||
f'su -c "cp /data/local/tmp/libgadget.so {gadget_path}"'],
|
||||
check=False)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Failed to install gadget: {result.stderr}")
|
||||
return False
|
||||
|
||||
# Set permissions
|
||||
adb.run(['shell', f'su -c "chmod 755 {gadget_path}"'], check=False)
|
||||
|
||||
# Verify
|
||||
if adb.file_exists(gadget_path):
|
||||
print(f"✓ Gadget installed successfully: {gadget_path}")
|
||||
# Clean up temp file on device
|
||||
adb.run(['shell', 'rm -f /data/local/tmp/libgadget.so'], check=False)
|
||||
return True
|
||||
else:
|
||||
print("Failed to verify gadget installation")
|
||||
return False
|
||||
|
||||
finally:
|
||||
# Clean up local temp file
|
||||
temp_dir = os.path.dirname(local_gadget)
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
def generate_config_files(package_name: str, gadget_config: Dict) -> tuple:
|
||||
"""Generate config.json and gadget config content"""
|
||||
|
||||
# Prepare SO file reference for gadget
|
||||
gadget_name = gadget_config['gadgetName']
|
||||
gadget_so_ref = {
|
||||
"name": gadget_name,
|
||||
"storedPath": f"{SO_STORAGE_DIR}/{gadget_name}",
|
||||
"originalPath": f"{SO_STORAGE_DIR}/{gadget_name}"
|
||||
}
|
||||
|
||||
# Generate main module config.json
|
||||
module_config = {
|
||||
"enabled": True,
|
||||
"hideInjection": False,
|
||||
"injectionDelay": 2,
|
||||
"globalSoFiles": [],
|
||||
"perAppConfig": {
|
||||
package_name: {
|
||||
"enabled": True,
|
||||
"soFiles": [gadget_so_ref], # Include gadget SO file
|
||||
"injectionMethod": "standard",
|
||||
"gadgetConfig": gadget_config,
|
||||
"useGlobalGadget": False
|
||||
}
|
||||
},
|
||||
"globalGadgetConfig": None
|
||||
}
|
||||
|
||||
# Generate gadget config content based on mode
|
||||
if gadget_config['mode'] == 'server':
|
||||
gadget_config_content = {
|
||||
"interaction": {
|
||||
"type": "listen",
|
||||
"address": gadget_config['address'],
|
||||
"port": gadget_config['port'],
|
||||
"on_port_conflict": gadget_config['onPortConflict'],
|
||||
"on_load": gadget_config['onLoad']
|
||||
}
|
||||
}
|
||||
else: # script mode
|
||||
gadget_config_content = {
|
||||
"interaction": {
|
||||
"type": "script",
|
||||
"path": gadget_config['scriptPath']
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
json.dumps(module_config, indent=2, ensure_ascii=False),
|
||||
json.dumps(gadget_config_content, indent=2, ensure_ascii=False)
|
||||
)
|
||||
|
||||
|
||||
def quick_test(adb: ADBHelper, port: int = 27042):
|
||||
"""Quick test connectivity after configuration"""
|
||||
print("\n=== Quick Test ===")
|
||||
print("Testing Frida connectivity...\n")
|
||||
|
||||
# Check if frida is installed
|
||||
result = subprocess.run(['which', 'frida'], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print("⚠️ Frida CLI not found. Please install it with:")
|
||||
print(" pip install frida-tools")
|
||||
return False
|
||||
|
||||
# Test 1: Basic connection test
|
||||
print("Test 1: Basic connection...")
|
||||
# 使用 stdin 输入命令和 exit,避免交互式 REPL 导致超时
|
||||
test_input = "console.log('Connected! PID:', Process.id)\nexit\n"
|
||||
test_cmd = ['frida', '-H', f'127.0.0.1:{port}', 'Gadget']
|
||||
|
||||
result = subprocess.run(test_cmd, input=test_input, capture_output=True, text=True, timeout=10)
|
||||
if result.returncode == 0 and 'Connected!' in result.stdout:
|
||||
print("✓ Connection successful!")
|
||||
# 提取并显示 PID
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'Connected! PID:' in line:
|
||||
print(f" {line.strip()}")
|
||||
break
|
||||
else:
|
||||
print("✗ Connection failed")
|
||||
print(f" Error: {result.stderr.strip() if result.stderr else 'Unknown error'}")
|
||||
print("\nTroubleshooting:")
|
||||
print(" 1. Check if the target app is running")
|
||||
print(" 2. Verify port forwarding: adb forward tcp:{} tcp:{}".format(port, port))
|
||||
print(" 3. Check logcat for errors: adb logcat -s Gadget:*")
|
||||
return False
|
||||
|
||||
# Test 2: Enumerate modules
|
||||
print("\nTest 2: Enumerate loaded modules...")
|
||||
test_input = "Process.enumerateModules().slice(0, 5).forEach(m => console.log(' -', m.name))\nexit\n"
|
||||
test_cmd = ['frida', '-H', f'127.0.0.1:{port}', 'Gadget']
|
||||
|
||||
result = subprocess.run(test_cmd, input=test_input, capture_output=True, text=True, timeout=10)
|
||||
if result.returncode == 0:
|
||||
print("✓ Modules enumerated:")
|
||||
# 提取并显示模块列表
|
||||
in_output = False
|
||||
for line in result.stdout.split('\n'):
|
||||
if ' -' in line:
|
||||
in_output = True
|
||||
print(line)
|
||||
elif in_output and line.strip() == '':
|
||||
break
|
||||
else:
|
||||
print("✗ Failed to enumerate modules")
|
||||
print(f" Error: {result.stderr.strip()}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup_port_forward(adb: ADBHelper, port: int) -> bool:
|
||||
"""Setup ADB port forwarding"""
|
||||
print(f"\n=== Setting up port forwarding ===")
|
||||
print(f"Forwarding local port {port} to device port {port}...")
|
||||
|
||||
result = adb.run(['forward', f'tcp:{port}', f'tcp:{port}'], check=False)
|
||||
if result.returncode == 0:
|
||||
print(f"✓ Port forwarding established: tcp:{port} -> tcp:{port}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ Failed to setup port forwarding")
|
||||
print(f" Error: {result.stderr}")
|
||||
return False
|
||||
|
||||
|
||||
def check_and_set_selinux(adb: ADBHelper) -> bool:
|
||||
"""Check SELinux status and set to Permissive if needed"""
|
||||
print("\n=== Checking SELinux Status ===")
|
||||
|
||||
# Check current SELinux status
|
||||
result = adb.run(['shell', 'getenforce'], check=False)
|
||||
if result.returncode != 0:
|
||||
print("⚠️ Failed to check SELinux status")
|
||||
return True # Continue anyway
|
||||
|
||||
status = result.stdout.strip()
|
||||
print(f"Current SELinux mode: {status}")
|
||||
|
||||
if status == 'Permissive':
|
||||
print("✓ SELinux is already in Permissive mode")
|
||||
return True
|
||||
elif status == 'Enforcing':
|
||||
print("\n⚠️ SELinux is in Enforcing mode")
|
||||
print(" Zygisk modules cannot read config files when SELinux is Enforcing.")
|
||||
print(" We need to set it to Permissive mode.")
|
||||
|
||||
# Ask user for confirmation
|
||||
while True:
|
||||
choice = input("\nSet SELinux to Permissive? (Y/n): ").strip().lower()
|
||||
if choice in ['', 'y', 'yes']:
|
||||
break
|
||||
elif choice in ['n', 'no']:
|
||||
print("\nWarning: Continuing with SELinux Enforcing may cause injection to fail.")
|
||||
print("You can manually set it later with: adb shell \"su -c 'setenforce 0'\"")
|
||||
return True
|
||||
else:
|
||||
print("Please enter 'y' or 'n'")
|
||||
|
||||
# Set SELinux to Permissive
|
||||
print("\nSetting SELinux to Permissive...")
|
||||
result = adb.run(['shell', 'su', '-c', 'setenforce 0'], check=False)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("✓ SELinux set to Permissive mode")
|
||||
print(" Note: This setting will reset after reboot")
|
||||
return True
|
||||
else:
|
||||
print("✗ Failed to set SELinux to Permissive")
|
||||
print(f" Error: {result.stderr.strip()}")
|
||||
print("\nPlease manually run: adb shell \"su -c 'setenforce 0'\"")
|
||||
return False
|
||||
else:
|
||||
print(f"Unknown SELinux status: {status}")
|
||||
return True
|
||||
|
||||
|
||||
def restart_app(adb: ADBHelper, package_name: str):
|
||||
"""Restart the target application"""
|
||||
print(f"\n=== Restarting Application ===")
|
||||
|
||||
# Force stop
|
||||
print(f"Stopping {package_name}...")
|
||||
result = adb.run(['shell', 'am', 'force-stop', package_name], check=False)
|
||||
if result.returncode == 0:
|
||||
print("✓ App stopped")
|
||||
else:
|
||||
print("⚠️ Failed to stop app")
|
||||
|
||||
# Get main activity
|
||||
print(f"\nGetting launch activity...")
|
||||
result = adb.run(['shell', 'pm', 'dump', package_name, '|', 'grep', '-A', '1', 'android.intent.action.MAIN'], check=False)
|
||||
|
||||
# Try to start the app
|
||||
print(f"Starting {package_name}...")
|
||||
result = adb.run(['shell', 'monkey', '-p', package_name, '-c', 'android.intent.category.LAUNCHER', '1'], check=False)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("✓ App started")
|
||||
print(" Note: The app should now load with Frida Gadget injected")
|
||||
return True
|
||||
else:
|
||||
print("⚠️ Failed to start app automatically")
|
||||
print(" Please start the app manually from the device")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
# Parse command line arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Zygisk-MyInjector Auto Config Tool',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
'-p', '--port',
|
||||
type=int,
|
||||
choices=DEFAULT_PORTS,
|
||||
help=f'Preset Gadget port (choices: {", ".join(map(str, DEFAULT_PORTS))})'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 60)
|
||||
print("Zygisk-MyInjector Auto Config Tool")
|
||||
print("=" * 60)
|
||||
|
||||
# Step 1: Select device
|
||||
device_id = select_device()
|
||||
if not device_id:
|
||||
sys.exit(1)
|
||||
|
||||
adb = ADBHelper(device_id)
|
||||
|
||||
# Step 2: Check and set SELinux
|
||||
if not check_and_set_selinux(adb):
|
||||
print("\nError: Failed to configure SELinux")
|
||||
print("The injection may fail. Please fix SELinux manually and try again.")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 3: Select package
|
||||
package_name = select_package(adb)
|
||||
if not package_name:
|
||||
sys.exit(1)
|
||||
|
||||
# Step 4: Configure gadget
|
||||
gadget_config = configure_gadget(preset_port=args.port)
|
||||
if not gadget_config:
|
||||
sys.exit(1)
|
||||
|
||||
# Step 5: Ensure Frida Gadget is installed
|
||||
if not ensure_gadget_installed(adb, gadget_config['gadgetName']):
|
||||
print("\nError: Failed to install Frida Gadget")
|
||||
print("Please manually download and install libgadget.so")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 6: Generate config files
|
||||
print("\n=== Generating Configuration Files ===")
|
||||
config_json, gadget_config_json = generate_config_files(package_name, gadget_config)
|
||||
|
||||
print("\nGenerated config.json:")
|
||||
print(config_json)
|
||||
print("\nGenerated gadget config:")
|
||||
print(gadget_config_json)
|
||||
|
||||
# Step 7: Save to temp files
|
||||
temp_dir = tempfile.mkdtemp(prefix='frida_gadget_config_')
|
||||
|
||||
config_file = os.path.join(temp_dir, 'config.json')
|
||||
gadget_name = gadget_config['gadgetName'].replace('.so', '.config.so')
|
||||
gadget_config_file = os.path.join(temp_dir, gadget_name)
|
||||
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
f.write(config_json)
|
||||
|
||||
with open(gadget_config_file, 'w', encoding='utf-8') as f:
|
||||
f.write(gadget_config_json)
|
||||
|
||||
print(f"\nConfig files saved to: {temp_dir}")
|
||||
|
||||
# Step 8: Push to device
|
||||
print("\n=== Pushing Files to Device ===")
|
||||
|
||||
remote_config = '/data/local/tmp/zygisk_config.json'
|
||||
remote_gadget_config = f'/data/local/tmp/{gadget_name}'
|
||||
|
||||
if not adb.push_file(config_file, remote_config):
|
||||
print("Error: Failed to push config.json")
|
||||
sys.exit(1)
|
||||
print(f"✓ Pushed config.json -> {remote_config}")
|
||||
|
||||
if not adb.push_file(gadget_config_file, remote_gadget_config):
|
||||
print("Error: Failed to push gadget config")
|
||||
sys.exit(1)
|
||||
print(f"✓ Pushed gadget config -> {remote_gadget_config}")
|
||||
|
||||
# Step 9: Send broadcast
|
||||
print("\n=== Sending Broadcast to Apply Config ===")
|
||||
|
||||
success = adb.send_broadcast(
|
||||
action='com.jiqiu.configapp.APPLY_CONFIG',
|
||||
component='com.jiqiu.configapp/.ConfigApplyReceiver',
|
||||
extras={
|
||||
'package_name': package_name,
|
||||
'tmp_config_path': remote_config,
|
||||
'tmp_gadget_config_path': remote_gadget_config
|
||||
}
|
||||
)
|
||||
|
||||
if success:
|
||||
print("\n✓ Configuration applied successfully!")
|
||||
print(f"\nThe app '{package_name}' has been configured.")
|
||||
|
||||
# 自动完成工作流程
|
||||
print("\n=== Completing Workflow ===")
|
||||
|
||||
# Step 1: Restart app
|
||||
restart_app(adb, package_name)
|
||||
|
||||
# Step 2: Setup port forwarding
|
||||
port = gadget_config.get('port', 27042)
|
||||
if setup_port_forward(adb, port):
|
||||
# Step 3: Quick test
|
||||
import time
|
||||
print("\nWaiting 3 seconds for app to initialize...")
|
||||
time.sleep(3)
|
||||
|
||||
try:
|
||||
test_success = quick_test(adb, port)
|
||||
if test_success:
|
||||
# 彩色打印 frida 命令
|
||||
print("\n" + "="*60)
|
||||
print("\033[1;32m✓ All tests passed!\033[0m\n")
|
||||
print("\033[1;36mYou can now use Frida with the following commands:\033[0m\n")
|
||||
|
||||
# Interactive mode
|
||||
print("\033[1;33m# Interactive REPL:\033[0m")
|
||||
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget\033[0m\n")
|
||||
|
||||
# Execute script
|
||||
print("\033[1;33m# Execute JavaScript code:\033[0m")
|
||||
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget -e 'YOUR_CODE_HERE'\033[0m\n")
|
||||
|
||||
# Load script file
|
||||
print("\033[1;33m# Load script file:\033[0m")
|
||||
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget -l your_script.js\033[0m\n")
|
||||
|
||||
print("="*60)
|
||||
except Exception as e:
|
||||
print(f"\n⚠️ Test failed: {e}")
|
||||
print("\nYou can manually test with:")
|
||||
if gadget_config['mode'] == 'server':
|
||||
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget -l your_script.js\033[0m")
|
||||
else:
|
||||
print("\n✗ Failed to apply configuration")
|
||||
print("Please check logcat for details:")
|
||||
print(f" adb -s {device_id} logcat -s ConfigApplyReceiver:* ConfigManager:*")
|
||||
sys.exit(1)
|
||||
|
||||
# Clean up temp directory
|
||||
try:
|
||||
shutil.rmtree(temp_dir)
|
||||
except Exception:
|
||||
pass # Ignore cleanup errors
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nOperation cancelled by user")
|
||||
sys.exit(130)
|
||||
Reference in New Issue
Block a user