29 Commits

Author SHA1 Message Date
Yuuki
ad2b785f1e Merge branch 'feature/add_inject_hide' of https://github.com/jiqiu2022/Zygisk-MyInjector into feature/add_inject_hide 2025-10-25 21:10:58 +08:00
Yuuki
29a678c6dd Update injectHide.kpm 2025-10-25 21:10:55 +08:00
jiqiu2021
6a79189290 feat:修改kpm config的路径 2025-10-25 20:16:44 +08:00
Yuuki
7e23ed7281 Update injectHide.kpm 2025-10-25 18:21:11 +08:00
Yuuki
aba55bfa57 Create injectHide.kpm 2025-10-25 17:43:04 +08:00
jiqiu2021
ab46a223f1 feat:增加kpm注入功能 2025-10-25 17:28:47 +08:00
jiqiu2021
3e5453ecf8 feat:移除旧版无用代码 2025-10-25 16:50:51 +08:00
Ji qiu
ec89b7bf2c Merge pull request #29 from vwww-droid/fix/improve-meta-inf-handling
feat: Fix META-INF handling in build script
2025-10-15 15:57:50 +08:00
vwvw
cc47334970 feat: Fix META-INF handling in build script
1. 修复因 META-INF 文件在 pixel 4 android 13 狐妖magisk 27001 无法识别为 magisk module 的问题
2025-10-15 11:16:13 +08:00
Ji qiu
8a23161e0f Merge pull request #22 from jiqiu2022/fix_bug
Fix bug
2025-06-28 21:21:49 +08:00
jiqiu2021
964c975cdd feat:bugfix 2025-06-28 21:18:43 +08:00
jiqiu2021
6110216556 feat:bugfix 2025-06-28 20:25:17 +08:00
jiqiu2021
e02a3df7fc feat:bugfix 2025-06-27 21:52:51 +08:00
Ji qiu
cb64bc7d48 Merge pull request #20 from jiqiu2022/fix_bug
紧急bug修复
2025-06-27 18:26:17 +08:00
jiqiu2021
fc24ab3455 feat:自定义linker静态编译,解决报错 2025-06-27 17:47:26 +08:00
jiqiu2021
7e7b38caf6 feat:gadget全局配置 2025-06-27 17:31:01 +08:00
Ji qiu
b8b8dafed0 Update README.md 2025-06-27 17:02:08 +08:00
Ji qiu
7b7389c0a0 Update README.md 2025-06-27 16:59:57 +08:00
Ji qiu
122878f8fa Update README.md 2025-06-27 16:59:13 +08:00
Ji qiu
13ee77e96f Merge pull request #19 from jiqiu2022/fix_bug
Fix bug
2025-06-27 16:52:25 +08:00
jiqiu2021
e4ca55d6cd feat:add file 2025-06-27 16:49:41 +08:00
jiqiu2021
f955835df5 feat:文件选择器增加为三种 2025-06-27 16:46:09 +08:00
jiqiu2021
7e5a96cd78 feat:增加gadget配置界面 2025-06-27 16:15:38 +08:00
jiqiu2021
cc4fb60b7b feat:全局设置增加延迟注入秒数 2025-06-27 15:29:55 +08:00
jiqiu2021
0c713a26da fix:修复界面滚动导致选项消失bug 2025-06-27 15:08:59 +08:00
jiqiu2021
fee5600129 fix:自带的文件管理器无法打开so 2025-06-27 15:02:32 +08:00
Ji qiu
cec1db61fb Update README.md 2025-06-26 21:31:18 +08:00
Ji qiu
d201c72e22 Merge pull request #15 from jiqiu2022/linker_load
add README.md
2025-06-26 21:24:48 +08:00
jiqiu2021
7b7f6de828 add README.md 2025-06-26 21:18:00 +08:00
43 changed files with 3027 additions and 394 deletions

210
README.md
View File

