Compare commits
7 Commits
main
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad2b785f1e | ||
|
|
29a678c6dd | ||
|
|
6a79189290 | ||
|
|
7e23ed7281 | ||
|
|
aba55bfa57 | ||
|
|
ab46a223f1 | ||
|
|
3e5453ecf8 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -11,11 +11,4 @@
|
|||||||
/captures
|
/captures
|
||||||
/out
|
/out
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
|
|
||||||
# Frida Gadget temporary downloads
|
|
||||||
tmp_gadget/
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Frida Gadget local cache
|
|
||||||
scripts/.cache/
|
|
||||||
24
README.md
24
README.md
@@ -134,30 +134,6 @@ 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)
|
|
||||||
|
|
||||||
## 编译指南
|
## 编译指南
|
||||||
|
|
||||||
### 自动编译
|
### 自动编译
|
||||||
|
|||||||
Binary file not shown.
@@ -5,7 +5,6 @@
|
|||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ConfigApplication"
|
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -27,4 +26,4 @@
|
|||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,10 +18,14 @@ public class ConfigManager {
|
|||||||
public static final String MODULE_PATH = "/data/adb/modules/zygisk-myinjector";
|
public static final String MODULE_PATH = "/data/adb/modules/zygisk-myinjector";
|
||||||
public static final String CONFIG_FILE = MODULE_PATH + "/config.json";
|
public static final String CONFIG_FILE = MODULE_PATH + "/config.json";
|
||||||
public static final String SO_STORAGE_DIR = MODULE_PATH + "/so_files";
|
public static final String SO_STORAGE_DIR = MODULE_PATH + "/so_files";
|
||||||
|
public static final String KPM_MODULE_PATH = MODULE_PATH + "/injectHide.kpm";
|
||||||
|
public static final String KPM_HIDE_CONFIG = "/data/local/tmp/kpm_hide_config.txt";
|
||||||
|
private static final String KPM_MODULE_NAME = "hideInject";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
private ModuleConfig config;
|
private ModuleConfig config;
|
||||||
|
private final Object kpmLock = new Object(); // 用于同步 KPM 操作
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// Configure Shell to use root
|
// Configure Shell to use root
|
||||||
@@ -89,15 +93,6 @@ public class ConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Public method to reload configuration from file
|
|
||||||
* 从文件重新加载配置(用于外部更新配置后)
|
|
||||||
*/
|
|
||||||
public void reloadConfig() {
|
|
||||||
loadConfig();
|
|
||||||
Log.i(TAG, "Configuration reloaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveConfig() {
|
public void saveConfig() {
|
||||||
String json = gson.toJson(config);
|
String json = gson.toJson(config);
|
||||||
// Write to temp file first
|
// Write to temp file first
|
||||||
@@ -660,17 +655,256 @@ public class ConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== KPM Module Management ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public method to deploy SO files for a specific package
|
* 检查 KPM 模块是否已加载
|
||||||
* 为指定包名部署 SO 文件(外部调用)
|
|
||||||
* @param packageName Target package name
|
|
||||||
*/
|
*/
|
||||||
public void deployForPackage(String packageName) {
|
public boolean isKpmModuleLoaded() {
|
||||||
if (packageName == null || packageName.isEmpty()) {
|
Shell.Result result = Shell.cmd("lsmod | grep " + KPM_MODULE_NAME).exec();
|
||||||
Log.e(TAG, "Package name cannot be null or empty");
|
return result.isSuccess() && !result.getOut().isEmpty();
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载 KPM 模块
|
||||||
|
*/
|
||||||
|
public boolean loadKpmModule() {
|
||||||
|
if (!isRootAvailable()) {
|
||||||
|
Log.e(TAG, "Root access not available!");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
deploySoFilesToApp(packageName);
|
|
||||||
|
// Check if module file exists
|
||||||
|
Shell.Result checkResult = Shell.cmd("test -f \"" + KPM_MODULE_PATH + "\" && echo 'exists'").exec();
|
||||||
|
if (!checkResult.isSuccess() || checkResult.getOut().isEmpty()) {
|
||||||
|
Log.e(TAG, "KPM module file not found: " + KPM_MODULE_PATH);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already loaded
|
||||||
|
if (isKpmModuleLoaded()) {
|
||||||
|
Log.i(TAG, "KPM module already loaded");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load module
|
||||||
|
Shell.Result result = Shell.cmd("insmod \"" + KPM_MODULE_PATH + "\"").exec();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
Log.i(TAG, "KPM module loaded successfully");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to load KPM module: " + String.join("\n", result.getErr()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载 KPM 模块
|
||||||
|
*/
|
||||||
|
public boolean unloadKpmModule() {
|
||||||
|
if (!isRootAvailable()) {
|
||||||
|
Log.e(TAG, "Root access not available!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isKpmModuleLoaded()) {
|
||||||
|
Log.i(TAG, "KPM module not loaded");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Shell.Result result = Shell.cmd("rmmod " + KPM_MODULE_NAME).exec();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
Log.i(TAG, "KPM module unloaded successfully");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to unload KPM module: " + String.join("\n", result.getErr()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新加载 KPM 模块
|
||||||
|
*/
|
||||||
|
public boolean reloadKpmModule() {
|
||||||
|
// 使用锁防止并发重载
|
||||||
|
synchronized (kpmLock) {
|
||||||
|
Log.i(TAG, "Reloading KPM module...");
|
||||||
|
|
||||||
|
// Unload first
|
||||||
|
if (isKpmModuleLoaded()) {
|
||||||
|
if (!unloadKpmModule()) {
|
||||||
|
Log.e(TAG, "Failed to unload module before reload");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give kernel more time to cleanup (1 second for safety)
|
||||||
|
// 等待时间足够让内核完全清理资源
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.w(TAG, "Sleep interrupted during module reload", e);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify unload was successful
|
||||||
|
int retries = 5;
|
||||||
|
for (int i = 0; i < retries; i++) {
|
||||||
|
if (!isKpmModuleLoaded()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Log.w(TAG, "Module still loaded, waiting... (" + (i+1) + "/" + retries + ")");
|
||||||
|
try {
|
||||||
|
Thread.sleep(200);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKpmModuleLoaded()) {
|
||||||
|
Log.e(TAG, "Module still loaded after unload attempts, aborting reload");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load again
|
||||||
|
boolean success = loadKpmModule();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Log.i(TAG, "Module reloaded successfully");
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to load module after unload");
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} // synchronized
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 KPM 隐藏配置并重载模块
|
||||||
|
*/
|
||||||
|
public boolean updateKpmHideConfig(List<String> soNames) {
|
||||||
|
if (!isRootAvailable()) {
|
||||||
|
Log.e(TAG, "Root access not available!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build config content
|
||||||
|
StringBuilder configContent = new StringBuilder();
|
||||||
|
for (String soName : soNames) {
|
||||||
|
configContent.append(soName).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to temp file
|
||||||
|
String tempFile = context.getCacheDir() + "/kpm_hide_config.txt";
|
||||||
|
try {
|
||||||
|
java.io.FileWriter writer = new java.io.FileWriter(tempFile);
|
||||||
|
writer.write(configContent.toString());
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
// Ensure module directory exists
|
||||||
|
Shell.cmd("mkdir -p \"" + MODULE_PATH + "\"").exec();
|
||||||
|
|
||||||
|
// Ensure /data/local/tmp exists and is writable
|
||||||
|
Shell.cmd("mkdir -p /data/local/tmp && chmod 777 /data/local/tmp").exec();
|
||||||
|
|
||||||
|
// Copy to /data/local/tmp with root
|
||||||
|
Shell.Result copyResult = Shell.cmd("cp \"" + tempFile + "\" \"" + KPM_HIDE_CONFIG + "\"").exec();
|
||||||
|
if (!copyResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to copy KPM config: " + String.join("\n", copyResult.getErr()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Shell.cmd("chmod 666 \"" + KPM_HIDE_CONFIG + "\"").exec();
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
new File(tempFile).delete();
|
||||||
|
|
||||||
|
Log.i(TAG, "KPM hide config updated with " + soNames.size() + " entries");
|
||||||
|
|
||||||
|
// Reload module to apply changes
|
||||||
|
return reloadKpmModule();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to update KPM config", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前隐藏的 SO 列表
|
||||||
|
*/
|
||||||
|
public List<String> getHiddenSoList() {
|
||||||
|
List<String> hiddenList = new ArrayList<>();
|
||||||
|
|
||||||
|
Shell.Result result = Shell.cmd("cat \"" + KPM_HIDE_CONFIG + "\"").exec();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
for (String line : result.getOut()) {
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
hiddenList.add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hiddenList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加 SO 到隐藏列表
|
||||||
|
*/
|
||||||
|
public boolean addSoToHideList(String soName) {
|
||||||
|
List<String> hiddenList = getHiddenSoList();
|
||||||
|
if (!hiddenList.contains(soName)) {
|
||||||
|
hiddenList.add(soName);
|
||||||
|
return updateKpmHideConfig(hiddenList);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从隐藏列表移除 SO
|
||||||
|
*/
|
||||||
|
public boolean removeSoFromHideList(String soName) {
|
||||||
|
List<String> hiddenList = getHiddenSoList();
|
||||||
|
if (hiddenList.remove(soName)) {
|
||||||
|
return updateKpmHideConfig(hiddenList);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有可隐藏的 SO 文件列表(从已启用应用中提取)
|
||||||
|
*/
|
||||||
|
public List<String> getAvailableSoList() {
|
||||||
|
List<String> availableSos = new ArrayList<>();
|
||||||
|
|
||||||
|
// Always include our injector library
|
||||||
|
availableSos.add("libmyinjector.so");
|
||||||
|
|
||||||
|
// Add SOs from all enabled apps
|
||||||
|
for (Map.Entry<String, AppConfig> entry : config.perAppConfig.entrySet()) {
|
||||||
|
AppConfig appConfig = entry.getValue();
|
||||||
|
if (appConfig.enabled && appConfig.soFiles != null) {
|
||||||
|
for (SoFile soFile : appConfig.soFiles) {
|
||||||
|
if (!availableSos.contains(soFile.name)) {
|
||||||
|
availableSos.add(soFile.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add global SO files
|
||||||
|
if (config.globalSoFiles != null) {
|
||||||
|
for (SoFile soFile : config.globalSoFiles) {
|
||||||
|
if (!availableSos.contains(soFile.name)) {
|
||||||
|
availableSos.add(soFile.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableSos;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data classes
|
// Data classes
|
||||||
|
|||||||
105
configapp/src/main/java/com/jiqiu/configapp/HideSoAdapter.java
Normal file
105
configapp/src/main/java/com/jiqiu/configapp/HideSoAdapter.java
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SO 文件隐藏列表适配器
|
||||||
|
*/
|
||||||
|
public class HideSoAdapter extends RecyclerView.Adapter<HideSoAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
private final List<KpmHideFragment.HideSoItem> items;
|
||||||
|
private final OnItemCheckedChangeListener listener;
|
||||||
|
|
||||||
|
public interface OnItemCheckedChangeListener {
|
||||||
|
void onItemCheckedChanged(KpmHideFragment.HideSoItem item, boolean isChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HideSoAdapter(List<KpmHideFragment.HideSoItem> items,
|
||||||
|
OnItemCheckedChangeListener listener) {
|
||||||
|
this.items = items;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_hide_so, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
KpmHideFragment.HideSoItem item = items.get(position);
|
||||||
|
holder.bind(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return items.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
private final CheckBox cbHide;
|
||||||
|
private final TextView tvSoName;
|
||||||
|
private final TextView tvSoStatus;
|
||||||
|
private final TextView tvFixedBadge;
|
||||||
|
|
||||||
|
public ViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
cbHide = itemView.findViewById(R.id.cbHide);
|
||||||
|
tvSoName = itemView.findViewById(R.id.tvSoName);
|
||||||
|
tvSoStatus = itemView.findViewById(R.id.tvSoStatus);
|
||||||
|
tvFixedBadge = itemView.findViewById(R.id.tvFixedBadge);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bind(KpmHideFragment.HideSoItem item) {
|
||||||
|
tvSoName.setText(item.soName);
|
||||||
|
|
||||||
|
// 设置勾选状态
|
||||||
|
cbHide.setOnCheckedChangeListener(null); // 先移除监听器避免触发
|
||||||
|
cbHide.setChecked(item.isHidden);
|
||||||
|
|
||||||
|
// 显示状态
|
||||||
|
if (item.isFixed) {
|
||||||
|
tvSoStatus.setText("必需项 - 始终隐藏");
|
||||||
|
tvSoStatus.setTextColor(itemView.getContext().getResources()
|
||||||
|
.getColor(android.R.color.holo_green_dark, null));
|
||||||
|
tvFixedBadge.setVisibility(View.VISIBLE);
|
||||||
|
cbHide.setEnabled(false);
|
||||||
|
cbHide.setChecked(true); // 固定项始终勾选
|
||||||
|
} else {
|
||||||
|
tvSoStatus.setText(item.isHidden ? "已隐藏" : "未隐藏");
|
||||||
|
tvSoStatus.setTextColor(itemView.getContext().getResources()
|
||||||
|
.getColor(android.R.color.darker_gray, null));
|
||||||
|
tvFixedBadge.setVisibility(View.GONE);
|
||||||
|
cbHide.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置点击监听器
|
||||||
|
cbHide.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
|
if (listener != null && !item.isFixed) {
|
||||||
|
listener.onItemCheckedChanged(item, isChecked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 整行点击也触发勾选
|
||||||
|
itemView.setOnClickListener(v -> {
|
||||||
|
if (!item.isFixed) {
|
||||||
|
cbHide.setChecked(!cbHide.isChecked());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
249
configapp/src/main/java/com/jiqiu/configapp/KpmHideFragment.java
Normal file
249
configapp/src/main/java/com/jiqiu/configapp/KpmHideFragment.java
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.material.card.MaterialCardView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KPM 隐藏功能 Fragment
|
||||||
|
* 管理 KPM 内核模块和 SO 文件隐藏配置
|
||||||
|
*/
|
||||||
|
public class KpmHideFragment extends Fragment {
|
||||||
|
|
||||||
|
private static final String TAG = "KpmHideFragment";
|
||||||
|
|
||||||
|
private TextView tvModuleStatus;
|
||||||
|
private TextView tvModuleInfo;
|
||||||
|
private TextView tvConfigPath;
|
||||||
|
private Button btnReloadModule;
|
||||||
|
private RecyclerView rvSoList;
|
||||||
|
|
||||||
|
private ConfigManager configManager;
|
||||||
|
private HideSoAdapter adapter;
|
||||||
|
private ExecutorService executor;
|
||||||
|
private Handler mainHandler;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_kpm_hide, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
initViews(view);
|
||||||
|
initExecutor();
|
||||||
|
initConfigManager();
|
||||||
|
setupListeners();
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews(View view) {
|
||||||
|
tvModuleStatus = view.findViewById(R.id.tvModuleStatus);
|
||||||
|
tvModuleInfo = view.findViewById(R.id.tvModuleInfo);
|
||||||
|
tvConfigPath = view.findViewById(R.id.tvConfigPath);
|
||||||
|
btnReloadModule = view.findViewById(R.id.btnReloadModule);
|
||||||
|
rvSoList = view.findViewById(R.id.rvSoList);
|
||||||
|
|
||||||
|
rvSoList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initExecutor() {
|
||||||
|
executor = Executors.newSingleThreadExecutor();
|
||||||
|
mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initConfigManager() {
|
||||||
|
configManager = new ConfigManager(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupListeners() {
|
||||||
|
btnReloadModule.setOnClickListener(v -> reloadModule());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadData() {
|
||||||
|
// 显示配置路径
|
||||||
|
tvConfigPath.setText("配置文件: " + ConfigManager.KPM_HIDE_CONFIG);
|
||||||
|
|
||||||
|
// 异步加载模块状态和 SO 列表
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
final boolean isLoaded = configManager.isKpmModuleLoaded();
|
||||||
|
final List<String> availableSos = configManager.getAvailableSoList();
|
||||||
|
final List<String> hiddenSos = configManager.getHiddenSoList();
|
||||||
|
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
updateModuleStatus(isLoaded);
|
||||||
|
setupSoList(availableSos, hiddenSos);
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error loading data", e);
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
Toast.makeText(getContext(), "加载数据失败: " + e.getMessage(),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateModuleStatus(boolean isLoaded) {
|
||||||
|
if (isLoaded) {
|
||||||
|
tvModuleStatus.setText("● 已加载");
|
||||||
|
tvModuleStatus.setTextColor(getResources().getColor(android.R.color.holo_green_dark, null));
|
||||||
|
tvModuleInfo.setText("KPM 内核模块运行中\n模块名称: hideInject\n版本: 0.0.1");
|
||||||
|
} else {
|
||||||
|
tvModuleStatus.setText("● 未加载");
|
||||||
|
tvModuleStatus.setTextColor(getResources().getColor(android.R.color.holo_red_dark, null));
|
||||||
|
tvModuleInfo.setText("KPM 内核模块未运行\n请检查模块文件是否存在或手动重载");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupSoList(List<String> availableSos, List<String> hiddenSos) {
|
||||||
|
List<HideSoItem> items = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String soName : availableSos) {
|
||||||
|
HideSoItem item = new HideSoItem();
|
||||||
|
item.soName = soName;
|
||||||
|
item.isHidden = hiddenSos.contains(soName);
|
||||||
|
// libmyinjector.so 是固定隐藏的
|
||||||
|
item.isFixed = soName.equals("libmyinjector.so");
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = new HideSoAdapter(items, this::onSoItemCheckedChanged);
|
||||||
|
rvSoList.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSoItemCheckedChanged(HideSoItem item, boolean isChecked) {
|
||||||
|
if (item.isFixed) {
|
||||||
|
// 固定项不允许取消勾选
|
||||||
|
Toast.makeText(getContext(), "libmyinjector.so 是必需的,不能取消隐藏",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步更新配置
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
boolean success;
|
||||||
|
if (isChecked) {
|
||||||
|
success = configManager.addSoToHideList(item.soName);
|
||||||
|
} else {
|
||||||
|
success = configManager.removeSoFromHideList(item.soName);
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean finalSuccess = success;
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
if (finalSuccess) {
|
||||||
|
item.isHidden = isChecked;
|
||||||
|
Toast.makeText(getContext(),
|
||||||
|
isChecked ? "已添加到隐藏列表" : "已从隐藏列表移除",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
// 更新模块状态
|
||||||
|
refreshModuleStatus();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "更新失败,请检查日志",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
// 恢复原来的状态
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error updating SO hide status", e);
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
Toast.makeText(getContext(), "更新失败: " + e.getMessage(),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadModule() {
|
||||||
|
btnReloadModule.setEnabled(false);
|
||||||
|
btnReloadModule.setText("重载中...");
|
||||||
|
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
final boolean success = configManager.reloadKpmModule();
|
||||||
|
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
btnReloadModule.setEnabled(true);
|
||||||
|
btnReloadModule.setText("重载模块");
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Toast.makeText(getContext(), "模块重载成功", Toast.LENGTH_SHORT).show();
|
||||||
|
refreshModuleStatus();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "模块重载失败,请查看日志",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error reloading module", e);
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
btnReloadModule.setEnabled(true);
|
||||||
|
btnReloadModule.setText("重载模块");
|
||||||
|
Toast.makeText(getContext(), "重载失败: " + e.getMessage(),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshModuleStatus() {
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
final boolean isLoaded = configManager.isKpmModuleLoaded();
|
||||||
|
mainHandler.post(() -> updateModuleStatus(isLoaded));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error refreshing module status", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
if (executor != null && !executor.isShutdown()) {
|
||||||
|
executor.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SO 隐藏项数据类
|
||||||
|
*/
|
||||||
|
public static class HideSoItem {
|
||||||
|
public String soName;
|
||||||
|
public boolean isHidden;
|
||||||
|
public boolean isFixed; // 是否是固定隐藏项(不可取消)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
|
|||||||
private AppListFragment appListFragment;
|
private AppListFragment appListFragment;
|
||||||
private SettingsFragment settingsFragment;
|
private SettingsFragment settingsFragment;
|
||||||
private SoManagerFragment soManagerFragment;
|
private SoManagerFragment soManagerFragment;
|
||||||
|
private KpmHideFragment kpmHideFragment;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -53,6 +54,9 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
|
|||||||
} else if (itemId == R.id.navigation_so_manager) {
|
} else if (itemId == R.id.navigation_so_manager) {
|
||||||
showSoManagerFragment();
|
showSoManagerFragment();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (itemId == R.id.navigation_kpm_hide) {
|
||||||
|
showKpmHideFragment();
|
||||||
|
return true;
|
||||||
} else if (itemId == R.id.navigation_settings) {
|
} else if (itemId == R.id.navigation_settings) {
|
||||||
showSettingsFragment();
|
showSettingsFragment();
|
||||||
return true;
|
return true;
|
||||||
@@ -75,6 +79,13 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
|
|||||||
showFragment(soManagerFragment);
|
showFragment(soManagerFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showKpmHideFragment() {
|
||||||
|
if (kpmHideFragment == null) {
|
||||||
|
kpmHideFragment = new KpmHideFragment();
|
||||||
|
}
|
||||||
|
showFragment(kpmHideFragment);
|
||||||
|
}
|
||||||
|
|
||||||
private void showSettingsFragment() {
|
private void showSettingsFragment() {
|
||||||
if (settingsFragment == null) {
|
if (settingsFragment == null) {
|
||||||
settingsFragment = new SettingsFragment();
|
settingsFragment = new SettingsFragment();
|
||||||
|
|||||||
202
configapp/src/main/res/layout/fragment_kpm_hide.xml
Normal file
202
configapp/src/main/res/layout/fragment_kpm_hide.xml
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".KpmHideFragment">
|
||||||
|
|
||||||
|
<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="KPM 注入隐藏"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- KPM 模块状态卡片 -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<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:id="@+id/tvModuleStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="● 检查中..."
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvModuleInfo"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="正在检查模块状态..."
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnReloadModule"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="重载模块"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</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"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:cardBackgroundColor="#FFF3E0">
|
||||||
|
|
||||||
|
<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="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="• KPM 内核模块可以隐藏进程的内存映射信息\n• libmyinjector.so 将自动隐藏(不可取消)\n• 勾选其他 SO 文件可隐藏其在 /proc/[pid]/maps 中的显示\n• 每次更改都会自动重载内核模块以应用配置"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:lineSpacingExtra="4dp"
|
||||||
|
android:textColor="#E65100"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvConfigPath"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="配置文件: /data/local/tmp/kpm_hide_config.txt"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</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"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<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="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="以下库文件将始终被隐藏:"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="• libmyinjector.so (Zygisk 注入器)"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textColor="@android:color/holo_green_dark" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- SO 隐藏列表 -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<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="可选隐藏 SO 列表"
|
||||||
|
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="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvSoList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="100dp"
|
||||||
|
tools:listitem="@layout/item_hide_so" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
58
configapp/src/main/res/layout/item_hide_so.xml
Normal file
58
configapp/src/main/res/layout/item_hide_so.xml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/cbHide"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSoName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="libexample.so"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSoStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="未隐藏"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvFixedBadge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="固定"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:background="@android:color/holo_green_dark"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
android:id="@+id/navigation_so_manager"
|
android:id="@+id/navigation_so_manager"
|
||||||
android:icon="@android:drawable/ic_menu_save"
|
android:icon="@android:drawable/ic_menu_save"
|
||||||
android:title="@string/title_so_manager" />
|
android:title="@string/title_so_manager" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/navigation_kpm_hide"
|
||||||
|
android:icon="@android:drawable/ic_secure"
|
||||||
|
android:title="@string/title_kpm_hide" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_settings"
|
android:id="@+id/navigation_settings"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<!-- 底部导航 -->
|
<!-- 底部导航 -->
|
||||||
<string name="title_apps">应用列表</string>
|
<string name="title_apps">应用列表</string>
|
||||||
<string name="title_so_manager">SO库管理</string>
|
<string name="title_so_manager">SO库管理</string>
|
||||||
|
<string name="title_kpm_hide">KPM隐藏</string>
|
||||||
<string name="title_settings">全局设置</string>
|
<string name="title_settings">全局设置</string>
|
||||||
|
|
||||||
<!-- 应用列表 -->
|
<!-- 应用列表 -->
|
||||||
@@ -21,4 +22,25 @@
|
|||||||
<!-- 关于 -->
|
<!-- 关于 -->
|
||||||
<string name="about">关于</string>
|
<string name="about">关于</string>
|
||||||
<string name="app_description">MyInjector 配置应用,用于管理注入设置</string>
|
<string name="app_description">MyInjector 配置应用,用于管理注入设置</string>
|
||||||
|
|
||||||
|
<!-- KPM 隐藏 -->
|
||||||
|
<string name="kpm_hide_title">KPM 注入隐藏</string>
|
||||||
|
<string name="kpm_module_status">模块状态</string>
|
||||||
|
<string name="kpm_module_loaded">已加载</string>
|
||||||
|
<string name="kpm_module_not_loaded">未加载</string>
|
||||||
|
<string name="kpm_module_info">KPM 内核模块信息</string>
|
||||||
|
<string name="kpm_reload_module">重载模块</string>
|
||||||
|
<string name="kpm_reload_success">模块重载成功</string>
|
||||||
|
<string name="kpm_reload_failed">模块重载失败</string>
|
||||||
|
<string name="kpm_config_path">配置文件路径</string>
|
||||||
|
<string name="kpm_fixed_items">固定隐藏项</string>
|
||||||
|
<string name="kpm_optional_items">可选隐藏 SO 列表</string>
|
||||||
|
<string name="kpm_usage_info">使用说明</string>
|
||||||
|
<string name="kpm_fixed_badge">固定</string>
|
||||||
|
<string name="kpm_hidden">已隐藏</string>
|
||||||
|
<string name="kpm_not_hidden">未隐藏</string>
|
||||||
|
<string name="kpm_add_success">已添加到隐藏列表</string>
|
||||||
|
<string name="kpm_remove_success">已从隐藏列表移除</string>
|
||||||
|
<string name="kpm_update_failed">更新失败</string>
|
||||||
|
<string name="kpm_fixed_cannot_uncheck">libmyinjector.so 是必需的,不能取消隐藏</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -70,6 +70,10 @@ afterEvaluate {
|
|||||||
from("$projectDir") {
|
from("$projectDir") {
|
||||||
include 'service.sh'
|
include 'service.sh'
|
||||||
}
|
}
|
||||||
|
// Copy KPM module
|
||||||
|
from("$projectDir/kpm") {
|
||||||
|
include 'injectHide.kpm'
|
||||||
|
}
|
||||||
// Copy ConfigApp APK if it exists
|
// Copy ConfigApp APK if it exists
|
||||||
def apkFile = file("$rootDir/configapp/build/outputs/apk/debug/configapp-debug.apk")
|
def apkFile = file("$rootDir/configapp/build/outputs/apk/debug/configapp-debug.apk")
|
||||||
if (apkFile.exists()) {
|
if (apkFile.exists()) {
|
||||||
|
|||||||
BIN
module/kpm/injectHide.kpm
Normal file
BIN
module/kpm/injectHide.kpm
Normal file
Binary file not shown.
@@ -94,5 +94,46 @@ chown -R root:root /data/adb/modules/zygisk-myinjector
|
|||||||
|
|
||||||
log "ConfigApp 安装脚本执行完成"
|
log "ConfigApp 安装脚本执行完成"
|
||||||
|
|
||||||
|
# ==================== KPM 模块加载 ====================
|
||||||
|
|
||||||
|
# KPM 模块路径
|
||||||
|
KPM_MODULE="$MODDIR/injectHide.kpm"
|
||||||
|
KPM_CONFIG="/data/local/tmp/kpm_hide_config.txt"
|
||||||
|
|
||||||
|
log "开始加载 KPM 内核模块"
|
||||||
|
|
||||||
|
# 检查 KPM 模块文件是否存在
|
||||||
|
if [ ! -f "$KPM_MODULE" ]; then
|
||||||
|
log "KPM 模块文件不存在: $KPM_MODULE"
|
||||||
|
else
|
||||||
|
log "找到 KPM 模块文件: $KPM_MODULE"
|
||||||
|
|
||||||
|
# 创建初始配置文件(如果不存在)
|
||||||
|
if [ ! -f "$KPM_CONFIG" ]; then
|
||||||
|
log "创建初始 KPM 配置文件"
|
||||||
|
# 确保 /data/local/tmp 目录存在且权限正确
|
||||||
|
mkdir -p /data/local/tmp
|
||||||
|
chmod 777 /data/local/tmp
|
||||||
|
echo "libmyinjector.so" > "$KPM_CONFIG"
|
||||||
|
chmod 666 "$KPM_CONFIG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 等待一段时间确保系统稳定
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# 加载 KPM 模块
|
||||||
|
log "正在加载 KPM 模块..."
|
||||||
|
insmod "$KPM_MODULE" 2>&1 | while read line; do
|
||||||
|
log "insmod: $line"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 检查模块是否加载成功
|
||||||
|
if lsmod | grep -q "hideInject"; then
|
||||||
|
log "KPM 模块加载成功!"
|
||||||
|
else
|
||||||
|
log "KPM 模块加载失败,请检查日志"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# 脚本完成
|
# 脚本完成
|
||||||
exit 0
|
exit 0
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Perfare on 2020/7/4.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef ZYGISK_IL2CPPDUMPER_GAME_H
|
|
||||||
#define ZYGISK_IL2CPPDUMPER_GAME_H
|
|
||||||
|
|
||||||
#define AimPackageName "com.tencent.mobileqq"
|
|
||||||
|
|
||||||
#endif //ZYGISK_IL2CPPDUMPER_GAME_H
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Perfare on 2020/7/4.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "hack.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "xdl.h"
|
|
||||||
#include <cstring>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <sys/system_properties.h>
|
|
||||||
#include <dlfcn.h>
|
|
||||||
#include <jni.h>
|
|
||||||
#include <thread>
|
|
||||||
#include <sys/mman.h>
|
|
||||||
#include <linux/unistd.h>
|
|
||||||
#include <array>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
//#include <asm-generic/fcntl.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include "newriruhide.h"
|
|
||||||
void load_so(const char *game_data_dir, JavaVM *vm, const char *soname) {
|
|
||||||
bool load = false;
|
|
||||||
LOGI("hack_start %s", game_data_dir);
|
|
||||||
|
|
||||||
// 构建新文件路径,使用传入的 soname 参数
|
|
||||||
char new_so_path[256];
|
|
||||||
snprintf(new_so_path, sizeof(new_so_path), "%s/files/%s.so", game_data_dir, soname);
|
|
||||||
|
|
||||||
// 构建源文件路径
|
|
||||||
char src_path[256];
|
|
||||||
snprintf(src_path, sizeof(src_path), "/data/local/tmp/%s.so", soname);
|
|
||||||
|
|
||||||
// 打开源文件
|
|
||||||
int src_fd = open(src_path, O_RDONLY);
|
|
||||||
if (src_fd < 0) {
|
|
||||||
LOGE("Failed to open %s: %s (errno: %d)", src_path, strerror(errno), errno);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开目标文件
|
|
||||||
int dest_fd = open(new_so_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
||||||
if (dest_fd < 0) {
|
|
||||||
LOGE("Failed to open %s", new_so_path);
|
|
||||||
close(src_fd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制文件内容
|
|
||||||
char buffer[4096];
|
|
||||||
ssize_t bytes;
|
|
||||||
while ((bytes = read(src_fd, buffer, sizeof(buffer))) > 0) {
|
|
||||||
if (write(dest_fd, buffer, bytes) != bytes) {
|
|
||||||
LOGE("Failed to write to %s", new_so_path);
|
|
||||||
close(src_fd);
|
|
||||||
close(dest_fd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭文件描述符
|
|
||||||
close(src_fd);
|
|
||||||
close(dest_fd);
|
|
||||||
|
|
||||||
// 修改文件权限
|
|
||||||
if (chmod(new_so_path, 0755) != 0) {
|
|
||||||
LOGE("Failed to change permissions on %s: %s (errno: %d)", new_so_path, strerror(errno), errno);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
LOGI("Successfully changed permissions to 755 on %s", new_so_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载 .so 文件
|
|
||||||
void *handle;
|
|
||||||
for (int i = 0; i < 10; i++) {
|
|
||||||
handle = dlopen(new_so_path, RTLD_NOW | RTLD_LOCAL);
|
|
||||||
if (handle) {
|
|
||||||
LOGI("Successfully loaded %s", new_so_path);
|
|
||||||
load = true;
|
|
||||||
char new_soname[256];
|
|
||||||
sprintf(new_soname, "%s.so", soname);
|
|
||||||
riru_hide(new_soname);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
LOGE("Failed to load %s: %s", new_so_path, dlerror());
|
|
||||||
sleep(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果加载失败
|
|
||||||
if (!load) {
|
|
||||||
LOGI("%s.so not found in thread %d", soname, gettid());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找 JNI_OnLoad 并调用
|
|
||||||
// void (*setupSignalHandler)();
|
|
||||||
// *(void **) (&setupSignalHandler) = dlsym(handle, "setupSignalHandler");
|
|
||||||
//
|
|
||||||
// if (setupSignalHandler) {
|
|
||||||
// LOGI("setupSignalHandler symbol found, calling setupSignalHandler.");
|
|
||||||
// setupSignalHandler(); // 调用找到的函数
|
|
||||||
// } else {
|
|
||||||
// LOGE("setupSignalHandler symbol not found in %s", new_so_path);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
void hack_start(const char *game_data_dir,JavaVM *vm) {
|
|
||||||
load_so(game_data_dir,vm,"test");
|
|
||||||
//如果要注入多个so,那么就在这里不断的添加load_so函数即可
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string GetLibDir(JavaVM *vms) {
|
|
||||||
JNIEnv *env = nullptr;
|
|
||||||
vms->AttachCurrentThread(&env, nullptr);
|
|
||||||
jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
|
|
||||||
if (activity_thread_clz != nullptr) {
|
|
||||||
jmethodID currentApplicationId = env->GetStaticMethodID(activity_thread_clz,
|
|
||||||
"currentApplication",
|
|
||||||
"()Landroid/app/Application;");
|
|
||||||
if (currentApplicationId) {
|
|
||||||
jobject application = env->CallStaticObjectMethod(activity_thread_clz,
|
|
||||||
currentApplicationId);
|
|
||||||
jclass application_clazz = env->GetObjectClass(application);
|
|
||||||
if (application_clazz) {
|
|
||||||
jmethodID get_application_info = env->GetMethodID(application_clazz,
|
|
||||||
"getApplicationInfo",
|
|
||||||
"()Landroid/content/pm/ApplicationInfo;");
|
|
||||||
if (get_application_info) {
|
|
||||||
jobject application_info = env->CallObjectMethod(application,
|
|
||||||
get_application_info);
|
|
||||||
jfieldID native_library_dir_id = env->GetFieldID(
|
|
||||||
env->GetObjectClass(application_info), "nativeLibraryDir",
|
|
||||||
"Ljava/lang/String;");
|
|
||||||
if (native_library_dir_id) {
|
|
||||||
auto native_library_dir_jstring = (jstring) env->GetObjectField(
|
|
||||||
application_info, native_library_dir_id);
|
|
||||||
auto path = env->GetStringUTFChars(native_library_dir_jstring, nullptr);
|
|
||||||
LOGI("lib dir %s", path);
|
|
||||||
std::string lib_dir(path);
|
|
||||||
env->ReleaseStringUTFChars(native_library_dir_jstring, path);
|
|
||||||
return lib_dir;
|
|
||||||
} else {
|
|
||||||
LOGE("nativeLibraryDir not found");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGE("getApplicationInfo not found");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGE("application class not found");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGE("currentApplication not found");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGE("ActivityThread not found");
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string GetNativeBridgeLibrary() {
|
|
||||||
auto value = std::array<char, PROP_VALUE_MAX>();
|
|
||||||
__system_property_get("ro.dalvik.vm.native.bridge", value.data());
|
|
||||||
return {value.data()};
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NativeBridgeCallbacks {
|
|
||||||
uint32_t version;
|
|
||||||
void *initialize;
|
|
||||||
|
|
||||||
void *(*loadLibrary)(const char *libpath, int flag);
|
|
||||||
|
|
||||||
void *(*getTrampoline)(void *handle, const char *name, const char *shorty, uint32_t len);
|
|
||||||
|
|
||||||
void *isSupported;
|
|
||||||
void *getAppEnv;
|
|
||||||
void *isCompatibleWith;
|
|
||||||
void *getSignalHandler;
|
|
||||||
void *unloadLibrary;
|
|
||||||
void *getError;
|
|
||||||
void *isPathSupported;
|
|
||||||
void *initAnonymousNamespace;
|
|
||||||
void *createNamespace;
|
|
||||||
void *linkNamespaces;
|
|
||||||
|
|
||||||
void *(*loadLibraryExt)(const char *libpath, int flag, void *ns);
|
|
||||||
};
|
|
||||||
|
|
||||||
bool NativeBridgeLoad(const char *game_data_dir, int api_level, void *data, size_t length) {
|
|
||||||
//TODO 等待houdini初始化
|
|
||||||
sleep(5);
|
|
||||||
|
|
||||||
auto libart = dlopen("libart.so", RTLD_NOW);
|
|
||||||
auto JNI_GetCreatedJavaVMs = (jint (*)(JavaVM **, jsize, jsize *)) dlsym(libart,
|
|
||||||
"JNI_GetCreatedJavaVMs");
|
|
||||||
LOGI("JNI_GetCreatedJavaVMs %p", JNI_GetCreatedJavaVMs);
|
|
||||||
JavaVM *vms_buf[1];
|
|
||||||
JavaVM *vms;
|
|
||||||
jsize num_vms;
|
|
||||||
jint status = JNI_GetCreatedJavaVMs(vms_buf, 1, &num_vms);
|
|
||||||
if (status == JNI_OK && num_vms > 0) {
|
|
||||||
vms = vms_buf[0];
|
|
||||||
} else {
|
|
||||||
LOGE("GetCreatedJavaVMs error");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto lib_dir = GetLibDir(vms);
|
|
||||||
if (lib_dir.empty()) {
|
|
||||||
LOGE("GetLibDir error");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (lib_dir.find("/lib/x86") != std::string::npos) {
|
|
||||||
LOGI("no need NativeBridge");
|
|
||||||
munmap(data, length);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto nb = dlopen("libhoudini.so", RTLD_NOW);
|
|
||||||
if (!nb) {
|
|
||||||
auto native_bridge = GetNativeBridgeLibrary();
|
|
||||||
LOGI("native bridge: %s", native_bridge.data());
|
|
||||||
nb = dlopen(native_bridge.data(), RTLD_NOW);
|
|
||||||
}
|
|
||||||
if (nb) {
|
|
||||||
LOGI("nb %p", nb);
|
|
||||||
auto callbacks = (NativeBridgeCallbacks *) dlsym(nb, "NativeBridgeItf");
|
|
||||||
if (callbacks) {
|
|
||||||
LOGI("NativeBridgeLoadLibrary %p", callbacks->loadLibrary);
|
|
||||||
LOGI("NativeBridgeLoadLibraryExt %p", callbacks->loadLibraryExt);
|
|
||||||
LOGI("NativeBridgeGetTrampoline %p", callbacks->getTrampoline);
|
|
||||||
int fd = syscall(__NR_memfd_create, "anon", MFD_CLOEXEC);
|
|
||||||
ftruncate(fd, (off_t) length);
|
|
||||||
void *mem = mmap(nullptr, length, PROT_WRITE, MAP_SHARED, fd, 0);
|
|
||||||
memcpy(mem, data, length);
|
|
||||||
munmap(mem, length);
|
|
||||||
munmap(data, length);
|
|
||||||
char path[PATH_MAX];
|
|
||||||
snprintf(path, PATH_MAX, "/proc/self/fd/%d", fd);
|
|
||||||
LOGI("arm path %s", path);
|
|
||||||
|
|
||||||
void *arm_handle;
|
|
||||||
if (api_level >= 26) {
|
|
||||||
arm_handle = callbacks->loadLibraryExt(path, RTLD_NOW, (void *) 3);
|
|
||||||
} else {
|
|
||||||
arm_handle = callbacks->loadLibrary(path, RTLD_NOW);
|
|
||||||
}
|
|
||||||
if (arm_handle) {
|
|
||||||
LOGI("arm handle %p", arm_handle);
|
|
||||||
auto init = (void (*)(JavaVM *, void *)) callbacks->getTrampoline(arm_handle,
|
|
||||||
"JNI_OnLoad",
|
|
||||||
nullptr, 0);
|
|
||||||
LOGI("JNI_OnLoad %p", init);
|
|
||||||
init(vms, (void *) game_data_dir);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
close(fd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void hack_prepare(const char *_data_dir, void *data, size_t length) {
|
|
||||||
LOGI("hack thread: %d", gettid());
|
|
||||||
int api_level = android_get_device_api_level();
|
|
||||||
LOGI("api level: %d", api_level);
|
|
||||||
|
|
||||||
#if defined(__i386__) || defined(__x86_64__)
|
|
||||||
if (!NativeBridgeLoad(_data_dir, api_level, data, length)) {
|
|
||||||
#endif
|
|
||||||
hack_start(_data_dir, nullptr);
|
|
||||||
#if defined(__i386__) || defined(__x86_64__)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(__arm__) || defined(__aarch64__)
|
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
|
|
||||||
auto game_data_dir = (const char *) reserved;
|
|
||||||
std::thread hack_thread(hack_start, game_data_dir,vm);
|
|
||||||
hack_thread.detach();
|
|
||||||
return JNI_VERSION_1_6;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include "hack.h"
|
#include "hack.h"
|
||||||
#include "zygisk.hpp"
|
#include "zygisk.hpp"
|
||||||
#include "game.h"
|
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "dlfcn.h"
|
#include "dlfcn.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -30,9 +29,6 @@ public:
|
|||||||
void preAppSpecialize(AppSpecializeArgs *args) override {
|
void preAppSpecialize(AppSpecializeArgs *args) override {
|
||||||
auto package_name = env->GetStringUTFChars(args->nice_name, nullptr);
|
auto package_name = env->GetStringUTFChars(args->nice_name, nullptr);
|
||||||
auto app_data_dir = env->GetStringUTFChars(args->app_data_dir, nullptr);
|
auto app_data_dir = env->GetStringUTFChars(args->app_data_dir, nullptr);
|
||||||
// if (strcmp(package_name, AimPackageName) == 0){
|
|
||||||
// args->runtime_flags=8451;
|
|
||||||
// }
|
|
||||||
LOGI("preAppSpecialize %s %s %d", package_name, app_data_dir,args->runtime_flags);
|
LOGI("preAppSpecialize %s %s %d", package_name, app_data_dir,args->runtime_flags);
|
||||||
|
|
||||||
preSpecialize(package_name, app_data_dir);
|
preSpecialize(package_name, app_data_dir);
|
||||||
|
|||||||
4
scripts/.gitignore
vendored
4
scripts/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
# Temporary config files
|
|
||||||
.tmp/
|
|
||||||
*.pyc
|
|
||||||
__pycache__/
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
# 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))"
|
|
||||||
```
|
|
||||||
@@ -1,841 +0,0 @@
|
|||||||
#!/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