@@ -1,65 +1,189 @@
# [Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
# Zygisk注入器 - 动动手指轻松注入并隐藏你的SO文件
## 前言
作者在去年发布了[Zygisk注入模块的实现](https://bbs.kanxue.com/thread-283790.htm)成功实现了模块的自动注入和Riru隐藏功能。
最新开发进度:**多模块注入完成maps隐藏注入完成**并且修复了riru内存泄漏问题 现在仍然面临soinfo链表遍历无法隐藏的情况
然而,每次更换目标应用都需要重新编译,操作繁琐,导致用户采用率不高。为了解决这个问题,作者对项目进行了全面重构,带来了以下改进:
我会初步进行框架层面的hook进行隐藏进一步使用自定义linker来伪装成系统库的加载。
- **图形化界面**:告别命令行,操作更直观
- **一键安装**:面具模块快速部署
- **配套管理APP**:轻松管理注入配置
项目https://github.com/Perfare/Zygisk-Il2CppDumper
项目已完全开源包含面具模块、管理APP以及所有打包脚本并配置了GitHub CI自动构建。欢迎各位开发者贡献代码提交PR。
本项目在原项目基础上做局部更改,请支持原项目作者劳动成果
### 版本规划&更新记录
版本规划:
- **v1.x**:专注功能添加,暂不考虑反检测
- **v2.x**实现各种检测绕过达到100%无痕注入
更新记录:
- **v1.2**: 增加gadget配置的自动生成支持脚本和server模式解决了若干bug增加了全局注入延迟设置
1. 安装[Magisk](https://github.com/topjohnwu/Magisk) v24以上版本并开启Zygisk
## 致谢
**项目地址**[https://github.com/jiqiu2022/Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
2. 生成模块
- GitHub Actions
1. Fork这个项目
2. 在你fork的项目中选择**Actions**选项卡
3. 在左边的侧边栏中,单击**Build**
4. 选择**Run workflow**
5. 输入游戏包名并点击**Run workflow**
6. 等待操作完成并下载
- Android Studio
1. 下载源码
2. 编辑`game.h`, 修改`GamePackageName`为游戏包名
3. 使用Android Studio运行gradle任务`:module:assembleRelease`编译zip包会生成在`out`文件夹下
特别感谢以下项目和开发者(按时间顺序):
3. 在Magisk里安装模块
- [Zygisk-Il2CppDumper](https://github.com/Perfare/Zygisk-Il2CppDumper) - 提供最原始的注入思路
- Riru - 提供隐藏思路
- 小佳大佬 - 提供App界面化的思路
- [soLoader](https://github.com/SoyBeanMilkx/soLoader) - 提供自定义linker注入的全部逻辑期待认识作者和作者一起继续完善
4. 将要注入的so放入到/data/local/tmp下修改为test.so
如果对自定义linker感兴趣的大佬推荐soLoader作者的文章https://yuuki.cool/2025/06/15/CustomLinker/
(部分手机第一次注入不会成功,请重启,再之后的注入会成功)
## 使用教程
多模块注入已经开发完成:
### 步骤一:下载并安装面具模块
1. 前往 [GitHub Release](https://github.com/jiqiu2022/Zygisk-MyInjector/tags) 页面下载最新版本的面具模块
本教程以 [v1.1.0](https://github.com/jiqiu2022/Zygisk-MyInjector/releases/tag/v1.1.0) 版本为例
2. 下载模块文件(如下图所示):
![CleanShot_2025_06_26_at_20_33_37](assets/CleanShot_2025_06_26_at_20_33_37-20250626203353-mvarjy3.png)
3. 安装到手机支持所有面具模块相关的APP如 KernelSU、APatch 等)
> **注意**如果您的APP不支持Zygisk注入请额外安装 Zygisk-Next 模块,否则可能导致注入失败。
### 步骤二准备SO文件并配置
#### 1. 添加SO文件
进入"SO文件管理",点击"增加SO文件"
![CleanShot_2025_06_26_at_20_36_35](assets/CleanShot_2025_06_26_at_20_36_35-20250626203644-vum10yb.png)
选择要注入的SO文件
![CleanShot_2025_06_26_at_20_37_10](assets/CleanShot_2025_06_26_at_20_37_10-20250626203718-fx50ptc.png)
本教程使用以下两个SO文件进行测试
- `libmylib.so`(全注入测试)
- `libgadget.so`(普通注入方式)
> **提示**后文将解释为什么自定义linker无法注入 `libgadget.so`。
#### 2. 处理原文件
选择SO后系统会询问是否删除原文件
![CleanShot_2025_06_26_at_20_38_42](assets/CleanShot_2025_06_26_at_20_38_42-20250626203859-532f217.png)
> **设计理念**此功能可防止某些APP扫描tmp目录中的SO文件进行风控检测。
添加成功后SO管理界面会显示已添加的文件
![CleanShot_2025_06_26_at_20_39_38](assets/CleanShot_2025_06_26_at_20_39_38-20250626204000-p3oe63m.png)
### 步骤三配置目标APP
本教程以珍惜大佬的Hunter为例演示配置过程。
1. 点击空白处,配置注入参数:
![CleanShot_2025_06_26_at_20_40_50](assets/CleanShot_2025_06_26_at_20_40_50-20250626204056-cn1613i.png)
2. 保存配置后,开启注入开关:
![CleanShot_2025_06_26_at_20_41_17](assets/CleanShot_2025_06_26_at_20_41_17-20250626204121-6k6wqym.png)
### 步骤四:验证注入效果
打开目标APP查看日志输出
![CleanShot_2025_06_26_at_20_42_17](assets/CleanShot_2025_06_26_at_20_42_17-20250626204224-gylrvtd.png)
成功打印"我已经成功加载",表明注入成功。
#### 测试Gadget注入
切换到gadget进行测试
![CleanShot_2025_06_26_at_20_43_27](assets/CleanShot_2025_06_26_at_20_43_27-20250626204331-wse9zyb.png)
> **特性**本注入模块支持同时注入多个SO文件。
![CleanShot_2025_06_26_at_20_44_09](assets/CleanShot_2025_06_26_at_20_44_09-20250626204414-lov146o.png)
注入成功:
![CleanShot_2025_06_26_at_20_44_19](assets/CleanShot_2025_06_26_at_20_44_19-20250626204426-2ubkjyi.png)
> **说明**由于使用的Hunter版本较旧可能无法检测到注入。建议使用新版本进行测试。
#### 测试其他注入方式
测试Riru Hide功能
![CleanShot_2025_06_26_at_20_46_07](assets/CleanShot_2025_06_26_at_20_46_07-20250626204610-d2o279q.png)
> **提示**:修改配置后,建议先关闭再开启注入开关,确保新配置生效。
![CleanShot_2025_06_26_at_20_46_45](assets/CleanShot_2025_06_26_at_20_46_45-20250626204649-sj0w84k.png)
Riru Hide成功生效。
测试自定义Linker加载
![CleanShot_2025_06_26_at_20_47_19](assets/CleanShot_2025_06_26_at_20_47_19-20250626204724-7dtm624.png)
加载成功:
![CleanShot_2025_06_26_at_20_47_34](assets/CleanShot_2025_06_26_at_20_47_34-20250626204744-v9hdf53.png)
## 编译指南
### 自动编译
参考项目中的 [CI配置文件](https://github.com/jiqiu2022/Zygisk-MyInjector/blob/main/.github/workflows/ci.yml)。
如果您fork了本项目只需在本地修改代码并push云端将自动编译。
### 本地编译
使用项目提供的 `build_all.sh` 脚本进行编译。
> **环境要求**需要Java 17环境。
## 工作原理
### 注入流程详解
1. **SO文件管理**
- 用户选择SO文件后APP自动将其复制到 `/data/adb/modules/module-name` 目录下进行统一管理
2. **配置生成**
- 完成APP配置后管理器在 `/data/adb/modules/module-name` 下创建config文件
- 面具模块启动时读取此配置执行SO加载
3. **注入激活**
- 开启注入时APP将管理器中的SO复制到目标APP目录并将config状态设置为开启
- 关闭注入时APP自动删除已复制的SO文件将config状态设置为关闭
## 为什么自定义linker不能注入gadget
```
void hack_start(const char *game_data_dir,JavaVM *vm) {
load_so(game_data_dir,vm,"test");
//如果要注入多个so那么就在这里不断的添加load_so函数即可
}
frida_KjnwyG_detect_location: assertion failed: (our_range != null)
Bail out! Frida:ERROR:../lib/KjnwyG/KjnwyG.vala:809:frida_KjnwyG_detect_location: assertion failed: (our_range != null)
Aborted
```
因为gadget的init_array里面其中有一个函数尝试使用maps和soinfo获取到自己模块的信息防止递归调用。
但是自定义linker的soinfo和内存段都是自己分配的导致gadget找不到自己发生了断言失败我们可以简单的去掉断言但是可能造成其他bug后续作者会和小佳一起修复。
当然还有一种复杂的方法就是用注入的so的soinfo替换成要注入的so的多见于360等壳子等后续作者会实现。
目前正在开发的分支:
1. 使用Java的System.load加载so
## 后续更新计划
2. 注入多个so的分支(已完成)
- **深度隐藏**联动内核模块提供maps等更深层次的隐藏功能
- **用户体验优化**
- 增加gadget配置一键生成功能
- 提供更友好的界面交互
- 支持批量配置管理
计划开发:
---
1. 第一步仿照Riru将注入的so进行内存上的初步隐藏可以对抗部分业务检测游戏安全相关已经补齐建议不要尝试已经开发完成
2. 第二步实现一个自定义的linker进行更深层次的注入隐藏
3. 第三步,搭配对应配套手机的内核模块对注入的模块进行进一步完美擦除,达到完美注入的目的
以此项目为脚手架的计划开发:
1. 一个全新的Frida框架保留大部分原生api并可以过任何相关注入检测
2. 一个全新的Trace框架高性能Trace速度是Stallker的60倍并且支持更全面的信息打印。具体效果可以参考看雪帖子
3. 一个全新的无痕调试框架支持像GDB一样调试没有ptrace痕迹两种思路进行无痕调试基于硬件断点以及基于VM
欢迎关注项目进展,期待您的贡献!

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 KiB

View File

@@ -105,10 +105,14 @@ else
exit 1
fi
# 创建 META-INF 目录Magisk 需要)
mkdir -p $TEMP_DIR/META-INF/com/google/android
touch $TEMP_DIR/META-INF/com/google/android/update-binary
touch $TEMP_DIR/META-INF/com/google/android/updater-script
# 复制 META-INF 目录Magisk 需要)
if [ -d "template/magisk_module/META-INF" ]; then
cp -r template/magisk_module/META-INF $TEMP_DIR/
echo -e " ${GREEN}✓ 复制 META-INF${NC}"
else
echo -e " ${RED}✗ 未找到 META-INF 模板${NC}"
exit 1
fi
# 打包
echo -e "\n${YELLOW}[5/5] 打包模块...${NC}"

View File

@@ -140,6 +140,12 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
RadioButton radioStandardInjection = dialogView.findViewById(R.id.radioStandardInjection);
RadioButton radioRiruInjection = dialogView.findViewById(R.id.radioRiruInjection);
RadioButton radioCustomLinkerInjection = dialogView.findViewById(R.id.radioCustomLinkerInjection);
RadioGroup gadgetConfigGroup = dialogView.findViewById(R.id.gadgetConfigGroup);
RadioButton radioNoGadget = dialogView.findViewById(R.id.radioNoGadget);
RadioButton radioUseGlobalGadget = dialogView.findViewById(R.id.radioUseGlobalGadget);
RadioButton radioUseCustomGadget = dialogView.findViewById(R.id.radioUseCustomGadget);
TextView tvGlobalGadgetInfo = dialogView.findViewById(R.id.tvGlobalGadgetInfo);
com.google.android.material.button.MaterialButton btnConfigureGadget = dialogView.findViewById(R.id.btnConfigureGadget);
appIcon.setImageDrawable(appInfo.getAppIcon());
appName.setText(appInfo.getAppName());
@@ -155,6 +161,74 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
radioStandardInjection.setChecked(true);
}
// Load gadget config
boolean useGlobalGadget = configManager.getAppUseGlobalGadget(appInfo.getPackageName());
ConfigManager.GadgetConfig appSpecificGadget = configManager.getAppGadgetConfig(appInfo.getPackageName());
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
// Update global gadget info
if (globalGadget != null) {
String info = "全局: " + globalGadget.gadgetName;
if (globalGadget.mode.equals("server")) {
info += " (端口: " + globalGadget.port + ")";
}
tvGlobalGadgetInfo.setText(info);
} else {
tvGlobalGadgetInfo.setText("未配置全局Gadget");
}
// Set initial radio selection
if (!useGlobalGadget && appSpecificGadget != null) {
radioUseCustomGadget.setChecked(true);
btnConfigureGadget.setVisibility(View.VISIBLE);
btnConfigureGadget.setEnabled(true);
} else if (useGlobalGadget && globalGadget != null) {
radioUseGlobalGadget.setChecked(true);
btnConfigureGadget.setVisibility(View.GONE);
} else {
radioNoGadget.setChecked(true);
btnConfigureGadget.setVisibility(View.GONE);
}
// Setup gadget radio group listener
gadgetConfigGroup.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.radioNoGadget) {
btnConfigureGadget.setVisibility(View.GONE);
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
} else if (checkedId == R.id.radioUseGlobalGadget) {
btnConfigureGadget.setVisibility(View.GONE);
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), true);
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
} else if (checkedId == R.id.radioUseCustomGadget) {
btnConfigureGadget.setVisibility(View.VISIBLE);
btnConfigureGadget.setEnabled(true);
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
}
});
// Configure button listener
btnConfigureGadget.setOnClickListener(v -> {
ConfigManager.GadgetConfig currentConfig = null;
if (!useGlobalGadget) {
currentConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
}
if (currentConfig == null) {
currentConfig = new ConfigManager.GadgetConfig();
}
GadgetConfigDialog dialog = new GadgetConfigDialog(
getContext(),
"配置" + appInfo.getAppName() + "的Gadget",
currentConfig,
config -> {
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
configManager.setAppGadgetConfig(appInfo.getPackageName(), config);
}
);
dialog.show();
});
// Setup SO list
List<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles();
List<ConfigManager.SoFile> appSoFiles = configManager.getAppSoFiles(appInfo.getPackageName());

View File

@@ -18,10 +18,14 @@ public class ConfigManager {
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 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 Gson gson;
private ModuleConfig config;
private final Object kpmLock = new Object(); // 用于同步 KPM 操作
static {
// Configure Shell to use root
@@ -163,20 +167,44 @@ public class ConfigManager {
}
}
// Ensure SO storage directory exists
Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
Shell.cmd("chmod 755 " + SO_STORAGE_DIR).exec();
// Copy SO file to our storage
Log.i(TAG, "Copying SO file from: " + originalPath + " to: " + storedPath);
Shell.Result result = Shell.cmd("cp \"" + originalPath + "\" \"" + storedPath + "\"").exec();
if (result.isSuccess()) {
// Verify the file was actually copied
Shell.Result verifyResult = Shell.cmd("test -f \"" + storedPath + "\" && echo 'exists'").exec();
if (!verifyResult.isSuccess() || verifyResult.getOut().isEmpty()) {
Log.e(TAG, "File copy appeared successful but file not found at: " + storedPath);
return;
}
// Set proper permissions for SO file (readable and executable)
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + storedPath + "\"").exec();
if (!chmodResult.isSuccess()) {
Log.e(TAG, "Failed to set permissions on SO file: " + String.join("\n", chmodResult.getErr()));
}
SoFile soFile = new SoFile();
soFile.name = fileName;
soFile.storedPath = storedPath;
soFile.originalPath = originalPath;
config.globalSoFiles.add(soFile);
Log.i(TAG, "Successfully added SO file: " + fileName + " to storage");
if (deleteOriginal) {
Shell.cmd("rm \"" + originalPath + "\"").exec();
Log.i(TAG, "Deleted original file: " + originalPath);
}
saveConfig();
} else {
Log.e(TAG, "Failed to copy SO file: " + String.join("\n", result.getErr()));
}
}
@@ -253,6 +281,176 @@ public class ConfigManager {
saveConfig();
}
public int getInjectionDelay() {
return config.injectionDelay;
}
public void setInjectionDelay(int delay) {
config.injectionDelay = delay;
saveConfig();
}
public GadgetConfig getAppGadgetConfig(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
// If no app config, return global gadget config
return config.globalGadgetConfig;
}
// If app is set to use global gadget, return global config
if (appConfig.useGlobalGadget) {
return config.globalGadgetConfig;
}
// Otherwise return app-specific gadget config
return appConfig.gadgetConfig;
}
public GadgetConfig getGlobalGadgetConfig() {
return config.globalGadgetConfig;
}
public void setGlobalGadgetConfig(GadgetConfig gadgetConfig) {
config.globalGadgetConfig = gadgetConfig;
saveConfig();
}
public boolean getAppUseGlobalGadget(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
return true; // Default to use global
}
return appConfig.useGlobalGadget;
}
public void setAppUseGlobalGadget(String packageName, boolean useGlobal) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
appConfig = new AppConfig();
config.perAppConfig.put(packageName, appConfig);
}
appConfig.useGlobalGadget = useGlobal;
saveConfig();
}
public void setAppGadgetConfig(String packageName, GadgetConfig gadgetConfig) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
appConfig = new AppConfig();
config.perAppConfig.put(packageName, appConfig);
}
// Remove old gadget from SO list if exists
if (appConfig.gadgetConfig != null) {
String oldGadgetName = appConfig.gadgetConfig.gadgetName;
appConfig.soFiles.removeIf(soFile -> soFile.name.equals(oldGadgetName));
}
appConfig.gadgetConfig = gadgetConfig;
// Add new gadget to SO list if configured
if (gadgetConfig != null) {
// Check if gadget SO file exists in global storage
String gadgetPath = SO_STORAGE_DIR + "/" + gadgetConfig.gadgetName;
Shell.Result checkResult = Shell.cmd("test -f \"" + gadgetPath + "\" && echo 'exists'").exec();
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
// Add gadget as a SO file
SoFile gadgetSoFile = new SoFile();
gadgetSoFile.name = gadgetConfig.gadgetName;
gadgetSoFile.storedPath = gadgetPath;
gadgetSoFile.originalPath = gadgetPath;
// Check if already in list
boolean alreadyExists = false;
for (SoFile soFile : appConfig.soFiles) {
if (soFile.name.equals(gadgetSoFile.name)) {
alreadyExists = true;
break;
}
}
if (!alreadyExists) {
appConfig.soFiles.add(gadgetSoFile);
Log.i(TAG, "Added gadget SO to app's SO list: " + gadgetSoFile.name);
}
} else {
Log.w(TAG, "Gadget SO file not found in storage: " + gadgetPath);
Log.w(TAG, "Please ensure " + gadgetConfig.gadgetName + " is added to SO library");
}
}
saveConfig();
// If app is enabled, deploy both gadget SO and config file
if (appConfig.enabled) {
if (gadgetConfig != null) {
deployGadgetConfigFile(packageName, gadgetConfig);
}
// Re-deploy all SO files including gadget
deploySoFilesToApp(packageName);
}
}
private void deployGadgetConfigFile(String packageName, GadgetConfig gadgetConfig) {
try {
// Create gadget config JSON
String configJson;
if ("script".equals(gadgetConfig.mode)) {
configJson = String.format(
"{\n" +
" \"interaction\": {\n" +
" \"type\": \"script\",\n" +
" \"path\": \"%s\"\n" +
" }\n" +
"}",
gadgetConfig.scriptPath
);
} else {
configJson = String.format(
"{\n" +
" \"interaction\": {\n" +
" \"type\": \"listen\",\n" +
" \"address\": \"%s\",\n" +
" \"port\": %d,\n" +
" \"on_port_conflict\": \"%s\",\n" +
" \"on_load\": \"%s\"\n" +
" }\n" +
"}",
gadgetConfig.address,
gadgetConfig.port,
gadgetConfig.onPortConflict,
gadgetConfig.onLoad
);
}
// Write to temp file
String tempFile = context.getCacheDir() + "/" + gadgetConfig.gadgetName + ".config";
java.io.FileWriter writer = new java.io.FileWriter(tempFile);
writer.write(configJson);
writer.close();
// Copy to app's files directory
String filesDir = "/data/data/" + packageName + "/files";
String gadgetConfigName = gadgetConfig.gadgetName.replace(".so", ".config.so");
String targetPath = filesDir + "/" + gadgetConfigName;
Shell.Result copyResult = Shell.cmd("cp " + tempFile + " " + targetPath).exec();
if (copyResult.isSuccess()) {
// Set permissions
Shell.cmd("chmod 644 " + targetPath).exec();
Log.i(TAG, "Deployed gadget config to: " + targetPath);
} else {
Log.e(TAG, "Failed to deploy gadget config: " + String.join("\n", copyResult.getErr()));
}
// Clean up temp file
new java.io.File(tempFile).delete();
} catch (Exception e) {
Log.e(TAG, "Failed to create gadget config file", e);
}
}
// Copy SO files directly to app's data directory
private void deploySoFilesToApp(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName);
@@ -270,28 +468,36 @@ public class ConfigManager {
// Create files directory in app's data dir
String filesDir = "/data/data/" + packageName + "/files";
// Use su -c for better compatibility
Shell.Result mkdirResult = Shell.cmd("su -c 'mkdir -p " + filesDir + "'").exec();
Log.i(TAG, "Deploying SO files to: " + filesDir);
// Create directory without su -c for better compatibility
Shell.Result mkdirResult = Shell.cmd("mkdir -p " + filesDir).exec();
if (!mkdirResult.isSuccess()) {
Log.e(TAG, "Failed to create directory: " + filesDir);
Log.e(TAG, "Error: " + String.join("\n", mkdirResult.getErr()));
// Try without su -c
mkdirResult = Shell.cmd("mkdir -p " + filesDir).exec();
if (!mkdirResult.isSuccess()) {
Log.e(TAG, "Also failed without su -c");
return;
}
return;
}
// Set proper permissions and ownership
Shell.cmd("chmod 755 " + filesDir).exec();
// Set proper permissions and ownership for the files directory
Shell.cmd("chmod 771 " + filesDir).exec();
// Get UID for the package
// Get UID and GID for the package
Shell.Result uidResult = Shell.cmd("stat -c %u /data/data/" + packageName).exec();
String uid = "";
if (uidResult.isSuccess() && !uidResult.getOut().isEmpty()) {
uid = uidResult.getOut().get(0).trim();
Log.i(TAG, "Package UID: " + uid);
// Set ownership of files directory to match app
Shell.Result chownDirResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + filesDir + "\"").exec();
if (!chownDirResult.isSuccess()) {
Log.e(TAG, "Failed to set directory ownership: " + String.join("\n", chownDirResult.getErr()));
}
// Set SELinux context for the directory
Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + filesDir + "\"").exec();
} else {
Log.e(TAG, "Failed to get package UID");
}
// Copy each SO file configured for this app
@@ -303,44 +509,77 @@ public class ConfigManager {
Shell.Result checkResult = Shell.cmd("test -f \"" + soFile.storedPath + "\" && echo 'exists'").exec();
if (!checkResult.isSuccess() || checkResult.getOut().isEmpty()) {
Log.e(TAG, "Source SO file not found: " + soFile.storedPath);
// Log more details about the missing file
Shell.Result lsResult = Shell.cmd("ls -la \"" + SO_STORAGE_DIR + "\"").exec();
Log.e(TAG, "Contents of SO storage dir: " + String.join("\n", lsResult.getOut()));
continue;
}
Log.i(TAG, "Copying: " + soFile.storedPath + " to " + destPath);
// Copy file using cat to avoid permission issues
String copyCmd = "cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"";
Shell.Result result = Shell.cmd(copyCmd).exec();
// First, ensure the destination directory exists and has proper permissions
Shell.cmd("mkdir -p \"" + filesDir + "\"").exec();
Shell.cmd("chmod 755 \"" + filesDir + "\"").exec();
// Copy file using cp with force flag
Shell.Result result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
if (!result.isSuccess()) {
Log.e(TAG, "Failed with cat, trying cp");
// Fallback to cp
result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
Log.e(TAG, "Failed with cp, trying cat method");
Log.e(TAG, "cp error: " + String.join("\n", result.getErr()));
// Fallback to cat method
result = Shell.cmd("cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"").exec();
if (!result.isSuccess()) {
Log.e(TAG, "Also failed with cat method");
Log.e(TAG, "cat error: " + String.join("\n", result.getErr()));
}
}
// Set permissions
Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
// Set permissions - SO files need to be readable and executable
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
if (!chmodResult.isSuccess()) {
Log.e(TAG, "Failed to set permissions: " + String.join("\n", chmodResult.getErr()));
}
// Set ownership if we have the UID
// Set ownership to match the app's UID
if (!uid.isEmpty()) {
Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
Shell.Result chownResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
if (!chownResult.isSuccess()) {
Log.e(TAG, "Failed to set ownership: " + String.join("\n", chownResult.getErr()));
// Try alternative method
Shell.cmd("chown " + uid + ".app_" + uid + " \"" + destPath + "\"").exec();
}
}
// Verify the file was copied
Shell.Result verifyResult = Shell.cmd("ls -la \"" + destPath + "\" 2>/dev/null").exec();
// Set SELinux context to match app's data files
Shell.Result contextResult = Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + destPath + "\"").exec();
if (!contextResult.isSuccess()) {
Log.w(TAG, "Failed to set SELinux context (this may be normal on some devices)");
}
// Verify the file was copied with correct permissions
Shell.Result verifyResult = Shell.cmd("ls -laZ \"" + destPath + "\" 2>/dev/null").exec();
if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
} else {
Log.e(TAG, "Failed to verify SO file copy: " + destPath);
// Try another verification method
Shell.Result sizeResult = Shell.cmd("stat -c %s \"" + destPath + "\" 2>/dev/null").exec();
if (sizeResult.isSuccess() && !sizeResult.getOut().isEmpty()) {
Log.i(TAG, "File exists with size: " + sizeResult.getOut().get(0) + " bytes");
// Fallback verification without SELinux context
verifyResult = Shell.cmd("ls -la \"" + destPath + "\" 2>/dev/null").exec();
if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
} else {
Log.e(TAG, "Failed to verify SO file copy: " + destPath);
}
}
}
Log.i(TAG, "Deployment complete for: " + packageName);
// Deploy gadget config if configured
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
if (gadgetToUse != null) {
deployGadgetConfigFile(packageName, gadgetToUse);
}
}
// Clean up deployed SO files when app is disabled
@@ -386,6 +625,24 @@ public class ConfigManager {
}
}
// Clean up gadget config file if exists
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
if (gadgetToUse != null) {
String gadgetConfigName = gadgetToUse.gadgetName.replace(".so", ".config.so");
String configPath = filesDir + "/" + gadgetConfigName;
Shell.Result checkConfigResult = Shell.cmd("test -f \"" + configPath + "\" && echo 'exists'").exec();
if (checkConfigResult.isSuccess() && !checkConfigResult.getOut().isEmpty()) {
Shell.Result deleteResult = Shell.cmd("rm -f \"" + configPath + "\"").exec();
if (deleteResult.isSuccess()) {
Log.i(TAG, "Deleted gadget config: " + configPath);
} else {
// Try with su -c
Shell.cmd("su -c 'rm -f \"" + configPath + "\"'").exec();
}
}
}
Log.i(TAG, "Cleanup complete for: " + packageName);
}
@@ -398,18 +655,274 @@ public class ConfigManager {
}
}
// ==================== KPM Module Management ====================
/**
* 检查 KPM 模块是否已加载
*/
public boolean isKpmModuleLoaded() {
Shell.Result result = Shell.cmd("lsmod | grep " + KPM_MODULE_NAME).exec();
return result.isSuccess() && !result.getOut().isEmpty();
}
/**
* 加载 KPM 模块
*/
public boolean loadKpmModule() {
if (!isRootAvailable()) {
Log.e(TAG, "Root access not available!");
return false;
}
// 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
public static class ModuleConfig {
public boolean enabled = true;
public boolean hideInjection = false;
public int injectionDelay = 2; // Default 2 seconds
public List<SoFile> globalSoFiles = new ArrayList<>();
public Map<String, AppConfig> perAppConfig = new HashMap<>();
public GadgetConfig globalGadgetConfig = null; // Global gadget configuration
}
public static class AppConfig {
public boolean enabled = false;
public List<SoFile> soFiles = new ArrayList<>();
public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker"
public GadgetConfig gadgetConfig = null;
public boolean useGlobalGadget = true; // Whether to use global gadget settings
}
public static class SoFile {
@@ -425,4 +938,17 @@ public class ConfigManager {
return false;
}
}
public static class GadgetConfig {
public String mode = "server"; // "server" or "script"
// Server mode config
public String address = "0.0.0.0";
public int port = 27042;
public String onPortConflict = "fail";
public String onLoad = "wait";
// Script mode config
public String scriptPath = "/data/local/tmp/script.js";
// Common config
public String gadgetName = "libgadget.so";
}
}

View File

@@ -0,0 +1,171 @@
package com.jiqiu.configapp;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import com.topjohnwu.superuser.Shell;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class FileUtils {
private static final String TAG = "FileUtils";
/**
* Get real file path from URI, handling both file:// and content:// URIs
* @param context Context
* @param uri The URI to resolve
* @return The real file path, or null if unable to resolve
*/
public static String getRealPathFromUri(Context context, Uri uri) {
if (uri == null) return null;
String scheme = uri.getScheme();
if (scheme == null) return null;
// Handle file:// URIs
if ("file".equals(scheme)) {
return uri.getPath();
}
// Handle content:// URIs
if ("content".equals(scheme)) {
// For content URIs, we need to copy the file to a temporary location
return copyFileFromContentUri(context, uri);
}
// Try direct path extraction as fallback
String path = uri.getPath();
if (path != null) {
// Some file managers return paths like /external_files/...
// Try to resolve these to actual paths
if (path.contains(":")) {
String[] parts = path.split(":");
if (parts.length == 2) {
String type = parts[0];
String relativePath = parts[1];
// Common storage locations
if (type.endsWith("/primary")) {
return "/storage/emulated/0/" + relativePath;
} else if (type.contains("external")) {
return "/storage/emulated/0/" + relativePath;
}
}
}
// Remove any file:// prefix
if (path.startsWith("file://")) {
path = path.substring(7);
}
// Check if the path exists
Shell.Result result = Shell.cmd("test -f \"" + path + "\" && echo 'exists'").exec();
if (result.isSuccess() && !result.getOut().isEmpty()) {
return path;
}
}
return null;
}
/**
* Copy a file from content URI to temporary location
* @param context Context
* @param uri Content URI
* @return Path to copied file, or null on failure
*/
private static String copyFileFromContentUri(Context context, Uri uri) {
ContentResolver resolver = context.getContentResolver();
String fileName = getFileName(context, uri);
if (fileName == null || !fileName.endsWith(".so")) {
fileName = "temp_" + System.currentTimeMillis() + ".so";
}
// Create temp directory
File tempDir = new File(context.getCacheDir(), "so_temp");
if (!tempDir.exists()) {
tempDir.mkdirs();
}
File tempFile = new File(tempDir, fileName);
try (InputStream inputStream = resolver.openInputStream(uri);
OutputStream outputStream = new FileOutputStream(tempFile)) {
if (inputStream == null) {
Log.e(TAG, "Unable to open input stream for URI: " + uri);
return null;
}
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// Make file readable
tempFile.setReadable(true, false);
// First copy to /data/local/tmp as a temporary location
String tempTargetPath = "/data/local/tmp/" + fileName;
Shell.Result result = Shell.cmd(
"cp \"" + tempFile.getAbsolutePath() + "\" \"" + tempTargetPath + "\"",
"chmod 644 \"" + tempTargetPath + "\""
).exec();
// Clean up temp file
tempFile.delete();
if (result.isSuccess()) {
// Return the temporary path - it will be moved to the proper location by addGlobalSoFile
return tempTargetPath;
} else {
Log.e(TAG, "Failed to copy file to /data/local/tmp/");
return null;
}
} catch (IOException e) {
Log.e(TAG, "Error copying file from content URI", e);
return null;
}
}
/**
* Get file name from URI
* @param context Context
* @param uri URI to get name from
* @return File name or null
*/
private static String getFileName(Context context, Uri uri) {
String fileName = null;
if ("content".equals(uri.getScheme())) {
try (Cursor cursor = context.getContentResolver().query(
uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (nameIndex >= 0) {
fileName = cursor.getString(nameIndex);
}
}
} catch (Exception e) {
Log.e(TAG, "Error getting file name from URI", e);
}
}
if (fileName == null) {
fileName = uri.getLastPathSegment();
}
return fileName;
}
}

View File

@@ -0,0 +1,676 @@
package com.jiqiu.configapp;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.LinearLayout;
import android.content.Intent;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.content.ContentResolver;
import android.database.Cursor;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.json.JSONException;
import org.json.JSONObject;
public class GadgetConfigDialog extends DialogFragment {
// UI elements
private RadioGroup modeRadioGroup;
private RadioButton radioModeServer;
private RadioButton radioModeScript;
private LinearLayout serverModeLayout;
private LinearLayout scriptModeLayout;
private RadioGroup addressRadioGroup;
private RadioButton radioAddressAll;
private RadioButton radioAddressLocal;
private RadioButton radioAddressCustom;
private EditText editCustomAddress;
private EditText editPort;
private RadioGroup portConflictRadioGroup;
private RadioButton radioConflictFail;
private RadioButton radioConflictPickNext;
private RadioGroup onLoadRadioGroup;
private RadioButton radioLoadWait;
private RadioButton radioLoadResume;
private EditText editScriptPath;
private EditText editGadgetName;
private EditText editJsonPreview;
// Configuration data
private ConfigManager.GadgetConfig config;
private OnGadgetConfigListener listener;
private String customTitle;
// Flag to prevent recursive updates
private boolean isUpdatingUI = false;
// Activity result launchers
private ActivityResultLauncher<Intent> fileBrowserLauncher;
private ActivityResultLauncher<Intent> filePickerLauncher;
public interface OnGadgetConfigListener {
void onGadgetConfigSaved(ConfigManager.GadgetConfig config);
}
public static GadgetConfigDialog newInstance(ConfigManager.GadgetConfig config) {
GadgetConfigDialog dialog = new GadgetConfigDialog();
dialog.config = config != null ? config : new ConfigManager.GadgetConfig();
return dialog;
}
// Constructor for non-fragment usage
public GadgetConfigDialog(Context context, String title, ConfigManager.GadgetConfig config, OnGadgetConfigListener listener) {
// This constructor is for compatibility with direct dialog creation
// The actual dialog will be created in show() method
this.savedContext = context;
this.customTitle = title;
this.config = config != null ? config : new ConfigManager.GadgetConfig();
this.listener = listener;
}
// Default constructor required for DialogFragment
public GadgetConfigDialog() {
// Empty constructor required
}
public void setOnGadgetConfigListener(OnGadgetConfigListener listener) {
this.listener = listener;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initialize file browser launcher
fileBrowserLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == android.app.Activity.RESULT_OK && result.getData() != null) {
String selectedPath = result.getData().getStringExtra("selected_path");
if (selectedPath != null) {
editScriptPath.setText(selectedPath);
config.scriptPath = selectedPath;
updateJsonPreview();
}
}
}
);
// Initialize file picker launcher
filePickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == android.app.Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
if (uri != null) {
String path = getPathFromUri(uri);
if (path != null) {
editScriptPath.setText(path);
config.scriptPath = path;
updateJsonPreview();
} else {
Toast.makeText(getContext(), "无法获取文件路径", Toast.LENGTH_SHORT).show();
}
}
}
}
);
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_gadget_config, null);
initViews(view);
loadConfig();
setupListeners();
updateJsonPreview();
String title = customTitle != null ? customTitle : "Gadget 配置";
return new MaterialAlertDialogBuilder(getContext())
.setTitle(title)
.setView(view)
.setPositiveButton("保存", (dialog, which) -> saveConfig())
.setNegativeButton("取消", null)
.create();
}
private void initViews(View view) {
modeRadioGroup = view.findViewById(R.id.modeRadioGroup);
radioModeServer = view.findViewById(R.id.radioModeServer);
radioModeScript = view.findViewById(R.id.radioModeScript);
serverModeLayout = view.findViewById(R.id.serverModeLayout);
scriptModeLayout = view.findViewById(R.id.scriptModeLayout);
addressRadioGroup = view.findViewById(R.id.addressRadioGroup);
radioAddressAll = view.findViewById(R.id.radioAddressAll);
radioAddressLocal = view.findViewById(R.id.radioAddressLocal);
radioAddressCustom = view.findViewById(R.id.radioAddressCustom);
editCustomAddress = view.findViewById(R.id.editCustomAddress);
editPort = view.findViewById(R.id.editPort);
portConflictRadioGroup = view.findViewById(R.id.portConflictRadioGroup);
radioConflictFail = view.findViewById(R.id.radioConflictFail);
radioConflictPickNext = view.findViewById(R.id.radioConflictPickNext);
onLoadRadioGroup = view.findViewById(R.id.onLoadRadioGroup);
radioLoadWait = view.findViewById(R.id.radioLoadWait);
radioLoadResume = view.findViewById(R.id.radioLoadResume);
editScriptPath = view.findViewById(R.id.editScriptPath);
editGadgetName = view.findViewById(R.id.editGadgetName);
editJsonPreview = view.findViewById(R.id.editJsonPreview);
// File select button
View btnSelectScript = view.findViewById(R.id.btnSelectScript);
if (btnSelectScript != null) {
btnSelectScript.setOnClickListener(v -> selectScriptFile());
}
}
private void loadConfig() {
isUpdatingUI = true;
// Load mode
if ("script".equals(config.mode)) {
radioModeScript.setChecked(true);
serverModeLayout.setVisibility(View.GONE);
scriptModeLayout.setVisibility(View.VISIBLE);
} else {
radioModeServer.setChecked(true);
serverModeLayout.setVisibility(View.VISIBLE);
scriptModeLayout.setVisibility(View.GONE);
}
// Load address
if ("127.0.0.1".equals(config.address)) {
radioAddressLocal.setChecked(true);
} else if ("0.0.0.0".equals(config.address)) {
radioAddressAll.setChecked(true);
} else {
radioAddressCustom.setChecked(true);
editCustomAddress.setText(config.address);
editCustomAddress.setEnabled(true);
}
// Load port
editPort.setText(String.valueOf(config.port));
// Load port conflict handling
if ("pick-next".equals(config.onPortConflict)) {
radioConflictPickNext.setChecked(true);
} else {
radioConflictFail.setChecked(true);
}
// Load on load handling
if ("resume".equals(config.onLoad)) {
radioLoadResume.setChecked(true);
} else {
radioLoadWait.setChecked(true);
}
// Load script path
editScriptPath.setText(config.scriptPath);
// Load gadget name
editGadgetName.setText(config.gadgetName);
isUpdatingUI = false;
}
private void setupListeners() {
// Mode radio group listener
modeRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
if (!isUpdatingUI) {
if (checkedId == R.id.radioModeScript) {
config.mode = "script";
serverModeLayout.setVisibility(View.GONE);
scriptModeLayout.setVisibility(View.VISIBLE);
} else {
config.mode = "server";
serverModeLayout.setVisibility(View.VISIBLE);
scriptModeLayout.setVisibility(View.GONE);
}
updateJsonPreview();
}
});
// Address radio group listener
addressRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
if (!isUpdatingUI) {
if (checkedId == R.id.radioAddressCustom) {
editCustomAddress.setEnabled(true);
editCustomAddress.requestFocus();
} else {
editCustomAddress.setEnabled(false);
if (checkedId == R.id.radioAddressAll) {
config.address = "0.0.0.0";
} else if (checkedId == R.id.radioAddressLocal) {
config.address = "127.0.0.1";
}
updateJsonPreview();
}
}
});
// Custom address text watcher
editCustomAddress.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (!isUpdatingUI && radioAddressCustom.isChecked()) {
config.address = s.toString().trim();
updateJsonPreview();
}
}
});
// Port text watcher
editPort.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (!isUpdatingUI) {
try {
int port = Integer.parseInt(s.toString());
if (port >= 1 && port <= 65535) {
config.port = port;
updateJsonPreview();
}
} catch (NumberFormatException e) {
// Ignore invalid input
}
}
}
});
// Port conflict radio group listener
portConflictRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
if (!isUpdatingUI) {
config.onPortConflict = (checkedId == R.id.radioConflictPickNext) ? "pick-next" : "fail";
updateJsonPreview();
}
});
// On load radio group listener
onLoadRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
if (!isUpdatingUI) {
config.onLoad = (checkedId == R.id.radioLoadResume) ? "resume" : "wait";
updateJsonPreview();
}
});
// Script path text watcher
editScriptPath.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (!isUpdatingUI) {
config.scriptPath = s.toString().trim();
updateJsonPreview();
}
}
});
// Gadget name text watcher
editGadgetName.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (!isUpdatingUI) {
config.gadgetName = s.toString().trim();
}
}
});
// JSON preview text watcher
editJsonPreview.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (!isUpdatingUI) {
parseJsonAndUpdateUI(s.toString());
}
}
});
}
private void updateJsonPreview() {
if (isUpdatingUI) return;
try {
JSONObject root = new JSONObject();
JSONObject interaction = new JSONObject();
if ("script".equals(config.mode)) {
interaction.put("type", "script");
interaction.put("path", config.scriptPath);
} else {
interaction.put("type", "listen");
interaction.put("address", config.address);
interaction.put("port", config.port);
interaction.put("on_port_conflict", config.onPortConflict);
interaction.put("on_load", config.onLoad);
}
root.put("interaction", interaction);
isUpdatingUI = true;
editJsonPreview.setText(root.toString(2));
isUpdatingUI = false;
} catch (JSONException e) {
// Should not happen
e.printStackTrace();
}
}
private void parseJsonAndUpdateUI(String json) {
try {
JSONObject root = new JSONObject(json);
JSONObject interaction = root.getJSONObject("interaction");
isUpdatingUI = true;
// Update mode
String type = interaction.getString("type");
if ("script".equals(type)) {
config.mode = "script";
radioModeScript.setChecked(true);
serverModeLayout.setVisibility(View.GONE);
scriptModeLayout.setVisibility(View.VISIBLE);
// Update script path
if (interaction.has("path")) {
config.scriptPath = interaction.getString("path");
editScriptPath.setText(config.scriptPath);
}
} else {
config.mode = "server";
radioModeServer.setChecked(true);
serverModeLayout.setVisibility(View.VISIBLE);
scriptModeLayout.setVisibility(View.GONE);
// Update address
String address = interaction.getString("address");
config.address = address;
if ("0.0.0.0".equals(address)) {
radioAddressAll.setChecked(true);
editCustomAddress.setEnabled(false);
} else if ("127.0.0.1".equals(address)) {
radioAddressLocal.setChecked(true);
editCustomAddress.setEnabled(false);
} else {
radioAddressCustom.setChecked(true);
editCustomAddress.setText(address);
editCustomAddress.setEnabled(true);
}
// Update port
config.port = interaction.getInt("port");
editPort.setText(String.valueOf(config.port));
// Update port conflict
String onPortConflict = interaction.getString("on_port_conflict");
config.onPortConflict = onPortConflict;
if ("pick-next".equals(onPortConflict)) {
radioConflictPickNext.setChecked(true);
} else {
radioConflictFail.setChecked(true);
}
// Update on load
String onLoad = interaction.getString("on_load");
config.onLoad = onLoad;
if ("resume".equals(onLoad)) {
radioLoadResume.setChecked(true);
} else {
radioLoadWait.setChecked(true);
}
}
isUpdatingUI = false;
} catch (JSONException e) {
// Invalid JSON, ignore
}
}
private void saveConfig() {
if (listener != null) {
// Ensure gadget name is not empty
if (config.gadgetName == null || config.gadgetName.trim().isEmpty()) {
config.gadgetName = "libgadget.so";
}
listener.onGadgetConfigSaved(config);
}
}
private void selectScriptFile() {
String[] options = {"浏览文件系统", "从外部文件管理器选择", "手动输入路径"};
new MaterialAlertDialogBuilder(requireContext())
.setTitle("选择 Script 文件")
.setItems(options, (dialog, which) -> {
if (which == 0) {
openFileBrowser();
} else if (which == 1) {
openFilePicker();
} else {
showPathInputDialog();
}
})
.show();
}
private void openFileBrowser() {
// Show path selection dialog first
String[] paths = {
"/data/local/tmp",
"/sdcard",
"/sdcard/Download",
"/storage/emulated/0",
"自定义路径..."
};
new MaterialAlertDialogBuilder(requireContext())
.setTitle("选择起始目录")
.setItems(paths, (dialog, which) -> {
if (which == paths.length - 1) {
// Custom path
showCustomPathDialog();
} else {
Intent intent = new Intent(getContext(), FileBrowserActivity.class);
intent.putExtra(FileBrowserActivity.EXTRA_START_PATH, paths[which]);
intent.putExtra(FileBrowserActivity.EXTRA_FILE_FILTER, ".js");
fileBrowserLauncher.launch(intent);
}
})
.show();
}
private void showCustomPathDialog() {
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
android.widget.EditText editText = view.findViewById(android.R.id.edit);
editText.setText("/");
editText.setHint("输入起始路径");
new MaterialAlertDialogBuilder(requireContext())
.setTitle("自定义起始路径")
.setView(view)
.setPositiveButton("确定", (dialog, which) -> {
String path = editText.getText().toString().trim();
if (!path.isEmpty()) {
Intent intent = new Intent(getContext(), FileBrowserActivity.class);
intent.putExtra(FileBrowserActivity.EXTRA_START_PATH, path);
intent.putExtra(FileBrowserActivity.EXTRA_FILE_FILTER, ".js");
fileBrowserLauncher.launch(intent);
}
})
.setNegativeButton("取消", null)
.show();
}
private void openFilePicker() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Add MIME types that might help filter JS files
String[] mimeTypes = {"text/javascript", "application/javascript", "text/plain", "*/*"};
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
// Suggest starting location
intent.putExtra("android.provider.extra.INITIAL_URI",
android.net.Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload"));
filePickerLauncher.launch(intent);
}
private void showPathInputDialog() {
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
android.widget.EditText editText = view.findViewById(android.R.id.edit);
editText.setText("/data/local/tmp/");
editText.setHint("/data/local/tmp/script.js");
new MaterialAlertDialogBuilder(requireContext())
.setTitle("输入 Script 文件路径")
.setView(view)
.setPositiveButton("确定", (dialog, which) -> {
String path = editText.getText().toString().trim();
if (!path.isEmpty()) {
editScriptPath.setText(path);
config.scriptPath = path;
updateJsonPreview();
}
})
.setNegativeButton("取消", null)
.show();
}
// Show method for non-fragment usage
public void show() {
if (getContext() == null) {
throw new IllegalStateException("Context is required for non-fragment usage");
}
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_gadget_config, null);
initViews(view);
// Initialize config if null
if (config == null) {
config = new ConfigManager.GadgetConfig();
}
loadConfig();
setupListeners();
updateJsonPreview();
String title = customTitle != null ? customTitle : "Gadget 配置";
new MaterialAlertDialogBuilder(getContext())
.setTitle(title)
.setView(view)
.setPositiveButton("保存", (dialog, which) -> saveConfig())
.setNegativeButton("取消", null)
.show();
}
private Context savedContext;
@Override
public Context getContext() {
Context context = super.getContext();
if (context == null) {
return savedContext;
}
return context;
}
// Constructor for non-fragment usage needs to save context
public void setContext(Context context) {
this.savedContext = context;
}
private String getPathFromUri(Uri uri) {
String path = null;
// Try to get path from MediaStore
if ("content".equals(uri.getScheme())) {
try {
ContentResolver resolver = getContext().getContentResolver();
try (Cursor cursor = resolver.query(uri, new String[]{"_data"}, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int columnIndex = cursor.getColumnIndex("_data");
if (columnIndex != -1) {
path = cursor.getString(columnIndex);
}
}
}
} catch (Exception e) {
// Ignore
}
// Try DocumentsContract if MediaStore fails
if (path == null && DocumentsContract.isDocumentUri(getContext(), uri)) {
try {
String docId = DocumentsContract.getDocumentId(uri);
if (uri.getAuthority().equals("com.android.externalstorage.documents")) {
String[] split = docId.split(":");
if (split.length >= 2) {
String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
path = "/storage/emulated/0/" + split[1];
} else {
path = "/storage/" + type + "/" + split[1];
}
}
}
} catch (Exception e) {
// Ignore
}
}
} else if ("file".equals(uri.getScheme())) {
path = uri.getPath();
}
return path;
}
}

View 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());
}
});
}
}
}

View 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; // 是否是固定隐藏项(不可取消)
}
}

View File

@@ -19,6 +19,7 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
private AppListFragment appListFragment;
private SettingsFragment settingsFragment;
private SoManagerFragment soManagerFragment;
private KpmHideFragment kpmHideFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -53,6 +54,9 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
} else if (itemId == R.id.navigation_so_manager) {
showSoManagerFragment();
return true;
} else if (itemId == R.id.navigation_kpm_hide) {
showKpmHideFragment();
return true;
} else if (itemId == R.id.navigation_settings) {
showSettingsFragment();
return true;
@@ -75,6 +79,13 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
showFragment(soManagerFragment);
}
private void showKpmHideFragment() {
if (kpmHideFragment == null) {
kpmHideFragment = new KpmHideFragment();
}
showFragment(kpmHideFragment);
}
private void showSettingsFragment() {
if (settingsFragment == null) {
settingsFragment = new SettingsFragment();

View File

@@ -8,6 +8,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.EditText;
import android.text.TextWatcher;
import android.text.Editable;
import android.widget.TextView;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -24,6 +29,10 @@ public class SettingsFragment extends Fragment {
private RadioGroup radioGroupFilter;
private RadioButton radioShowAll;
private RadioButton radioHideSystem;
private EditText editInjectionDelay;
private TextView tvGlobalGadgetStatus;
private Button btnConfigureGlobalGadget;
private ConfigManager configManager;
private SharedPreferences sharedPreferences;
private OnSettingsChangeListener settingsChangeListener;
@@ -53,6 +62,11 @@ public class SettingsFragment extends Fragment {
radioGroupFilter = view.findViewById(R.id.radio_group_filter);
radioShowAll = view.findViewById(R.id.radio_show_all);
radioHideSystem = view.findViewById(R.id.radio_hide_system);
editInjectionDelay = view.findViewById(R.id.editInjectionDelay);
tvGlobalGadgetStatus = view.findViewById(R.id.tvGlobalGadgetStatus);
btnConfigureGlobalGadget = view.findViewById(R.id.btnConfigureGlobalGadget);
configManager = new ConfigManager(getContext());
}
private void initSharedPreferences() {
@@ -67,6 +81,13 @@ public class SettingsFragment extends Fragment {
} else {
radioShowAll.setChecked(true);
}
// Load injection delay
int injectionDelay = configManager.getInjectionDelay();
editInjectionDelay.setText(String.valueOf(injectionDelay));
// Load global gadget status
updateGlobalGadgetStatus();
}
private void setupListeners() {
@@ -86,6 +107,37 @@ public class SettingsFragment extends Fragment {
}
}
});
// Injection delay listener
editInjectionDelay.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
String text = s.toString().trim();
if (!text.isEmpty()) {
try {
int delay = Integer.parseInt(text);
// Limit delay between 0 and 60 seconds
if (delay < 0) delay = 0;
if (delay > 60) delay = 60;
configManager.setInjectionDelay(delay);
} catch (NumberFormatException e) {
// Ignore invalid input
}
}
}
});
// Global gadget configuration button
btnConfigureGlobalGadget.setOnClickListener(v -> {
showGlobalGadgetConfigDialog();
});
}
public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
@@ -95,4 +147,34 @@ public class SettingsFragment extends Fragment {
public boolean isHideSystemApps() {
return sharedPreferences.getBoolean(KEY_HIDE_SYSTEM_APPS, false);
}
private void updateGlobalGadgetStatus() {
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
if (globalGadget != null) {
String status = "已配置: " + globalGadget.gadgetName;
if (globalGadget.mode.equals("server")) {
status += " (Server模式, 端口: " + globalGadget.port + ")";
} else {
status += " (Script模式)";
}
tvGlobalGadgetStatus.setText(status);
} else {
tvGlobalGadgetStatus.setText("未配置");
}
}
private void showGlobalGadgetConfigDialog() {
// Use existing GadgetConfigDialog
GadgetConfigDialog dialog = new GadgetConfigDialog(
getContext(),
"全局Gadget配置",
configManager.getGlobalGadgetConfig(),
gadgetConfig -> {
// Save global gadget configuration
configManager.setGlobalGadgetConfig(gadgetConfig);
updateGlobalGadgetStatus();
}
);
dialog.show();
}
}

View File

@@ -196,6 +196,12 @@ public class SoManagerFragment extends Fragment {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Add MIME types that might help filter SO files
String[] mimeTypes = {"application/octet-stream", "application/x-sharedlib", "*/*"};
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
// Suggest starting location
intent.putExtra("android.provider.extra.INITIAL_URI",
android.net.Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload"));
filePickerLauncher.launch(intent);
}
@@ -219,14 +225,12 @@ public class SoManagerFragment extends Fragment {
}
private void handleFileSelection(Uri uri) {
// Get real path from URI
String path = uri.getPath();
// Get real path from URI using proper URI handling
String path = FileUtils.getRealPathFromUri(requireContext(), uri);
if (path != null) {
// Remove the file:// prefix if present
if (path.startsWith("file://")) {
path = path.substring(7);
}
showDeleteOriginalDialog(path);
} else {
Toast.makeText(getContext(), "无法获取文件路径,请尝试其他方式", Toast.LENGTH_SHORT).show();
}
}

View File

@@ -1,10 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
@@ -64,7 +68,7 @@
android:id="@+id/soListRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="200dp"
android:nestedScrollingEnabled="false"
android:layout_marginBottom="16dp" />
<TextView
@@ -77,13 +81,84 @@
android:textColor="?android:attr/textColorTertiary"
android:visibility="gone" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gadget 配置"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<RadioGroup
android:id="@+id/gadgetConfigGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="8dp">
<RadioButton
android:id="@+id/radioNoGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="不使用Gadget"
android:checked="true" />
<RadioButton
android:id="@+id/radioUseGlobalGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="使用全局Gadget配置" />
<TextView
android:id="@+id/tvGlobalGadgetInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginBottom="4dp"
android:text="未配置全局Gadget"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<RadioButton
android:id="@+id/radioUseCustomGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="自定义Gadget配置" />
</RadioGroup>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfigureGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="配置Gadget"
android:textSize="14sp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:enabled="false"
android:visibility="gone"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Gadget可用于Frida调试可配置监听地址和端口"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="注入方式"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp" />
<RadioGroup
@@ -138,5 +213,7 @@
android:layout_marginStart="32dp" />
</RadioGroup>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,299 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Gadget 配置"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="提示:请确保对应的 gadget SO 文件已添加到 SO 库中"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="16dp" />
<!-- 模式选择 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gadget 模式"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<RadioGroup
android:id="@+id/modeRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<RadioButton
android:id="@+id/radioModeServer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Server 模式"
android:checked="true" />
<RadioButton
android:id="@+id/radioModeScript"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Script 模式" />
</RadioGroup>
<!-- Server 模式配置区域 -->
<LinearLayout
android:id="@+id/serverModeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 监听地址配置 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="监听地址"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<RadioGroup
android:id="@+id/addressRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="8dp">
<RadioButton
android:id="@+id/radioAddressAll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0.0.0.0 (监听所有接口)"
android:checked="true" />
<RadioButton
android:id="@+id/radioAddressLocal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="127.0.0.1 (仅本地)" />
<RadioButton
android:id="@+id/radioAddressCustom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="自定义" />
</RadioGroup>
<EditText
android:id="@+id/editCustomAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入自定义地址"
android:inputType="text"
android:enabled="false"
android:layout_marginBottom="16dp" />
<!-- 端口配置 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="监听端口"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<EditText
android:id="@+id/editPort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="27042"
android:hint="1-65535"
android:layout_marginBottom="16dp" />
<!-- 端口冲突处理 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="端口冲突处理"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<RadioGroup
android:id="@+id/portConflictRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<RadioButton
android:id="@+id/radioConflictFail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="fail (启动失败)"
android:checked="true" />
<RadioButton
android:id="@+id/radioConflictPickNext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="pick-next (尝试下一个端口)" />
</RadioGroup>
<!-- 加载处理方式 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="加载时处理方式"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<RadioGroup
android:id="@+id/onLoadRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<RadioButton
android:id="@+id/radioLoadWait"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="wait (等待连接)"
android:checked="true" />
<RadioButton
android:id="@+id/radioLoadResume"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="resume (立即继续)" />
</RadioGroup>
</LinearLayout>
<!-- Script 模式配置区域 -->
<LinearLayout
android:id="@+id/scriptModeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Script 文件路径"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<EditText
android:id="@+id/editScriptPath"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="/data/local/tmp/script.js"
android:inputType="text"
android:layout_marginEnd="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSelectScript"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="选择"
android:textSize="12sp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="提示Script 模式下Gadget 会在程序入口点执行前自动加载并运行指定的脚本文件"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="16dp" />
</LinearLayout>
<!-- Gadget 名称 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gadget 文件名"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<EditText
android:id="@+id/editGadgetName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="libgadget.so"
android:hint="例如: libgadget.so"
android:layout_marginBottom="16dp" />
<!-- JSON 预览区域 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="配置预览 (可直接编辑)"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="2dp"
app:cardCornerRadius="4dp">
<EditText
android:id="@+id/editJsonPreview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="150dp"
android:gravity="top"
android:padding="12dp"
android:background="?android:attr/selectableItemBackground"
android:fontFamily="monospace"
android:textSize="12sp"
android:inputType="textMultiLine"
android:scrollbars="vertical"
android:overScrollMode="always" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>

View 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>

View File

@@ -72,6 +72,124 @@
</com.google.android.material.card.MaterialCardView>
<!-- 注入延迟时间设置 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="注入延迟时间"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="设置SO文件注入前的等待时间以确保应用完全初始化"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="延迟时间(秒):"
android:textSize="14sp"
android:layout_marginEnd="8dp" />
<EditText
android:id="@+id/editInjectionDelay"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:inputType="number"
android:text="2"
android:textAlignment="center"
android:hint="0-60"
android:layout_marginEnd="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="秒"
android:textSize="14sp" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="建议值2-5秒。某些应用可能需要更长时间。"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:layout_marginTop="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 全局Gadget配置 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="全局Gadget配置"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="配置全局默认的Gadget设置应用可以选择使用或覆盖"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/tvGlobalGadgetStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未配置"
android:textSize="14sp"
android:layout_marginBottom="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfigureGlobalGadget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="配置全局Gadget"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 其他设置可以在这里添加 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"

View 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>

View File

@@ -9,6 +9,11 @@
android:id="@+id/navigation_so_manager"
android:icon="@android:drawable/ic_menu_save"
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
android:id="@+id/navigation_settings"

View File

@@ -4,6 +4,7 @@
<!-- 底部导航 -->
<string name="title_apps">应用列表</string>
<string name="title_so_manager">SO库管理</string>
<string name="title_kpm_hide">KPM隐藏</string>
<string name="title_settings">全局设置</string>
<!-- 应用列表 -->
@@ -21,4 +22,25 @@
<!-- 关于 -->
<string name="about">关于</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>

View File

@@ -70,6 +70,10 @@ afterEvaluate {
from("$projectDir") {
include 'service.sh'
}
// Copy KPM module
from("$projectDir/kpm") {
include 'injectHide.kpm'
}
// Copy ConfigApp APK if it exists
def apkFile = file("$rootDir/configapp/build/outputs/apk/debug/configapp-debug.apk")
if (apkFile.exists()) {

BIN
module/kpm/injectHide.kpm Normal file

Binary file not shown.

View File

@@ -94,5 +94,46 @@ chown -R root:root /data/adb/modules/zygisk-myinjector
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

View File

@@ -31,6 +31,13 @@ namespace Config {
} else if (json[valueStart] == 't' || json[valueStart] == 'f') {
// Boolean value
return (json.substr(valueStart, 4) == "true") ? "true" : "false";
} else {
// Number value
size_t valueEnd = json.find_first_of(",} \t\n", valueStart);
if (valueEnd == std::string::npos) {
return json.substr(valueStart);
}
return json.substr(valueStart, valueEnd - valueStart);
}
return "";
@@ -85,11 +92,44 @@ namespace Config {
}
}
// Parse gadgetConfig if exists
size_t gadgetPos = appJson.find("\"gadgetConfig\"");
if (gadgetPos != std::string::npos) {
size_t gadgetObjStart = appJson.find("{", gadgetPos);
size_t gadgetObjEnd = appJson.find("}", gadgetObjStart);
if (gadgetObjStart != std::string::npos && gadgetObjEnd != std::string::npos) {
std::string gadgetObj = appJson.substr(gadgetObjStart, gadgetObjEnd - gadgetObjStart + 1);
GadgetConfig* gadgetConfig = new GadgetConfig();
std::string address = extractValue(gadgetObj, "address");
if (!address.empty()) gadgetConfig->address = address;
std::string portStr = extractValue(gadgetObj, "port");
if (!portStr.empty()) gadgetConfig->port = std::stoi(portStr);
std::string onPortConflict = extractValue(gadgetObj, "onPortConflict");
if (!onPortConflict.empty()) gadgetConfig->onPortConflict = onPortConflict;
std::string onLoad = extractValue(gadgetObj, "onLoad");
if (!onLoad.empty()) gadgetConfig->onLoad = onLoad;
std::string gadgetName = extractValue(gadgetObj, "gadgetName");
if (!gadgetName.empty()) gadgetConfig->gadgetName = gadgetName;
appConfig.gadgetConfig = gadgetConfig;
LOGD("Loaded gadget config: %s:%d, name: %s",
gadgetConfig->address.c_str(), gadgetConfig->port, gadgetConfig->gadgetName.c_str());
}
}
g_config.perAppConfig[packageName] = appConfig;
const char* methodName = appConfig.injectionMethod == InjectionMethod::CUSTOM_LINKER ? "custom_linker" :
appConfig.injectionMethod == InjectionMethod::RIRU ? "riru" : "standard";
LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu",
packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size());
LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu, gadget: %s",
packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size(),
appConfig.gadgetConfig ? "yes" : "no");
}
ModuleConfig readConfig() {
@@ -118,7 +158,13 @@ namespace Config {
std::string hideStr = extractValue(json, "hideInjection");
g_config.hideInjection = (hideStr == "true");
LOGD("Module enabled: %d, hide injection: %d", g_config.enabled, g_config.hideInjection);
std::string delayStr = extractValue(json, "injectionDelay");
if (!delayStr.empty()) {
g_config.injectionDelay = std::stoi(delayStr);
}
LOGD("Module enabled: %d, hide injection: %d, injection delay: %d",
g_config.enabled, g_config.hideInjection, g_config.injectionDelay);
// Parse perAppConfig
size_t perAppPos = json.find("\"perAppConfig\"");
@@ -212,4 +258,11 @@ namespace Config {
}
return InjectionMethod::STANDARD;
}
int getInjectionDelay() {
if (!g_configLoaded) {
readConfig();
}
return g_config.injectionDelay;
}
}

View File

@@ -19,15 +19,25 @@ namespace Config {
CUSTOM_LINKER = 2
};
struct GadgetConfig {
std::string address = "0.0.0.0";
int port = 27042;
std::string onPortConflict = "fail";
std::string onLoad = "wait";
std::string gadgetName = "libgadget.so";
};
struct AppConfig {
bool enabled = false;
InjectionMethod injectionMethod = InjectionMethod::STANDARD;
std::vector<SoFile> soFiles;
GadgetConfig* gadgetConfig = nullptr;
};
struct ModuleConfig {
bool enabled = true;
bool hideInjection = false;
int injectionDelay = 2; // Default 2 seconds
std::unordered_map<std::string, AppConfig> perAppConfig;
};
@@ -45,6 +55,9 @@ namespace Config {
// Get injection method for specific app
InjectionMethod getAppInjectionMethod(const std::string& packageName);
// Get injection delay in seconds
int getInjectionDelay();
}
#endif // CONFIG_H

View File

@@ -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

View File

@@ -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

View File

@@ -88,8 +88,8 @@ void load_so_file_custom_linker(const char *game_data_dir, const Config::SoFile
void hack_thread_func(const char *game_data_dir, const char *package_name, JavaVM *vm) {
LOGI("Hack thread started for package: %s", package_name);
// Wait a bit for app to initialize and files to be copied
sleep(2);
// Note: Delay is now handled in main thread before this thread is created
LOGI("Starting injection immediately (delay already applied in main thread)");
// Get injection method for this app
Config::InjectionMethod method = Config::getAppInjectionMethod(package_name);
@@ -103,6 +103,12 @@ void hack_thread_func(const char *game_data_dir, const char *package_name, JavaV
// Load each SO file using the configured method
for (const auto &soFile : soFiles) {
// Skip config files
if (soFile.name.find(".config.so") != std::string::npos) {
LOGI("Skipping config file: %s", soFile.name.c_str());
continue;
}
LOGI("Loading SO: %s (stored as: %s)", soFile.name.c_str(), soFile.storedPath.c_str());
if (method == Config::InjectionMethod::CUSTOM_LINKER) {
@@ -125,5 +131,5 @@ void hack_prepare(const char *game_data_dir, const char *package_name, void *dat
LOGI("hack_prepare called for package: %s, dir: %s", package_name, game_data_dir);
std::thread hack_thread(hack_thread_func, game_data_dir, package_name, vm);
hack_thread.detach();
hack_thread.join();
}

View File

@@ -11,7 +11,6 @@
#include <time.h>
#include "hack.h"
#include "zygisk.hpp"
#include "game.h"
#include "log.h"
#include "dlfcn.h"
#include "config.h"
@@ -30,9 +29,6 @@ public:
void preAppSpecialize(AppSpecializeArgs *args) override {
auto package_name = env->GetStringUTFChars(args->nice_name, 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);
preSpecialize(package_name, app_data_dir);
@@ -45,6 +41,13 @@ public:
// Get JavaVM
JavaVM *vm = nullptr;
if (env->GetJavaVM(&vm) == JNI_OK) {
// Get injection delay from config
int delay = Config::getInjectionDelay();
LOGI("Main thread blocking for %d seconds before injection", delay);
// Block main thread for the delay period
sleep(delay);
// Then start hack thread with JavaVM
std::thread hack_thread(hack_prepare, _data_dir, _package_name, data, length, vm);
hack_thread.detach();

View File

@@ -17,8 +17,8 @@ set(SOURCES
find_library(log-lib log)
# Build as shared library
add_library(mylinker SHARED ${SOURCES})
# Build as static library to be linked into main module
add_library(mylinker STATIC ${SOURCES})
target_link_libraries(mylinker ${log-lib})