29 Commits
v1.0.1 ... main

Author SHA1 Message Date
Ji qiu
8207e728dc Merge pull request #30 from vwww-droid/feature/enhance-cli-configure
feat: 基于广播, 实现脚本指定 app 注入和测试
2025-10-25 17:33:08 +08:00
vwvw
dd67f20b8d feat: 自动化脚本增强
1. 多端口适配;
2. 允许命令行指定端口;
3. 临时配置使用系统临时目录, 使用后删除;
2025-10-20 17:42:12 +08:00
vwvw
65e91d188f feat: 基于广播, 实现脚本指定 app 注入和测试
1. 广播实现命令行交互式指定 app 打开端口并自动配置;
2. 广播动态注册, 无法枚举广播接收器, 过滤 callingUid 只接收 shell 和 root 发出的广播;
3. 自动化脚本增加 check_and_set_selinux 方法设置 selinux 为宽容模式解决部分无法启动问题;
4. frida 官方 gadget.so 自动下载和缓存 scripts/.cache/;
5. 自动重启 apk & frida 命令测试;
2025-10-15 16:34:44 +08:00
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
Ji qiu
92a640a3c7 Merge pull request #14 from jiqiu2022/linker_load
feat:使用原来的so名称注入
2025-06-26 20:15:32 +08:00
jiqiu2021
680a93cba8 feat:使用原来的so名称注入 2025-06-26 20:14:54 +08:00
Ji qiu
da73a3f9bd Merge pull request #13 from jiqiu2022/linker_load
feat:增加自定义linker注入
2025-06-26 20:13:28 +08:00
jiqiu2021
d793712a13 feat:增加自定义linker注入 2025-06-26 19:59:33 +08:00
58 changed files with 5481 additions and 149 deletions

7
.gitignore vendored
View File

@@ -12,3 +12,10 @@
/out
.externalNativeBuild
.cxx
# Frida Gadget temporary downloads
tmp_gadget/
*.so
# Frida Gadget local cache
scripts/.cache/

236
README.md
View File

@@ -1,65 +1,213 @@
# [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%无痕注入
1. 安装[Magisk](https://github.com/topjohnwu/Magisk) v24以上版本并开启Zygisk
更新记录:
- **v1.2**: 增加gadget配置的自动生成支持脚本和server模式解决了若干bug增加了全局注入延迟设置
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`文件夹下
## 致谢
**项目地址**[https://github.com/jiqiu2022/Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
3. 在Magisk里安装模块
特别感谢以下项目和开发者(按时间顺序):
4. 将要注入的so放入到/data/local/tmp下修改为test.so
- [Zygisk-Il2CppDumper](https://github.com/Perfare/Zygisk-Il2CppDumper) - 提供最原始的注入思路
- Riru - 提供隐藏思路
- 小佳大佬 - 提供App界面化的思路
- [soLoader](https://github.com/SoyBeanMilkx/soLoader) - 提供自定义linker注入的全部逻辑期待认识作者和作者一起继续完善
(部分手机第一次注入不会成功,请重启,再之后的注入会成功)
如果对自定义linker感兴趣的大佬推荐soLoader作者的文章https://yuuki.cool/2025/06/15/CustomLinker/
多模块注入已经开发完成:
## 使用教程
```
void hack_start(const char *game_data_dir,JavaVM *vm) {
load_so(game_data_dir,vm,"test");
//如果要注入多个so那么就在这里不断的添加load_so函数即可
}
### 步骤一:下载并安装面具模块
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)
### 自动化注入
[脚本](./scripts/auto_config.py) 实现了针对 libgadget.so 的自动注入
```bash
# 1. 运行自动配置脚本(会自动检查并设置 SELinux
cd scripts
./auto_config.py
# 2. 按提示选择设备、应用和配置(全部使用默认值即可)
# 脚本会自动完成:
# - 生成配置文件
# - 推送到设备
# - 应用配置
# - 重启应用
# - 端口转发
# - 快速测试
# 3. 如果测试成功,直接使用 Frida 连接
frida -H 127.0.0.1:27042 Gadget -l your_script.js
```
见 [视频](assets/auto_config.record.mp4)
## 编译指南
### 自动编译
参考项目中的 [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
```
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的分支(已完成)
计划开发:
1. 第一步仿照Riru将注入的so进行内存上的初步隐藏可以对抗部分业务检测游戏安全相关已经补齐建议不要尝试已经开发完成
2. 第二步实现一个自定义的linker进行更深层次的注入隐藏
3. 第三步,搭配对应配套手机的内核模块对注入的模块进行进一步完美擦除,达到完美注入的目的
以此项目为脚手架的计划开发:
1. 一个全新的Frida框架保留大部分原生api并可以过任何相关注入检测
2. 一个全新的Trace框架高性能Trace速度是Stallker的60倍并且支持更全面的信息打印。具体效果可以参考看雪帖子
3. 一个全新的无痕调试框架支持像GDB一样调试没有ptrace痕迹两种思路进行无痕调试基于硬件断点以及基于VM
- **深度隐藏**联动内核模块提供maps等更深层次的隐藏功能
- **用户体验优化**
- 增加gadget配置一键生成功能
- 提供更友好的界面交互
- 支持批量配置管理
---
欢迎关注项目进展,期待您的贡献!

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

Binary file not shown.

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

@@ -29,8 +29,8 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled false
}
}

View File

@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:name=".ConfigApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"

View File

@@ -13,6 +13,8 @@ import android.widget.ProgressBar;
import android.app.Dialog;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -134,15 +136,98 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
TextView packageName = dialogView.findViewById(R.id.packageName);
RecyclerView soListRecyclerView = dialogView.findViewById(R.id.soListRecyclerView);
TextView emptyText = dialogView.findViewById(R.id.emptyText);
SwitchMaterial switchHideInjection = dialogView.findViewById(R.id.switchHideInjection);
RadioGroup injectionMethodGroup = dialogView.findViewById(R.id.injectionMethodGroup);
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());
packageName.setText(appInfo.getPackageName());
// Load current config
boolean hideInjection = configManager.getHideInjection();
switchHideInjection.setChecked(hideInjection);
String injectionMethod = configManager.getAppInjectionMethod(appInfo.getPackageName());
if ("custom_linker".equals(injectionMethod)) {
radioCustomLinkerInjection.setChecked(true);
} else if ("riru".equals(injectionMethod)) {
radioRiruInjection.setChecked(true);
} else {
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();
@@ -165,8 +250,16 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
.setTitle("配置注入")
.setView(dialogView)
.setPositiveButton("保存", (dialog, which) -> {
// Save hide injection setting
configManager.setHideInjection(switchHideInjection.isChecked());
// Save injection method
String selectedMethod;
if (radioCustomLinkerInjection.isChecked()) {
selectedMethod = "custom_linker";
} else if (radioRiruInjection.isChecked()) {
selectedMethod = "riru";
} else {
selectedMethod = "standard";
}
configManager.setAppInjectionMethod(appInfo.getPackageName(), selectedMethod);
// Save SO selection
if (soListRecyclerView.getAdapter() != null) {

View File

@@ -0,0 +1,54 @@
package com.jiqiu.configapp;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.content.IntentFilter;
import android.os.Build;
import android.util.Log;
/**
* Application class for dynamic receiver registration
* 动态注册 BroadcastReceiver避免被第三方 app 发现
*/
public class ConfigApplication extends Application {
private static final String TAG = "ConfigApplication";
private static final String ACTION_APPLY_CONFIG = "com.jiqiu.configapp.APPLY_CONFIG";
private ConfigApplyReceiver configReceiver;
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Application onCreate - registering receiver dynamically");
// 动态注册 ConfigApplyReceiver
configReceiver = new ConfigApplyReceiver();
IntentFilter filter = new IntentFilter(ACTION_APPLY_CONFIG);
// 使用 RECEIVER_NOT_EXPORTED 标志,明确表示不导出
if (Build.VERSION.SDK_INT >= 33) {
registerReceiver(configReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(configReceiver, filter);
}
Log.d(TAG, "Receiver registered dynamically (UID check: shell/root only)");
Log.i(TAG, "ConfigApplyReceiver registered dynamically - invisible to third-party apps");
}
@Override
public void onTerminate() {
super.onTerminate();
// 注销 receiver注意onTerminate 在真实设备上通常不会被调用,仅在模拟器中)
if (configReceiver != null) {
try {
unregisterReceiver(configReceiver);
Log.d(TAG, "Receiver unregistered");
} catch (IllegalArgumentException e) {
Log.w(TAG, "Receiver was not registered or already unregistered");
}
}
}
}

View File

@@ -0,0 +1,151 @@
package com.jiqiu.configapp;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Process;
import android.util.Log;
import com.topjohnwu.superuser.Shell;
/**
* BroadcastReceiver to apply configurations pushed from ADB
* 接收来自 ADB shell 的广播以应用配置
*
* 安全机制:动态注册 + UID 权限检查,只允许 shell(2000) 或 root(0) 调用
*/
public class ConfigApplyReceiver extends BroadcastReceiver {
private static final String TAG = "ConfigApplyReceiver";
// UID constants
private static final int SHELL_UID = 2000; // ADB shell user
private static final int ROOT_UID = 0; // Root user
@Override
public void onReceive(Context context, Intent intent) {
// 权限检查:只允许 shell 或 root 用户发送广播
int callingUid = Binder.getCallingUid();
if (callingUid != SHELL_UID && callingUid != ROOT_UID) {
Log.w(TAG, "Unauthorized broadcast attempt from UID: " + callingUid);
Log.w(TAG, "Only shell (2000) or root (0) can send this broadcast");
return;
}
Log.i(TAG, "Received config apply broadcast from authorized UID: " + callingUid);
String action = intent.getAction();
if (!"com.jiqiu.configapp.APPLY_CONFIG".equals(action)) {
Log.w(TAG, "Unknown action: " + action);
return;
}
// 获取广播参数
String packageName = intent.getStringExtra("package_name");
String tmpConfigPath = intent.getStringExtra("tmp_config_path");
String tmpGadgetConfigPath = intent.getStringExtra("tmp_gadget_config_path");
boolean deployOnly = intent.getBooleanExtra("deploy_only", false);
Log.i(TAG, "Processing config for package: " + packageName);
Log.i(TAG, "Config path: " + tmpConfigPath);
Log.i(TAG, "Gadget config path: " + tmpGadgetConfigPath);
Log.i(TAG, "Deploy only: " + deployOnly);
if (packageName == null || packageName.isEmpty()) {
Log.e(TAG, "Package name is required");
return;
}
// 在后台线程处理,避免阻塞主线程
new Thread(() -> {
try {
ConfigManager configManager = new ConfigManager(context);
// 确保目录存在
configManager.ensureModuleDirectories();
// 如果提供了配置文件路径,复制到模块目录
if (tmpConfigPath != null && !tmpConfigPath.isEmpty()) {
Shell.Result checkResult = Shell.cmd("test -f \"" + tmpConfigPath + "\" && echo 'exists'").exec();
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
Log.i(TAG, "Copying main config: " + tmpConfigPath + " -> " + ConfigManager.CONFIG_FILE);
Shell.Result copyResult = Shell.cmd(
"cp \"" + tmpConfigPath + "\" \"" + ConfigManager.CONFIG_FILE + "\"",
"chmod 644 \"" + ConfigManager.CONFIG_FILE + "\""
).exec();
if (copyResult.isSuccess()) {
Log.i(TAG, "Main config copied successfully");
// 重新加载配置
configManager.reloadConfig();
} else {
Log.e(TAG, "Failed to copy main config: " + String.join("\n", copyResult.getErr()));
}
} else {
Log.w(TAG, "Main config file not found at: " + tmpConfigPath);
}
}
// 如果提供了 Gadget 配置文件,复制到应用数据目录
if (tmpGadgetConfigPath != null && !tmpGadgetConfigPath.isEmpty()) {
Shell.Result checkResult = Shell.cmd("test -f \"" + tmpGadgetConfigPath + "\" && echo 'exists'").exec();
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
String filesDir = "/data/data/" + packageName + "/files";
// 从路径中提取文件名
String gadgetConfigFileName = tmpGadgetConfigPath.substring(tmpGadgetConfigPath.lastIndexOf('/') + 1);
String targetPath = filesDir + "/" + gadgetConfigFileName;
Log.i(TAG, "Copying gadget config: " + tmpGadgetConfigPath + " -> " + targetPath);
// 创建目录
Shell.cmd("mkdir -p \"" + filesDir + "\"").exec();
Shell.Result copyResult = Shell.cmd(
"cp \"" + tmpGadgetConfigPath + "\" \"" + targetPath + "\"",
"chmod 644 \"" + targetPath + "\""
).exec();
if (copyResult.isSuccess()) {
Log.i(TAG, "Gadget config copied successfully");
// 设置正确的所有权
Shell.Result uidResult = Shell.cmd("stat -c %u /data/data/" + packageName).exec();
if (uidResult.isSuccess() && !uidResult.getOut().isEmpty()) {
String uid = uidResult.getOut().get(0).trim();
Shell.cmd("chown " + uid + ":" + uid + " \"" + targetPath + "\"").exec();
Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + targetPath + "\"").exec();
}
} else {
Log.e(TAG, "Failed to copy gadget config: " + String.join("\n", copyResult.getErr()));
}
} else {
Log.w(TAG, "Gadget config file not found at: " + tmpGadgetConfigPath);
}
}
// 如果不是仅部署模式,或者没有提供配置文件,执行部署
if (!deployOnly || (tmpConfigPath == null && tmpGadgetConfigPath == null)) {
Log.i(TAG, "Deploying SO files for package: " + packageName);
configManager.deployForPackage(packageName);
Log.i(TAG, "Deployment completed for: " + packageName);
} else {
Log.i(TAG, "Config updated, skipping deployment (deploy_only=true)");
}
// 清理临时文件
if (tmpConfigPath != null && !tmpConfigPath.isEmpty()) {
Shell.cmd("rm -f \"" + tmpConfigPath + "\"").exec();
}
if (tmpGadgetConfigPath != null && !tmpGadgetConfigPath.isEmpty()) {
Shell.cmd("rm -f \"" + tmpGadgetConfigPath + "\"").exec();
}
Log.i(TAG, "Config application completed successfully");
} catch (Exception e) {
Log.e(TAG, "Error applying config", e);
}
}).start();
}
}

View File

@@ -89,6 +89,15 @@ public class ConfigManager {
}
}
/**
* Public method to reload configuration from file
* 从文件重新加载配置(用于外部更新配置后)
*/
public void reloadConfig() {
loadConfig();
Log.i(TAG, "Configuration reloaded");
}
public void saveConfig() {
String json = gson.toJson(config);
// Write to temp file first
@@ -151,24 +160,56 @@ public class ConfigManager {
config.globalSoFiles = new ArrayList<>();
}
// Generate unique filename
// Keep original filename
String fileName = new File(originalPath).getName();
String storedPath = SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
String storedPath = SO_STORAGE_DIR + "/" + fileName;
// Check if file already exists with same name
for (SoFile existing : config.globalSoFiles) {
if (existing.name.equals(fileName)) {
Log.w(TAG, "SO file with same name already exists: " + fileName);
return;
}
}
// 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()));
}
}
@@ -227,6 +268,194 @@ public class ConfigManager {
saveConfig();
}
public String getAppInjectionMethod(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
return "standard"; // Default to standard
}
return appConfig.injectionMethod != null ? appConfig.injectionMethod : "standard";
}
public void setAppInjectionMethod(String packageName, String method) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
appConfig = new AppConfig();
config.perAppConfig.put(packageName, appConfig);
}
appConfig.injectionMethod = method;
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);
@@ -244,78 +473,118 @@ 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
for (SoFile soFile : appConfig.soFiles) {
// Extract mapped filename
String mappedName = new File(soFile.storedPath).getName();
String destPath = filesDir + "/" + mappedName;
// Use original filename
String destPath = filesDir + "/" + soFile.name;
// Check if source file exists
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
@@ -336,8 +605,8 @@ public class ConfigManager {
// Only delete the SO files we deployed, not the entire directory
for (SoFile soFile : appConfig.soFiles) {
String mappedName = new File(soFile.storedPath).getName();
String filePath = filesDir + "/" + mappedName;
// Use original filename
String filePath = filesDir + "/" + soFile.name;
Log.i(TAG, "Cleaning up: " + filePath);
@@ -361,6 +630,24 @@ public class ConfigManager {
}
}
// Clean up gadget config file if exists
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
if (gadgetToUse != null) {
String gadgetConfigName = gadgetToUse.gadgetName.replace(".so", ".config.so");
String configPath = filesDir + "/" + gadgetConfigName;
Shell.Result checkConfigResult = Shell.cmd("test -f \"" + configPath + "\" && echo 'exists'").exec();
if (checkConfigResult.isSuccess() && !checkConfigResult.getOut().isEmpty()) {
Shell.Result deleteResult = Shell.cmd("rm -f \"" + configPath + "\"").exec();
if (deleteResult.isSuccess()) {
Log.i(TAG, "Deleted gadget config: " + configPath);
} else {
// Try with su -c
Shell.cmd("su -c 'rm -f \"" + configPath + "\"'").exec();
}
}
}
Log.i(TAG, "Cleanup complete for: " + packageName);
}
@@ -373,17 +660,35 @@ public class ConfigManager {
}
}
/**
* Public method to deploy SO files for a specific package
* 为指定包名部署 SO 文件(外部调用)
* @param packageName Target package name
*/
public void deployForPackage(String packageName) {
if (packageName == null || packageName.isEmpty()) {
Log.e(TAG, "Package name cannot be null or empty");
return;
}
deploySoFilesToApp(packageName);
}
// Data classes
public static class ModuleConfig {
public boolean enabled = true;
public boolean hideInjection = false;
public int injectionDelay = 2; // Default 2 seconds
public List<SoFile> globalSoFiles = new ArrayList<>();
public Map<String, AppConfig> perAppConfig = new HashMap<>();
public GadgetConfig globalGadgetConfig = null; // Global gadget configuration
}
public static class AppConfig {
public boolean enabled = false;
public List<SoFile> soFiles = new ArrayList<>();
public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker"
public GadgetConfig gadgetConfig = null;
public boolean useGlobalGadget = true; // Whether to use global gadget settings
}
public static class SoFile {
@@ -399,4 +704,17 @@ public class ConfigManager {
return false;
}
}
public static class GadgetConfig {
public String mode = "server"; // "server" or "script"
// Server mode config
public String address = "0.0.0.0";
public int port = 27042;
public String onPortConflict = "fail";
public String onLoad = "wait";
// Script mode config
public String scriptPath = "/data/local/tmp/script.js";
// Common config
public String gadgetName = "libgadget.so";
}
}

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

@@ -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,20 +81,139 @@
android:textColor="?android:attr/textColorTertiary"
android:visibility="gone" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchHideInjection"
<View
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="隐藏注入"
android:layout_marginTop="8dp" />
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="使用Riru Hide隐藏注入的SO文件"
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_marginStart="56dp"
android:layout_marginTop="4dp" />
android:layout_marginBottom="16dp" />
</LinearLayout>
<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/injectionMethodGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/radioStandardInjection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="标准注入"
android:checked="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginBottom="8dp"
android:text="使用标准dlopen方式注入"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<RadioButton
android:id="@+id/radioRiruInjection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Riru注入" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="使用Riru Hide隐藏注入的SO文件"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginStart="32dp"
android:layout_marginBottom="8dp" />
<RadioButton
android:id="@+id/radioCustomLinkerInjection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="自定义Linker注入" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="使用自定义ELF加载器进行注入"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginStart="32dp" />
</RadioGroup>
</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

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

@@ -29,10 +29,14 @@ set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${LINKER_FLAGS}")
include_directories(
xdl/include
mylinker/include
)
aux_source_directory(xdl xdl-src)
# Build mylinker as a subdirectory
add_subdirectory(mylinker)
add_library(${MODULE_NAME} SHARED
main.cpp
hack_new.cpp
@@ -40,7 +44,7 @@ add_library(${MODULE_NAME} SHARED
newriruhide.cpp
pmparser.cpp
${xdl-src})
target_link_libraries(${MODULE_NAME} log)
target_link_libraries(${MODULE_NAME} log mylinker)
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
add_custom_command(TARGET ${MODULE_NAME} POST_BUILD

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 "";
@@ -43,6 +50,16 @@ namespace Config {
std::string enabledStr = extractValue(appJson, "enabled");
appConfig.enabled = (enabledStr == "true");
// Parse injection method
std::string methodStr = extractValue(appJson, "injectionMethod");
if (methodStr == "2" || methodStr == "custom_linker") {
appConfig.injectionMethod = InjectionMethod::CUSTOM_LINKER;
} else if (methodStr == "1" || methodStr == "riru") {
appConfig.injectionMethod = InjectionMethod::RIRU;
} else {
appConfig.injectionMethod = InjectionMethod::STANDARD;
}
// Parse soFiles array
size_t soFilesPos = appJson.find("\"soFiles\"");
if (soFilesPos != std::string::npos) {
@@ -75,9 +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;
LOGD("Loaded config for app: %s, enabled: %d, SO files: %zu",
packageName.c_str(), appConfig.enabled, appConfig.soFiles.size());
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, gadget: %s",
packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size(),
appConfig.gadgetConfig ? "yes" : "no");
}
ModuleConfig readConfig() {
@@ -106,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\"");
@@ -188,4 +246,23 @@ namespace Config {
}
return g_config.hideInjection;
}
InjectionMethod getAppInjectionMethod(const std::string& packageName) {
if (!g_configLoaded) {
readConfig();
}
auto it = g_config.perAppConfig.find(packageName);
if (it != g_config.perAppConfig.end()) {
return it->second.injectionMethod;
}
return InjectionMethod::STANDARD;
}
int getInjectionDelay() {
if (!g_configLoaded) {
readConfig();
}
return g_config.injectionDelay;
}
}

View File

@@ -13,14 +13,31 @@ namespace Config {
std::string originalPath;
};
enum class InjectionMethod {
STANDARD = 0,
RIRU = 1,
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;
};
@@ -35,6 +52,12 @@ namespace Config {
// Get hide injection setting
bool shouldHideInjection();
// Get injection method for specific app
InjectionMethod getAppInjectionMethod(const std::string& packageName);
// Get injection delay in seconds
int getInjectionDelay();
}
#endif // CONFIG_H

View File

@@ -6,7 +6,8 @@
#define ZYGISK_IL2CPPDUMPER_HACK_H
#include <stddef.h>
#include <jni.h>
void hack_prepare(const char *game_data_dir, const char *package_name, void *data, size_t length);
void hack_prepare(const char *game_data_dir, const char *package_name, void *data, size_t length, JavaVM *vm);
#endif //ZYGISK_IL2CPPDUMPER_HACK_H

View File

@@ -1,6 +1,7 @@
#include "hack.h"
#include "config.h"
#include "log.h"
#include "mylinker.h"
#include <cstring>
#include <thread>
#include <dlfcn.h>
@@ -8,22 +9,15 @@
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#include <jni.h>
// External function from newriruhide.cpp
extern "C" void riru_hide(const char *name);
void load_so_file(const char *game_data_dir, const Config::SoFile &soFile) {
// Extract the mapped filename from storedPath (e.g., "1750851324251_libmylib.so")
const char *mapped_name = strrchr(soFile.storedPath.c_str(), '/');
if (!mapped_name) {
mapped_name = soFile.storedPath.c_str();
} else {
mapped_name++; // Skip the '/'
}
// The file should already be in app's files directory
void load_so_file_standard(const char *game_data_dir, const Config::SoFile &soFile) {
// Use original filename
char so_path[512];
snprintf(so_path, sizeof(so_path), "%s/files/%s", game_data_dir, mapped_name);
snprintf(so_path, sizeof(so_path), "%s/files/%s", game_data_dir, soFile.name.c_str());
// Check if file exists
if (access(so_path, F_OK) != 0) {
@@ -31,42 +25,111 @@ void load_so_file(const char *game_data_dir, const Config::SoFile &soFile) {
return;
}
// Load the SO file
// Load the SO file using standard dlopen (no hiding)
void *handle = dlopen(so_path, RTLD_NOW | RTLD_LOCAL);
if (handle) {
LOGI("Successfully loaded SO: %s (mapped: %s)", soFile.name.c_str(), mapped_name);
// Hide if configured
if (Config::shouldHideInjection()) {
// Hide using the mapped name since that's what we loaded
riru_hide(mapped_name);
LOGI("Applied riru_hide to: %s", mapped_name);
}
LOGI("Successfully loaded SO via standard dlopen: %s", soFile.name.c_str());
} else {
LOGE("Failed to load SO: %s - %s", so_path, dlerror());
LOGE("Failed to load SO via standard dlopen: %s - %s", so_path, dlerror());
}
}
void hack_thread_func(const char *game_data_dir, const char *package_name) {
void load_so_file_riru(const char *game_data_dir, const Config::SoFile &soFile) {
// Use original filename
char so_path[512];
snprintf(so_path, sizeof(so_path), "%s/files/%s", game_data_dir, soFile.name.c_str());
// Check if file exists
if (access(so_path, F_OK) != 0) {
LOGE("SO file not found: %s", so_path);
return;
}
// Load the SO file using dlopen (Riru method)
void *handle = dlopen(so_path, RTLD_NOW | RTLD_LOCAL);
if (handle) {
LOGI("Successfully loaded SO via Riru: %s", soFile.name.c_str());
// Hide if configured
if (Config::shouldHideInjection()) {
// Hide using the original name
riru_hide(soFile.name.c_str());
LOGI("Applied riru_hide to: %s", soFile.name.c_str());
}
} else {
LOGE("Failed to load SO via Riru: %s - %s", so_path, dlerror());
}
}
void load_so_file_custom_linker(const char *game_data_dir, const Config::SoFile &soFile, JavaVM *vm) {
// Use original filename
char so_path[512];
snprintf(so_path, sizeof(so_path), "%s/files/%s", game_data_dir, soFile.name.c_str());
// Check if file exists
if (access(so_path, F_OK) != 0) {
LOGE("SO file not found: %s", so_path);
return;
}
// Load the SO file using custom linker
if (mylinker_load_library(so_path, vm)) {
LOGI("Successfully loaded SO via custom linker: %s", soFile.name.c_str());
// Custom linker doesn't appear in maps, so no need to hide
if (Config::shouldHideInjection()) {
LOGI("Custom linker injection is inherently hidden");
}
} else {
LOGE("Failed to load SO via custom linker: %s", so_path);
}
}
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);
const char* methodName = method == Config::InjectionMethod::CUSTOM_LINKER ? "Custom Linker" :
method == Config::InjectionMethod::RIRU ? "Riru" : "Standard";
LOGI("Using injection method: %s", methodName);
// Get SO files for this app
auto soFiles = Config::getAppSoFiles(package_name);
LOGI("Found %zu SO files to load", soFiles.size());
// Load each SO file
// 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());
load_so_file(game_data_dir, soFile);
if (method == Config::InjectionMethod::CUSTOM_LINKER) {
load_so_file_custom_linker(game_data_dir, soFile, vm);
} else if (method == Config::InjectionMethod::RIRU) {
load_so_file_riru(game_data_dir, soFile);
} else {
load_so_file_standard(game_data_dir, soFile);
}
}
// Cleanup custom linker resources when done (if used)
if (method == Config::InjectionMethod::CUSTOM_LINKER) {
// Keep libraries loaded, don't cleanup
LOGI("Custom linker injection completed, libraries remain loaded");
}
}
void hack_prepare(const char *game_data_dir, const char *package_name, void *data, size_t length) {
void hack_prepare(const char *game_data_dir, const char *package_name, void *data, size_t length, JavaVM *vm) {
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);
hack_thread.detach();
std::thread hack_thread(hack_thread_func, game_data_dir, package_name, vm);
hack_thread.join();
}

View File

@@ -42,9 +42,22 @@ public:
void postAppSpecialize(const AppSpecializeArgs *) override {
if (enable_hack) {
// Then start hack thread
std::thread hack_thread(hack_prepare, _data_dir, _package_name, data, length);
hack_thread.detach();
// 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();
} else {
LOGE("Failed to get JavaVM");
}
}
}

View File

@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.18.1)
project(ElfLoader)
set(CMAKE_CXX_STANDARD 17)
include_directories(include)
set(SOURCES
mylinker.cpp
elf_loader.cpp
elf_reader.cpp
memory_manager.cpp
relocator.cpp
soinfo_manager.cpp
utils.cpp
)
find_library(log-lib log)
# Build as static library to be linked into main module
add_library(mylinker STATIC ${SOURCES})
target_link_libraries(mylinker ${log-lib})
# Only build executable for standalone testing
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_executable(elf_loader_test main.cpp elf_loader.cpp elf_reader.cpp memory_manager.cpp relocator.cpp soinfo_manager.cpp utils.cpp)
target_link_libraries(elf_loader_test ${log-lib})
endif()

View File

@@ -0,0 +1,187 @@
#include "elf_loader.h"
ElfLoader::ElfLoader() : loaded_si_(nullptr) {
reader_ = std::make_unique<ElfReader>();
memory_manager_ = std::make_unique<MemoryManager>();
soinfo_manager_ = std::make_unique<SoinfoManager>();
relocator_ = std::make_unique<Relocator>();
}
ElfLoader::~ElfLoader() {
}
bool ElfLoader::LoadLibrary(const char* path) {
LOGI("Loading library: %s", path);
if (!reader_->Open(path)) {
LOGE("Failed to open %s", path);
return false;
}
if (!reader_->Read()) {
LOGE("Failed to read ELF file");
return false;
}
if (!memory_manager_->ReserveAddressSpace(reader_->GetProgramHeaders(),
reader_->GetProgramHeaderCount())) {
LOGE("Failed to reserve address space");
return false;
}
if (!memory_manager_->LoadSegments(reader_->GetProgramHeaders(),
reader_->GetProgramHeaderCount(),
reader_->GetMappedAddr(),
reader_->GetFileSize())) {
LOGE("Failed to load segments");
return false;
}
if (!memory_manager_->FindPhdr(reader_->GetProgramHeaders(),
reader_->GetProgramHeaderCount())) {
LOGE("Failed to find program headers");
return false;
}
const char* basename = strrchr(path, '/');
basename = basename ? basename + 1 : path;
loaded_si_ = soinfo_manager_->GetOrCreateSoinfo(basename);
if (!soinfo_manager_->UpdateSoinfo(loaded_si_, memory_manager_.get(), reader_.get())) {
LOGE("Failed to update soinfo");
return false;
}
if (!soinfo_manager_->PrelinkImage(loaded_si_)) {
LOGE("Failed to prelink image");
return false;
}
if (!memory_manager_->ProtectSegments(reader_->GetProgramHeaders(),
reader_->GetProgramHeaderCount())) {
LOGE("Failed to protect segments");
return false;
}
if (!relocator_->LinkImage(loaded_si_)) {
LOGE("Failed to link image");
return false;
}
reader_->Close();
LOGI("Successfully loaded %s", path);
return true;
}
void ElfLoader::CallConstructors() {
if (loaded_si_ == nullptr) {
return;
}
LOGD("Constructors already called during linking");
}
void* ElfLoader::GetSymbol(const char* name) {
if (loaded_si_ == nullptr) {
LOGE("loaded_si_ is null");
return nullptr;
}
if (name == nullptr) {
LOGE("Symbol name is null");
return nullptr;
}
LOGD("Looking for symbol: %s", name);
LOGD("soinfo state: symtab=%p, strtab=%p, gnu_bucket=%p, bucket=%p",
loaded_si_->symtab, loaded_si_->strtab, loaded_si_->gnu_bucket, loaded_si_->bucket);
if (loaded_si_->symtab != nullptr) {
if (loaded_si_->gnu_bucket != nullptr) {
LOGD("Trying GNU hash lookup for %s", name);
uint32_t hash = relocator_->gnu_hash(name);
LOGD("GNU hash for %s: 0x%x", name, hash);
ElfW(Sym)* sym = relocator_->gnu_lookup(hash, name, loaded_si_);
if (sym != nullptr && sym->st_shndx != SHN_UNDEF) {
ElfW(Addr) addr = sym->st_value + loaded_si_->load_bias;
LOGD("Found symbol %s via GNU hash: st_value=0x%llx, load_bias=0x%llx, final_addr=0x%llx",
name, (unsigned long long)sym->st_value, (unsigned long long)loaded_si_->load_bias, (unsigned long long)addr);
if (addr >= loaded_si_->base && addr < loaded_si_->base + loaded_si_->size) {
return reinterpret_cast<void*>(addr);
} else {
LOGE("Symbol %s address 0x%llx out of range [0x%llx, 0x%llx)",
name, (unsigned long long)addr, (unsigned long long)loaded_si_->base,
(unsigned long long)(loaded_si_->base + loaded_si_->size));
}
} else {
LOGD("Symbol %s not found via GNU hash", name);
}
}
if (loaded_si_->bucket != nullptr) {
LOGD("Trying ELF hash lookup for %s", name);
unsigned hash = relocator_->elf_hash(name);
LOGD("ELF hash for %s: 0x%x", name, hash);
ElfW(Sym)* sym = relocator_->elf_lookup(hash, name, loaded_si_);
if (sym != nullptr && sym->st_shndx != SHN_UNDEF) {
ElfW(Addr) addr = sym->st_value + loaded_si_->load_bias;
LOGD("Found symbol %s via ELF hash: st_value=0x%llx, load_bias=0x%llx, final_addr=0x%llx",
name, (unsigned long long)sym->st_value, (unsigned long long)loaded_si_->load_bias, (unsigned long long)addr);
if (addr >= loaded_si_->base && addr < loaded_si_->base + loaded_si_->size) {
return reinterpret_cast<void*>(addr);
} else {
LOGE("Symbol %s address 0x%llx out of range [0x%llx, 0x%llx)",
name, (unsigned long long)addr, (unsigned long long)loaded_si_->base,
(unsigned long long)(loaded_si_->base + loaded_si_->size));
}
} else {
LOGD("Symbol %s not found via ELF hash", name);
}
}
if (loaded_si_->gnu_bucket == nullptr && loaded_si_->bucket == nullptr) {
LOGD("No hash tables available, trying linear search");
if (loaded_si_->strtab != nullptr) {
size_t sym_count = 0;
if (loaded_si_->nchain > 0) {
sym_count = loaded_si_->nchain;
} else {
LOGD("Cannot determine symbol table size");
return nullptr;
}
LOGD("Trying linear search with %zu symbols", sym_count);
for (size_t i = 0; i < sym_count; ++i) {
ElfW(Sym)* sym = &loaded_si_->symtab[i];
if (sym->st_name != 0 && sym->st_shndx != SHN_UNDEF) {
const char* sym_name = loaded_si_->strtab + sym->st_name;
if (strcmp(sym_name, name) == 0) {
ElfW(Addr) addr = sym->st_value + loaded_si_->load_bias;
LOGD("Found symbol %s via linear search: st_value=0x%llx, load_bias=0x%llx, final_addr=0x%llx",
name, (unsigned long long)sym->st_value, (unsigned long long)loaded_si_->load_bias, (unsigned long long)addr);
if (addr >= loaded_si_->base && addr < loaded_si_->base + loaded_si_->size) {
return reinterpret_cast<void*>(addr);
} else {
LOGE("Symbol %s address 0x%llx out of range [0x%llx, 0x%llx)",
name, (unsigned long long)addr, (unsigned long long)loaded_si_->base, (unsigned long long)(loaded_si_->base + loaded_si_->size));
}
}
}
}
LOGD("Symbol %s not found via linear search", name);
}
}
} else {
LOGE("Symbol table is null");
}
LOGE("Symbol %s not found in any method", name);
return nullptr;
}

View File

@@ -0,0 +1,149 @@
#include "elf_reader.h"
#include <sys/types.h>
ElfReader::ElfReader() : fd_(-1), file_size_(0), file_offset_(0),
mapped_file_(nullptr), phdr_table_(nullptr), phdr_num_(0) {
memset(&header_, 0, sizeof(header_));
}
ElfReader::~ElfReader() {
Close();
}
bool ElfReader::Open(const char* path) {
path_ = path;
struct stat sb;
fd_ = open(path, O_RDONLY | O_CLOEXEC);
if (fd_ < 0) {
LOGE("Cannot open %s: %s", path, strerror(errno));
return false;
}
if (fstat(fd_, &sb) < 0) {
LOGE("Cannot stat %s: %s", path, strerror(errno));
close(fd_);
fd_ = -1;
return false;
}
file_size_ = sb.st_size;
// 映射整个文件到内存
mapped_file_ = mmap(nullptr, file_size_, PROT_READ, MAP_PRIVATE, fd_, 0);
if (mapped_file_ == MAP_FAILED) {
LOGE("Cannot mmap %s: %s", path, strerror(errno));
close(fd_);
fd_ = -1;
return false;
}
return true;
}
bool ElfReader::Read() {
if (!ReadElfHeader()) {
return false;
}
if (!VerifyElfHeader()) {
return false;
}
if (!ReadProgramHeaders()) {
return false;
}
return true;
}
void ElfReader::Close() {
if (mapped_file_ != nullptr && mapped_file_ != MAP_FAILED) {
munmap(mapped_file_, file_size_);
mapped_file_ = nullptr;
}
if (fd_ >= 0) {
close(fd_);
fd_ = -1;
}
if (phdr_table_ != nullptr) {
free(phdr_table_);
phdr_table_ = nullptr;
}
}
bool ElfReader::ReadElfHeader() {
if (file_size_ < sizeof(ElfW(Ehdr))) {
LOGE("File too small for ELF header");
return false;
}
memcpy(&header_, mapped_file_, sizeof(header_));
return true;
}
bool ElfReader::VerifyElfHeader() {
if (memcmp(header_.e_ident, ELFMAG, SELFMAG) != 0) {
LOGE("Invalid ELF magic");
return false;
}
if (header_.e_ident[EI_CLASS] != ELFCLASS64) {
LOGE("Not a 64-bit ELF file");
return false;
}
if (header_.e_machine != EM_AARCH64) {
LOGE("Not an ARM64 ELF file");
return false;
}
if (header_.e_version != EV_CURRENT) {
LOGE("Invalid ELF version");
return false;
}
if (header_.e_type != ET_DYN) {
LOGE("Not a shared object");
return false;
}
LOGD("ELF Header: type=%d, machine=%d, entry=0x%llx, phoff=0x%llx, phnum=%d",
header_.e_type, header_.e_machine, (unsigned long long)header_.e_entry,
(unsigned long long)header_.e_phoff, header_.e_phnum);
return true;
}
bool ElfReader::ReadProgramHeaders() {
phdr_num_ = header_.e_phnum;
if (phdr_num_ == 0) {
LOGE("No program headers");
return false;
}
if (header_.e_phentsize != sizeof(ElfW(Phdr))) {
LOGE("Invalid program header size");
return false;
}
size_t size = phdr_num_ * sizeof(ElfW(Phdr));
if (header_.e_phoff + size > file_size_) {
LOGE("Program headers out of file bounds");
return false;
}
phdr_table_ = static_cast<ElfW(Phdr)*>(malloc(size));
if (phdr_table_ == nullptr) {
LOGE("Cannot allocate memory for program headers");
return false;
}
memcpy(phdr_table_, static_cast<char*>(mapped_file_) + header_.e_phoff, size);
return true;
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <android/log.h>
#include <elf.h>
#include <link.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>
#define LOG_TAG "CustomLinker"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#if defined(__LP64__)
#define ELFW(what) ELF64_ ## what
#else
#define ELFW(what) ELF32_ ## what
#endif
#define PAGE_SIZE 4096
#define PAGE_MASK (~(PAGE_SIZE - 1))
#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr) PAGE_START((addr) + PAGE_SIZE - 1)
#define PAGE_OFFSET(addr) ((addr) & (PAGE_SIZE - 1))
// 权限标志转换
#define PFLAGS_TO_PROT(x) (((x) & PF_R) ? PROT_READ : 0) | \
(((x) & PF_W) ? PROT_WRITE : 0) | \
(((x) & PF_X) ? PROT_EXEC : 0)
struct soinfo;
class ElfReader;
class MemoryManager;
class Relocator;

View File

@@ -0,0 +1,27 @@
#pragma once
#include "common.h"
#include "elf_reader.h"
#include "memory_manager.h"
#include "soinfo_manager.h"
#include "relocator.h"
class ElfLoader {
public:
ElfLoader();
~ElfLoader();
bool LoadLibrary(const char* path);
void CallConstructors();
void* GetSymbol(const char* name);
private:
std::unique_ptr<ElfReader> reader_;
std::unique_ptr<MemoryManager> memory_manager_;
std::unique_ptr<SoinfoManager> soinfo_manager_;
std::unique_ptr<Relocator> relocator_;
soinfo* loaded_si_;
};

View File

@@ -0,0 +1,38 @@
#pragma once
#include "common.h"
class ElfReader {
public:
ElfReader();
~ElfReader();
bool Open(const char* path);
bool Read();
void Close();
const ElfW(Ehdr)* GetHeader() const { return &header_; }
const ElfW(Phdr)* GetProgramHeaders() const { return phdr_table_; }
size_t GetProgramHeaderCount() const { return phdr_num_; }
const char* GetPath() const { return path_.c_str(); }
int GetFd() const { return fd_; }
size_t GetFileSize() const { return file_size_; }
void* GetMappedAddr() const { return mapped_file_; }
private:
bool ReadElfHeader();
bool ReadProgramHeaders();
bool VerifyElfHeader();
std::string path_;
int fd_;
size_t file_size_;
off64_t file_offset_;
void* mapped_file_;
ElfW(Ehdr) header_;
ElfW(Phdr)* phdr_table_;
size_t phdr_num_;
};

View File

@@ -0,0 +1,34 @@
#pragma once
#include "common.h"
class MemoryManager {
public:
MemoryManager();
~MemoryManager();
bool ReserveAddressSpace(const ElfW(Phdr)* phdr_table, size_t phdr_num);
bool LoadSegments(const ElfW(Phdr)* phdr_table, size_t phdr_num,
void* mapped_file, size_t file_size);
bool FindPhdr(const ElfW(Phdr)* phdr_table, size_t phdr_num);
bool ProtectSegments(const ElfW(Phdr)* phdr_table, size_t phdr_num);
void* GetLoadStart() const { return load_start_; }
size_t GetLoadSize() const { return load_size_; }
ElfW(Addr) GetLoadBias() const { return load_bias_; }
const ElfW(Phdr)* GetLoadedPhdr() const { return loaded_phdr_; }
private:
bool CheckPhdr(ElfW(Addr) loaded, const ElfW(Phdr)* phdr_table, size_t phdr_num);
size_t phdr_table_get_load_size(const ElfW(Phdr)* phdr_table,
size_t phdr_count,
ElfW(Addr)* min_vaddr);
void* load_start_;
size_t load_size_;
ElfW(Addr) load_bias_;
const ElfW(Phdr)* loaded_phdr_;
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include <jni.h>
#ifdef __cplusplus
extern "C" {
#endif
__attribute__((visibility("default"))) bool mylinker_load_library(const char* library_path, JavaVM* vm);
__attribute__((visibility("default"))) void* mylinker_get_symbol(const char* library_path, const char* symbol_name);
__attribute__((visibility("default"))) void mylinker_cleanup();
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,25 @@
#pragma once
#include "common.h"
#include "soinfo_manager.h"
class Relocator {
public:
Relocator();
~Relocator();
bool RelocateImage(soinfo* si);
bool LinkImage(soinfo* si);
uint32_t gnu_hash(const char* name);
unsigned elf_hash(const char* name);
ElfW(Sym)* gnu_lookup(uint32_t hash, const char* name, soinfo* si);
ElfW(Sym)* elf_lookup(unsigned hash, const char* name, soinfo* si);
private:
bool ProcessRelaRelocation(soinfo* si, const ElfW(Rela)* rela);
ElfW(Addr) FindSymbolAddress(const char* name, soinfo* si);
};

View File

@@ -0,0 +1,75 @@
#pragma once
#include "common.h"
// soinfo结构体定义(简化版)
struct soinfo {
const char* name;
ElfW(Addr) base;
size_t size;
ElfW(Addr) load_bias;
const ElfW(Phdr)* phdr;
size_t phnum;
ElfW(Addr) entry;
// Dynamic段信息
ElfW(Dyn)* dynamic;
size_t dynamic_count;
// 符号表相关
const char* strtab;
ElfW(Sym)* symtab;
size_t nbucket;
size_t nchain;
uint32_t* bucket;
uint32_t* chain;
// 重定位相关
ElfW(Rela)* plt_rela;
size_t plt_rela_count;
ElfW(Rela)* rela;
size_t rela_count;
// GNU hash
size_t gnu_nbucket;
uint32_t* gnu_bucket;
uint32_t* gnu_chain;
uint32_t gnu_maskwords;
uint32_t gnu_shift2;
ElfW(Addr)* gnu_bloom_filter;
// 初始化函数
void (*init_func)();
void (**init_array)();
size_t init_array_count;
void (**fini_array)();
size_t fini_array_count;
// 依赖库
std::vector<std::string> needed_libs;
uint32_t flags;
};
class SoinfoManager {
public:
SoinfoManager();
~SoinfoManager();
soinfo* GetOrCreateSoinfo(const char* name);
bool UpdateSoinfo(soinfo* si, MemoryManager* mm, ElfReader* reader);
bool PrelinkImage(soinfo* si);
soinfo* FindSoinfo(const char* name);
soinfo* GetCurrentSoinfo();
private:
bool ParseDynamic(soinfo* si);
void ApplyRelaSections(soinfo* si);
std::unordered_map<std::string, std::unique_ptr<soinfo>> soinfo_map_;
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include "common.h"
namespace Utils {
bool safe_add(off64_t* out, off64_t a, size_t b);
soinfo* get_soinfo(const char* so_name);
void* getMapData(int fd, off64_t base_offset, size_t elf_offset, size_t size);
ElfW(Addr) get_export_func(const char* path, const char* func_name);
inline size_t page_start(size_t addr) {
return addr & ~(PAGE_SIZE - 1);
}
inline size_t page_offset(size_t addr) {
return addr & (PAGE_SIZE - 1);
}
}

View File

@@ -0,0 +1,49 @@
#include "elf_loader.h"
int (*yuuki_test_func) (int, int) = nullptr;
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s <so_file_path>\n", argv[0]);
return 1;
}
LOGI("Starting custom linker for: %s", argv[1]);
// 检查文件是否存在
if (access(argv[1], F_OK) != 0) {
LOGE("File does not exist: %s", argv[1]);
return 1;
}
if (access(argv[1], R_OK) != 0) {
LOGE("File is not readable: %s", argv[1]);
return 1;
}
ElfLoader loader;
if (loader.LoadLibrary(argv[1])) {
printf("Successfully loaded %s\n", argv[1]);
void* test_func = loader.GetSymbol("yuuki_test");
if (test_func) {
printf("Found yuuki_test function at %p\n", test_func);
yuuki_test_func = (int (*)(int, int)) test_func;
// 测试函数调用
printf("Testing function call: 1 + 1 = %d\n", yuuki_test_func(1, 1));
printf("Testing function call: 5 + 3 = %d\n", yuuki_test_func(5, 3));
} else {
printf("Failed to find yuuki_test function\n");
}
return 0;
} else {
printf("Failed to load %s\n", argv[1]);
return 1;
}
}
// logcat | grep "CustomLinker"
// logcat | grep "TEST_SO"
// ./data/local/tmp/elf_loader /storage/emulated/0/yuuki/test.so

View File

@@ -0,0 +1,241 @@
#include "memory_manager.h"
MemoryManager::MemoryManager() : load_start_(nullptr), load_size_(0),
load_bias_(0), loaded_phdr_(nullptr) {
}
MemoryManager::~MemoryManager() {
}
bool MemoryManager::ReserveAddressSpace(const ElfW(Phdr)* phdr_table, size_t phdr_num) {
ElfW(Addr) min_vaddr;
load_size_ = phdr_table_get_load_size(phdr_table, phdr_num, &min_vaddr);
if (load_size_ == 0) {
LOGE("No loadable segments");
return false;
}
LOGD("Load size: 0x%zx, min_vaddr: 0x%llx", load_size_, (unsigned long long)min_vaddr);
void* start = mmap(nullptr, load_size_, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (start == MAP_FAILED) {
LOGE("Cannot reserve %zu bytes: %s", load_size_, strerror(errno));
return false;
}
load_start_ = start;
load_bias_ = reinterpret_cast<ElfW(Addr)>(start) - min_vaddr;
LOGD("Reserved address space at %p, bias: 0x%llx", start, (unsigned long long)load_bias_);
return true;
}
bool MemoryManager::LoadSegments(const ElfW(Phdr)* phdr_table, size_t phdr_num,
void* mapped_file, size_t file_size) {
LOGD("Starting LoadSegments: phdr_num=%zu, file_size=%zu", phdr_num, file_size);
for (size_t i = 0; i < phdr_num; ++i) {
const ElfW(Phdr)* phdr = &phdr_table[i];
if (phdr->p_type != PT_LOAD) {
continue;
}
LOGD("Processing LOAD segment %zu: vaddr=0x%llx, memsz=0x%llx, filesz=0x%llx, offset=0x%llx",
i, (unsigned long long)phdr->p_vaddr, (unsigned long long)phdr->p_memsz,
(unsigned long long)phdr->p_filesz, (unsigned long long)phdr->p_offset);
ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
ElfW(Addr) seg_end = seg_start + phdr->p_memsz;
ElfW(Addr) seg_page_start = PAGE_START(seg_start);
ElfW(Addr) seg_page_end = PAGE_END(seg_end);
ElfW(Addr) seg_file_end = seg_start + phdr->p_filesz;
ElfW(Addr) file_start = phdr->p_offset;
ElfW(Addr) file_end = file_start + phdr->p_filesz;
ElfW(Addr) file_page_start = PAGE_START(file_start);
if (file_end > file_size) {
LOGE("Invalid file size: file_end=0x%llx > file_size=0x%zx", (unsigned long long)file_end, file_size);
return false;
}
if (phdr->p_filesz > 0) {
void* seg_addr = reinterpret_cast<void*>(seg_page_start);
size_t seg_size = seg_page_end - seg_page_start;
if (mprotect(seg_addr, seg_size, PROT_READ | PROT_WRITE) < 0) {
LOGE("Cannot mprotect for loading: %s", strerror(errno));
return false;
}
void* src = static_cast<char*>(mapped_file) + phdr->p_offset;
void* dst = reinterpret_cast<void*>(seg_start);
LOGD("Copying segment %zu: src=%p (offset=0x%llx), dst=%p, size=0x%llx",
i, src, (unsigned long long)phdr->p_offset, dst, (unsigned long long)phdr->p_filesz);
if (static_cast<char*>(src) + phdr->p_filesz > static_cast<char*>(mapped_file) + file_size) {
LOGE("Source copy would exceed file bounds");
return false;
}
if (reinterpret_cast<ElfW(Addr)>(dst) + phdr->p_filesz > seg_page_end) {
LOGE("Destination copy would exceed segment bounds");
return false;
}
memcpy(dst, src, phdr->p_filesz);
LOGD("Successfully copied segment %zu", i);
}
if (phdr->p_memsz > phdr->p_filesz) {
ElfW(Addr) bss_start = seg_start + phdr->p_filesz;
ElfW(Addr) bss_end = seg_start + phdr->p_memsz;
size_t bss_size = bss_end - bss_start;
LOGD("Zeroing BSS: start=0x%llx, size=0x%zx", (unsigned long long)bss_start, bss_size);
memset(reinterpret_cast<void*>(bss_start), 0, bss_size);
}
ElfW(Addr) aligned_file_end = PAGE_END(seg_file_end);
if (seg_page_end > aligned_file_end) {
size_t zeromap_size = seg_page_end - aligned_file_end;
void* zeromap = mmap(reinterpret_cast<void*>(aligned_file_end),
zeromap_size,
PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE,
-1, 0);
if (zeromap == MAP_FAILED) {
LOGE("Cannot zero fill gap: %s", strerror(errno));
return false;
}
LOGD("Zero-filled gap: addr=%p, size=0x%zx", zeromap, zeromap_size);
}
}
LOGD("LoadSegments complete");
return true;
}
bool MemoryManager::FindPhdr(const ElfW(Phdr)* phdr_table, size_t phdr_num) {
const ElfW(Phdr)* phdr_limit = phdr_table + phdr_num;
for (const ElfW(Phdr)* phdr = phdr_table; phdr < phdr_limit; ++phdr) {
if (phdr->p_type == PT_PHDR) {
return CheckPhdr(load_bias_ + phdr->p_vaddr, phdr_table, phdr_num);
}
}
for (const ElfW(Phdr)* phdr = phdr_table; phdr < phdr_limit; ++phdr) {
if (phdr->p_type == PT_LOAD) {
if (phdr->p_offset == 0) {
ElfW(Addr) elf_addr = load_bias_ + phdr->p_vaddr;
const ElfW(Ehdr)* ehdr = reinterpret_cast<const ElfW(Ehdr)*>(elf_addr);
ElfW(Addr) offset = ehdr->e_phoff;
return CheckPhdr(reinterpret_cast<ElfW(Addr)>(ehdr) + offset, phdr_table, phdr_num);
}
break;
}
}
LOGD("Using original phdr_table as loaded_phdr");
loaded_phdr_ = phdr_table;
return true;
}
bool MemoryManager::ProtectSegments(const ElfW(Phdr)* phdr_table, size_t phdr_num) {
for (size_t i = 0; i < phdr_num; ++i) {
const ElfW(Phdr)* phdr = &phdr_table[i];
if (phdr->p_type != PT_LOAD) {
continue;
}
ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
ElfW(Addr) seg_page_start = PAGE_START(seg_start);
ElfW(Addr) seg_page_end = PAGE_END(seg_start + phdr->p_memsz);
int prot = PFLAGS_TO_PROT(phdr->p_flags);
if (mprotect(reinterpret_cast<void*>(seg_page_start),
seg_page_end - seg_page_start, prot) < 0) {
LOGE("Cannot protect segment %zu: %s", i, strerror(errno));
return false;
}
LOGD("Protected segment %zu: 0x%llx-0x%llx, prot: %d",
i, (unsigned long long)seg_page_start, (unsigned long long)seg_page_end, prot);
}
return true;
}
bool MemoryManager::CheckPhdr(ElfW(Addr) loaded, const ElfW(Phdr)* phdr_table, size_t phdr_num) {
const ElfW(Phdr)* phdr_limit = phdr_table + phdr_num;
ElfW(Addr) loaded_end = loaded + (phdr_num * sizeof(ElfW(Phdr)));
for (const ElfW(Phdr)* phdr = phdr_table; phdr < phdr_limit; ++phdr) {
if (phdr->p_type != PT_LOAD) {
continue;
}
ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
ElfW(Addr) seg_end = phdr->p_filesz + seg_start;
if (seg_start <= loaded && loaded_end <= seg_end) {
loaded_phdr_ = reinterpret_cast<const ElfW(Phdr)*>(loaded);
return true;
}
}
LOGE("Loaded phdr %p not in loadable segment", reinterpret_cast<void*>(loaded));
return false;
}
size_t MemoryManager::phdr_table_get_load_size(const ElfW(Phdr)* phdr_table,
size_t phdr_count,
ElfW(Addr)* min_vaddr) {
ElfW(Addr) min_addr = UINTPTR_MAX;
ElfW(Addr) max_addr = 0;
bool found_pt_load = false;
for (size_t i = 0; i < phdr_count; ++i) {
const ElfW(Phdr)* phdr = &phdr_table[i];
if (phdr->p_type != PT_LOAD) {
continue;
}
found_pt_load = true;
if (phdr->p_vaddr < min_addr) {
min_addr = phdr->p_vaddr;
}
if (phdr->p_vaddr + phdr->p_memsz > max_addr) {
max_addr = phdr->p_vaddr + phdr->p_memsz;
}
}
if (!found_pt_load) {
return 0;
}
min_addr = PAGE_START(min_addr);
max_addr = PAGE_END(max_addr);
if (min_vaddr != nullptr) {
*min_vaddr = min_addr;
}
return max_addr - min_addr;
}

View File

@@ -0,0 +1,61 @@
#include "mylinker.h"
#include "elf_loader.h"
#include "common.h"
#include <unordered_map>
#include <memory>
#include <string>
static std::unordered_map<std::string, std::unique_ptr<ElfLoader>> loaded_libraries;
bool mylinker_load_library(const char* library_path, JavaVM* vm) {
if (!library_path) {
LOGE("Invalid library path");
return false;
}
std::string path(library_path);
if (loaded_libraries.find(path) != loaded_libraries.end()) {
LOGI("Library already loaded: %s", library_path);
return true;
}
auto loader = std::make_unique<ElfLoader>();
if (!loader->LoadLibrary(library_path)) {
LOGE("Failed to load library: %s", library_path);
return false;
}
JNIEnv* env = nullptr;
if (vm && vm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_OK) {
typedef jint (*JNI_OnLoad_t)(JavaVM*, void*);
auto jni_onload = reinterpret_cast<JNI_OnLoad_t>(loader->GetSymbol("JNI_OnLoad"));
if (jni_onload) {
LOGI("Calling JNI_OnLoad");
jni_onload(vm, nullptr);
}
}
loaded_libraries[path] = std::move(loader);
LOGI("Successfully loaded library: %s", library_path);
return true;
}
void* mylinker_get_symbol(const char* library_path, const char* symbol_name) {
if (!library_path || !symbol_name) {
return nullptr;
}
auto it = loaded_libraries.find(library_path);
if (it == loaded_libraries.end()) {
LOGE("Library not loaded: %s", library_path);
return nullptr;
}
return it->second->GetSymbol(symbol_name);
}
void mylinker_cleanup() {
loaded_libraries.clear();
LOGI("Cleaned up all loaded libraries");
}

View File

@@ -0,0 +1,342 @@
#include "relocator.h"
// Only define if not already defined
#ifndef R_AARCH64_NONE
#define R_AARCH64_NONE 0
#endif
#ifndef R_AARCH64_ABS64
#define R_AARCH64_ABS64 257
#endif
#ifndef R_AARCH64_GLOB_DAT
#define R_AARCH64_GLOB_DAT 1025
#endif
#ifndef R_AARCH64_JUMP_SLOT
#define R_AARCH64_JUMP_SLOT 1026
#endif
#ifndef R_AARCH64_RELATIVE
#define R_AARCH64_RELATIVE 1027
#endif
#ifndef R_AARCH64_TLS_TPREL64
#define R_AARCH64_TLS_TPREL64 1030
#endif
#ifndef R_AARCH64_TLS_DTPREL32
#define R_AARCH64_TLS_DTPREL32 1031
#endif
#ifndef R_AARCH64_IRELATIVE
#define R_AARCH64_IRELATIVE 1032
#endif
Relocator::Relocator() {
}
Relocator::~Relocator() {
}
bool Relocator::RelocateImage(soinfo* si) {
LOGD("Starting relocation for %s", si->name);
if (!si) {
LOGE("soinfo is null");
return false;
}
if (si->rela != nullptr && si->rela_count > 0) {
LOGD("Processing %zu RELA relocations", si->rela_count);
if (si->rela_count > 100000) {
LOGE("RELA count too large: %zu", si->rela_count);
return false;
}
for (size_t i = 0; i < si->rela_count; ++i) {
if (!ProcessRelaRelocation(si, &si->rela[i])) {
LOGE("Failed to process RELA relocation %zu", i);
// 继续处理其他重定位,不要因为一个失败就退出
// return false;
}
}
} else {
LOGD("No RELA relocations to process");
}
if (si->plt_rela != nullptr && si->plt_rela_count > 0) {
LOGD("Processing %zu PLT RELA relocations", si->plt_rela_count);
if (si->plt_rela_count > 10000) {
LOGE("PLT RELA count too large: %zu", si->plt_rela_count);
return false;
}
for (size_t i = 0; i < si->plt_rela_count; ++i) {
if (!ProcessRelaRelocation(si, &si->plt_rela[i])) {
LOGE("Failed to process PLT RELA relocation %zu", i);
// 继续处理其他重定位
// return false;
}
}
} else {
LOGD("No PLT RELA relocations to process");
}
LOGD("Relocation complete for %s", si->name);
return true;
}
bool Relocator::LinkImage(soinfo* si) {
if (!si) {
LOGE("soinfo is null in LinkImage");
return false;
}
if (!RelocateImage(si)) {
LOGE("Failed to relocate image");
return false;
}
if (si->init_func != nullptr) {
LOGD("Calling init function at %p", si->init_func);
si->init_func();
}
if (si->init_array != nullptr && si->init_array_count > 0) {
LOGD("Calling %zu init_array functions", si->init_array_count);
if (si->init_array_count > 1000) {
LOGE("init_array_count too large: %zu", si->init_array_count);
return false;
}
for (size_t i = 0; i < si->init_array_count; ++i) {
void (*func)() = si->init_array[i];
if (func != nullptr) {
LOGD("Calling init_array[%zu] at %p", i, func);
func();
}
}
}
return true;
}
bool Relocator::ProcessRelaRelocation(soinfo* si, const ElfW(Rela)* rela) {
if (!si || !rela) {
LOGE("Invalid parameters in ProcessRelaRelocation");
return false;
}
ElfW(Addr) reloc = static_cast<ElfW(Addr)>(rela->r_offset + si->load_bias);
ElfW(Word) type = ELFW(R_TYPE)(rela->r_info);
ElfW(Word) sym = ELFW(R_SYM)(rela->r_info);
LOGD("Processing relocation: offset=0x%llx, type=%d, sym=%d, addend=0x%llx",
(unsigned long long)rela->r_offset, type, sym, (long long)rela->r_addend);
if (reloc < si->base || reloc >= si->base + si->size) {
LOGE("Relocation address 0x%llx out of range [0x%llx, 0x%llx)",
(unsigned long long)reloc, (unsigned long long)si->base, (unsigned long long)(si->base + si->size));
return false;
}
ElfW(Addr) sym_addr = 0;
const char* sym_name = nullptr;
if (sym != 0) {
if (!si->symtab) {
LOGE("Symbol table is null");
return false;
}
const ElfW(Sym)* s = &si->symtab[sym];
if (si->strtab && s->st_name != 0) {
sym_name = si->strtab + s->st_name;
LOGD("Symbol name: %s", sym_name);
}
if (s->st_shndx != SHN_UNDEF) {
sym_addr = s->st_value + si->load_bias;
LOGD("Local symbol: addr=0x%llx", (unsigned long long)sym_addr);
} else if (sym_name) {
sym_addr = FindSymbolAddress(sym_name, si);
if (sym_addr == 0) {
LOGD("Cannot find symbol: %s (may be optional)", sym_name);
}
}
}
void* page_start = reinterpret_cast<void*>(PAGE_START(reloc));
size_t page_size = PAGE_SIZE;
int old_prot = PROT_READ | PROT_WRITE;
if (mprotect(page_start, page_size, old_prot) != 0) {
LOGD("mprotect failed for relocation, trying anyway: %s", strerror(errno));
}
switch (type) {
case R_AARCH64_NONE:
LOGD("R_AARCH64_NONE");
break;
case R_AARCH64_ABS64:
LOGD("R_AARCH64_ABS64: writing 0x%llx to 0x%llx",
(unsigned long long)(sym_addr + rela->r_addend), (unsigned long long)reloc);
*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr + rela->r_addend;
break;
case R_AARCH64_GLOB_DAT:
LOGD("R_AARCH64_GLOB_DAT: writing 0x%llx to 0x%llx",
(unsigned long long)(sym_addr + rela->r_addend), (unsigned long long)reloc);
*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr + rela->r_addend;
break;
case R_AARCH64_JUMP_SLOT:
LOGD("R_AARCH64_JUMP_SLOT: writing 0x%llx to 0x%llx",
(unsigned long long)(sym_addr + rela->r_addend), (unsigned long long)reloc);
*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr + rela->r_addend;
break;
case R_AARCH64_RELATIVE:
LOGD("R_AARCH64_RELATIVE: writing 0x%llx to 0x%llx",
(unsigned long long)(si->load_bias + rela->r_addend), (unsigned long long)reloc);
*reinterpret_cast<ElfW(Addr)*>(reloc) = si->load_bias + rela->r_addend;
break;
case R_AARCH64_IRELATIVE:
{
ElfW(Addr) resolver = si->load_bias + rela->r_addend;
LOGD("R_AARCH64_IRELATIVE: resolver at 0x%llx", (unsigned long long)resolver);
if (resolver < si->base || resolver >= si->base + si->size) {
LOGE("Invalid resolver address: 0x%llx", (unsigned long long)resolver);
return false;
}
ElfW(Addr) resolved = ((ElfW(Addr) (*)())resolver)();
*reinterpret_cast<ElfW(Addr)*>(reloc) = resolved;
LOGD("R_AARCH64_IRELATIVE: resolved to 0x%llx", (unsigned long long)resolved);
}
break;
default:
LOGD("Unknown relocation type %d, skipping", type);
break;
}
return true;
}
ElfW(Addr) Relocator::FindSymbolAddress(const char* name, soinfo* si) {
if (!name || !si) {
return 0;
}
if (si->symtab != nullptr) {
if (si->gnu_bucket != nullptr) {
uint32_t hash = gnu_hash(name);
ElfW(Sym)* sym = gnu_lookup(hash, name, si);
if (sym != nullptr && sym->st_shndx != SHN_UNDEF) {
ElfW(Addr) addr = sym->st_value + si->load_bias;
LOGD("Found symbol %s in current SO at 0x%llx", name, (unsigned long long)addr);
return addr;
}
}
if (si->bucket != nullptr) {
unsigned hash = elf_hash(name);
ElfW(Sym)* sym = elf_lookup(hash, name, si);
if (sym != nullptr && sym->st_shndx != SHN_UNDEF) {
ElfW(Addr) addr = sym->st_value + si->load_bias;
LOGD("Found symbol %s in current SO at 0x%llx", name, (unsigned long long)addr);
return addr;
}
}
}
for (const auto& lib : si->needed_libs) {
void* handle = dlopen(lib.c_str(), RTLD_NOW | RTLD_NOLOAD);
if (handle != nullptr) {
void* addr = dlsym(handle, name);
if (addr != nullptr) {
LOGD("Found symbol %s in %s at %p", name, lib.c_str(), addr);
dlclose(handle);
return reinterpret_cast<ElfW(Addr)>(addr);
}
dlclose(handle);
}
}
void* addr = dlsym(RTLD_DEFAULT, name);
if (addr != nullptr) {
LOGD("Found symbol %s globally at %p", name, addr);
return reinterpret_cast<ElfW(Addr)>(addr);
}
LOGD("Symbol %s not found", name);
return 0;
}
ElfW(Sym)* Relocator::gnu_lookup(uint32_t hash, const char* name, soinfo* si) {
if (!si->gnu_bucket || !si->gnu_chain || !si->symtab || !si->strtab) {
return nullptr;
}
uint32_t h2 = hash >> si->gnu_shift2;
uint32_t bloom_mask_bits = sizeof(ElfW(Addr)) * 8;
uint32_t word_num = (hash / bloom_mask_bits) & si->gnu_maskwords;
ElfW(Addr) bloom_word = si->gnu_bloom_filter[word_num];
if ((1 & (bloom_word >> (hash % bloom_mask_bits)) &
(bloom_word >> (h2 % bloom_mask_bits))) == 0) {
return nullptr;
}
uint32_t n = si->gnu_bucket[hash % si->gnu_nbucket];
if (n == 0) {
return nullptr;
}
do {
ElfW(Sym)* s = si->symtab + n;
if (((si->gnu_chain[n] ^ hash) >> 1) == 0 &&
strcmp(si->strtab + s->st_name, name) == 0) {
return s;
}
} while ((si->gnu_chain[n++] & 1) == 0);
return nullptr;
}
ElfW(Sym)* Relocator::elf_lookup(unsigned hash, const char* name, soinfo* si) {
if (!si->bucket || !si->chain || !si->symtab || !si->strtab) {
return nullptr;
}
for (unsigned n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n]) {
ElfW(Sym)* s = si->symtab + n;
if (s->st_name != 0 && strcmp(si->strtab + s->st_name, name) == 0) {
return s;
}
}
return nullptr;
}
uint32_t Relocator::gnu_hash(const char* name) {
uint32_t h = 5381;
for (const uint8_t* c = reinterpret_cast<const uint8_t*>(name); *c != '\0'; c++) {
h += (h << 5) + *c;
}
return h;
}
unsigned Relocator::elf_hash(const char* name) {
unsigned h = 0, g;
for (const unsigned char* p = reinterpret_cast<const unsigned char*>(name); *p; p++) {
h = (h << 4) + *p;
g = h & 0xf0000000;
h ^= g;
h ^= g >> 24;
}
return h;
}

View File

@@ -0,0 +1,312 @@
#include "soinfo_manager.h"
#include "utils.h"
#include "memory_manager.h"
#include "elf_reader.h"
SoinfoManager::SoinfoManager() {
}
SoinfoManager::~SoinfoManager() {
}
soinfo* SoinfoManager::GetOrCreateSoinfo(const char* name) {
auto it = soinfo_map_.find(name);
if (it != soinfo_map_.end()) {
return it->second.get();
}
auto si = std::make_unique<soinfo>();
memset(si.get(), 0, sizeof(soinfo));
si->name = strdup(name);
soinfo* result = si.get();
soinfo_map_[name] = std::move(si);
return result;
}
bool SoinfoManager::UpdateSoinfo(soinfo* si, MemoryManager* mm, ElfReader* reader) {
if (!si || !mm || !reader) {
return false;
}
si->base = reinterpret_cast<ElfW(Addr)>(mm->GetLoadStart());
si->size = mm->GetLoadSize();
si->load_bias = mm->GetLoadBias();
const ElfW(Phdr)* loaded_phdr = mm->GetLoadedPhdr();
if (loaded_phdr != nullptr) {
si->phdr = loaded_phdr;
} else {
si->phdr = reader->GetProgramHeaders();
LOGD("Using original program headers");
}
si->phnum = reader->GetProgramHeaderCount();
const ElfW(Ehdr)* header = reader->GetHeader();
si->entry = si->load_bias + header->e_entry;
LOGD("Updated soinfo: base=0x%llx, size=0x%zx, bias=0x%llx, entry=0x%llx, phdr=%p",
(unsigned long long)si->base, si->size, (unsigned long long)si->load_bias,
(unsigned long long)si->entry, si->phdr);
return true;
}
bool SoinfoManager::PrelinkImage(soinfo* si) {
LOGD("Starting PrelinkImage for %s", si->name);
if (!ParseDynamic(si)) {
LOGE("Failed to parse dynamic section");
return false;
}
if (si->strtab != nullptr && si->dynamic != nullptr) {
for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_NEEDED && si->needed_libs.empty()) {
const char* needed = si->strtab + d->d_un.d_val;
si->needed_libs.push_back(needed);
LOGD("Processing deferred DT_NEEDED: %s", needed);
}
}
}
ApplyRelaSections(si);
LOGD("PrelinkImage complete for %s", si->name);
return true;
}
soinfo* SoinfoManager::FindSoinfo(const char* name) {
auto it = soinfo_map_.find(name);
if (it != soinfo_map_.end()) {
return it->second.get();
}
return nullptr;
}
soinfo* SoinfoManager::GetCurrentSoinfo() {
return Utils::get_soinfo("libcustom_linker.so");
}
bool SoinfoManager::ParseDynamic(soinfo* si) {
if (!si || !si->phdr) {
LOGE("Invalid soinfo or phdr is null");
return false;
}
LOGD("Starting ParseDynamic: phdr=%p, phnum=%zu", si->phdr, si->phnum);
const ElfW(Phdr)* phdr_limit = si->phdr + si->phnum;
bool found_dynamic = false;
for (const ElfW(Phdr)* phdr = si->phdr; phdr < phdr_limit; ++phdr) {
if (phdr->p_type == PT_DYNAMIC) {
si->dynamic = reinterpret_cast<ElfW(Dyn)*>(si->load_bias + phdr->p_vaddr);
si->dynamic_count = phdr->p_memsz / sizeof(ElfW(Dyn));
found_dynamic = true;
LOGD("Found PT_DYNAMIC at vaddr=0x%llx, memsz=0x%llx",
(unsigned long long)phdr->p_vaddr, (unsigned long long)phdr->p_memsz);
break;
}
}
if (!found_dynamic || !si->dynamic) {
LOGE("No PT_DYNAMIC segment found");
return false;
}
LOGD("Dynamic section at %p, count=%zu", si->dynamic, si->dynamic_count);
if (si->dynamic_count == 0 || si->dynamic_count > 1000) {
LOGE("Invalid dynamic count: %zu", si->dynamic_count);
return false;
}
size_t dyn_count = 0;
for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL && dyn_count < si->dynamic_count; ++d, ++dyn_count) {
LOGD("Processing dynamic entry %zu: tag=0x%llx, val/ptr=0x%llx",
dyn_count, (unsigned long long)d->d_tag, (unsigned long long)d->d_un.d_val);
switch (d->d_tag) {
case DT_SYMTAB:
si->symtab = reinterpret_cast<ElfW(Sym)*>(si->load_bias + d->d_un.d_ptr);
LOGD("DT_SYMTAB: raw_ptr=0x%llx, final_addr=%p",
(unsigned long long)d->d_un.d_ptr, si->symtab);
break;
case DT_STRTAB:
si->strtab = reinterpret_cast<const char*>(si->load_bias + d->d_un.d_ptr);
LOGD("DT_STRTAB: raw_ptr=0x%llx, final_addr=%p",
(unsigned long long)d->d_un.d_ptr, si->strtab);
break;
case DT_STRSZ:
LOGD("DT_STRSZ: %lu", (unsigned long)d->d_un.d_val);
break;
case DT_HASH: {
uint32_t* hash = reinterpret_cast<uint32_t*>(si->load_bias + d->d_un.d_ptr);
si->nbucket = hash[0];
si->nchain = hash[1];
si->bucket = hash + 2;
si->chain = si->bucket + si->nbucket;
LOGD("DT_HASH: raw_ptr=0x%llx, nbucket=%zu, nchain=%zu",
(unsigned long long)d->d_un.d_ptr, si->nbucket, si->nchain);
break;
}
case DT_GNU_HASH: {
uint32_t* hash = reinterpret_cast<uint32_t*>(si->load_bias + d->d_un.d_ptr);
si->gnu_nbucket = hash[0];
uint32_t symbias = hash[1];
si->gnu_maskwords = hash[2];
si->gnu_shift2 = hash[3];
si->gnu_bloom_filter = reinterpret_cast<ElfW(Addr)*>(hash + 4);
si->gnu_bucket = reinterpret_cast<uint32_t*>(si->gnu_bloom_filter + si->gnu_maskwords);
si->gnu_chain = si->gnu_bucket + si->gnu_nbucket - symbias;
LOGD("DT_GNU_HASH: raw_ptr=0x%llx, nbucket=%zu, symbias=%u",
(unsigned long long)d->d_un.d_ptr, si->gnu_nbucket, symbias);
break;
}
case DT_JMPREL:
si->plt_rela = reinterpret_cast<ElfW(Rela)*>(si->load_bias + d->d_un.d_ptr);
LOGD("DT_JMPREL: raw_ptr=0x%llx, final_addr=%p",
(unsigned long long)d->d_un.d_ptr, si->plt_rela);
break;
case DT_PLTRELSZ:
si->plt_rela_count = d->d_un.d_val / sizeof(ElfW(Rela));
LOGD("DT_PLTRELSZ: raw_val=%lu, count=%zu",
(unsigned long)d->d_un.d_val, si->plt_rela_count);
break;
case DT_PLTREL:
LOGD("DT_PLTREL: %lu", (unsigned long)d->d_un.d_val);
break;
case DT_RELA:
si->rela = reinterpret_cast<ElfW(Rela)*>(si->load_bias + d->d_un.d_ptr);
LOGD("DT_RELA: raw_ptr=0x%llx, final_addr=%p",
(unsigned long long)d->d_un.d_ptr, si->rela);
break;
case DT_RELASZ:
si->rela_count = d->d_un.d_val / sizeof(ElfW(Rela));
LOGD("DT_RELASZ: raw_val=%lu, count=%zu",
(unsigned long)d->d_un.d_val, si->rela_count);
break;
case DT_RELAENT:
LOGD("DT_RELAENT: %lu", (unsigned long)d->d_un.d_val);
break;
case DT_INIT:
si->init_func = reinterpret_cast<void (*)()>(si->load_bias + d->d_un.d_ptr);
LOGD("DT_INIT: raw_ptr=0x%llx, final_addr=%p",
(unsigned long long)d->d_un.d_ptr, si->init_func);
break;
case DT_INIT_ARRAY:
si->init_array = reinterpret_cast<void (**)()>(si->load_bias + d->d_un.d_ptr);
LOGD("DT_INIT_ARRAY: raw_ptr=0x%llx, final_addr=%p",
(unsigned long long)d->d_un.d_ptr, si->init_array);
break;
case DT_INIT_ARRAYSZ:
si->init_array_count = d->d_un.d_val / sizeof(void*);
LOGD("DT_INIT_ARRAYSZ: raw_val=%lu, count=%zu",
(unsigned long)d->d_un.d_val, si->init_array_count);
break;
case DT_FINI:
LOGD("DT_FINI: 0x%llx", (unsigned long long)d->d_un.d_ptr);
break;
case DT_FINI_ARRAY:
si->fini_array = reinterpret_cast<void (**)()>(si->load_bias + d->d_un.d_ptr);
LOGD("DT_FINI_ARRAY: raw_ptr=0x%llx, final_addr=%p",
(unsigned long long)d->d_un.d_ptr, si->fini_array);
break;
case DT_FINI_ARRAYSZ:
si->fini_array_count = d->d_un.d_val / sizeof(void*);
LOGD("DT_FINI_ARRAYSZ: raw_val=%lu, count=%zu",
(unsigned long)d->d_un.d_val, si->fini_array_count);
break;
case DT_FLAGS:
si->flags = d->d_un.d_val;
LOGD("DT_FLAGS: 0x%x", si->flags);
break;
case DT_FLAGS_1:
LOGD("DT_FLAGS_1: 0x%llx", (unsigned long long)d->d_un.d_val);
break;
case DT_SONAME:
LOGD("DT_SONAME: offset=%lu", (unsigned long)d->d_un.d_val);
break;
case DT_RUNPATH:
LOGD("DT_RUNPATH: offset=%lu", (unsigned long)d->d_un.d_val);
break;
case DT_NEEDED:
// 跳过,稍后处理
LOGD("DT_NEEDED: offset=%lu (deferred)", (unsigned long)d->d_un.d_val);
break;
default:
LOGD("Unknown dynamic tag: 0x%llx, value=0x%llx",
(unsigned long long)d->d_tag, (unsigned long long)d->d_un.d_val);
break;
}
// 添加安全检查,防止无限循环
if (dyn_count > si->dynamic_count) {
LOGE("Dynamic parsing exceeded expected count");
break;
}
}
if (si->symtab == nullptr) {
LOGD("Warning: DT_SYMTAB not found or is null");
}
if (si->strtab == nullptr) {
LOGD("Warning: DT_STRTAB not found or is null");
}
if (si->strtab != nullptr) {
dyn_count = 0;
for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL && dyn_count < si->dynamic_count; ++d, ++dyn_count) {
if (d->d_tag == DT_NEEDED) {
if (d->d_un.d_val < 65536) {
const char* needed = si->strtab + d->d_un.d_val;
if (strlen(needed) > 0 && strlen(needed) < 256) {
si->needed_libs.push_back(needed);
LOGD("DT_NEEDED: %s", needed);
} else {
LOGD("DT_NEEDED: invalid string at offset %lu", (unsigned long)d->d_un.d_val);
}
} else {
LOGD("DT_NEEDED: offset too large: %lu", (unsigned long)d->d_un.d_val);
}
}
}
}
LOGD("Dynamic parsing complete: symtab=%p, strtab=%p, needed_libs=%zu",
si->symtab, si->strtab, si->needed_libs.size());
return true;
}
void SoinfoManager::ApplyRelaSections(soinfo* si) {
LOGD("RELA sections: rela_count=%zu, plt_rela_count=%zu",
si->rela_count, si->plt_rela_count);
}

View File

@@ -0,0 +1,157 @@
#include "utils.h"
namespace Utils {
bool safe_add(off64_t* out, off64_t a, size_t b) {
if (a < 0 || __builtin_add_overflow(a, b, out)) {
return false;
}
return true;
}
soinfo* get_soinfo(const char* so_name) {
typedef soinfo* (*FunctionPtr)(ElfW(Addr));
char line[1024];
ElfW(Addr) linker_base = 0;
ElfW(Addr) so_addr = 0;
FILE* fp = fopen("/proc/self/maps", "r");
if (!fp) {
LOGE("Cannot open /proc/self/maps");
return nullptr;
}
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "linker64") && !linker_base) {
char* addr = strtok(line, "-");
linker_base = strtoull(addr, nullptr, 16);
} else if (strstr(line, so_name) && !so_addr) {
char* addr = strtok(line, "-");
so_addr = strtoull(addr, nullptr, 16);
}
if (linker_base && so_addr) {
break;
}
}
fclose(fp);
if (!linker_base || !so_addr) {
LOGE("Cannot find addresses");
return nullptr;
}
ElfW(Addr) func_offset = get_export_func("/system/bin/linker64", "find_containing_library");
if (!func_offset) {
LOGE("Cannot find find_containing_library");
return nullptr;
}
ElfW(Addr) find_containing_library_addr = linker_base + func_offset;
FunctionPtr find_containing_library = reinterpret_cast<FunctionPtr>(find_containing_library_addr);
return find_containing_library(so_addr);
}
void* getMapData(int fd, off64_t base_offset, size_t elf_offset, size_t size) {
off64_t offset;
if (!safe_add(&offset, base_offset, elf_offset)) {
return nullptr;
}
off64_t page_min = page_start(offset);
off64_t end_offset;
if (!safe_add(&end_offset, offset, size)) {
return nullptr;
}
if (!safe_add(&end_offset, end_offset, page_offset(offset))) {
return nullptr;
}
size_t map_size = static_cast<size_t>(end_offset - page_min);
uint8_t* map_start = static_cast<uint8_t*>(
mmap(nullptr, map_size, PROT_READ, MAP_PRIVATE, fd, page_min));
if (map_start == MAP_FAILED) {
return nullptr;
}
return map_start + page_offset(offset);
}
ElfW(Addr) get_export_func(const char* path, const char* func_name) {
struct stat sb;
int fd = open(path, O_RDONLY);
if (fd < 0) {
return 0;
}
if (fstat(fd, &sb) < 0) {
close(fd);
return 0;
}
void* base = mmap(nullptr, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (base == MAP_FAILED) {
close(fd);
return 0;
}
ElfW(Ehdr) header;
memcpy(&header, base, sizeof(header));
size_t shdr_size = header.e_shnum * sizeof(ElfW(Shdr));
ElfW(Shdr)* shdr_table = static_cast<ElfW(Shdr)*>(malloc(shdr_size));
memcpy(shdr_table, static_cast<char*>(base) + header.e_shoff, shdr_size);
char* shstrtab = static_cast<char*>(base) + shdr_table[header.e_shstrndx].sh_offset;
void* symtab = nullptr;
char* strtab = nullptr;
uint32_t symtab_size = 0;
for (size_t i = 0; i < header.e_shnum; ++i) {
const ElfW(Shdr)* shdr = &shdr_table[i];
char* section_name = shstrtab + shdr->sh_name;
if (strcmp(section_name, ".symtab") == 0) {
symtab = static_cast<char*>(base) + shdr->sh_offset;
symtab_size = shdr->sh_size;
}
if (strcmp(section_name, ".strtab") == 0) {
strtab = static_cast<char*>(base) + shdr->sh_offset;
}
if (strtab && symtab) {
break;
}
}
ElfW(Addr) result = 0;
if (symtab && strtab) {
ElfW(Sym)* sym_table = static_cast<ElfW(Sym)*>(symtab);
int sym_num = symtab_size / sizeof(ElfW(Sym));
for (int i = 0; i < sym_num; i++) {
const ElfW(Sym)* sym = &sym_table[i];
char* sym_name = strtab + sym->st_name;
if (strstr(sym_name, func_name)) {
result = sym->st_value;
break;
}
}
}
free(shdr_table);
munmap(base, sb.st_size);
close(fd);
return result;
}
}

4
scripts/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Temporary config files
.tmp/
*.pyc
__pycache__/

246
scripts/README.md Normal file
View File

@@ -0,0 +1,246 @@
# Auto Config Tool for Zygisk-MyInjector
自动配置工具,通过交互式命令行快速配置应用注入。
## 功能特性
- 🎯 交互式设备选择(支持多设备)
- 🔍 Tab 自动补全选择应用包名(支持模糊搜索)
- ⚙️ 交互式 Gadget 配置Server/Script 模式)
- 📦 自动下载 Frida Gadget 16.0.7(自动检测设备架构)
- 📦 自动生成配置文件
- 🚀 一键推送并应用配置
## 安装依赖
```bash
pip install prompt_toolkit
```
## 使用方法
### 基本用法
```bash
./auto_config.py
```
### 工作流程
1. **设备选择**
- 自动检测连接的设备
- 单设备时自动选择
- 多设备时交互式选择
2. **应用选择**
- 可选是否包含系统应用(默认仅第三方应用)
- 使用 Tab 键自动补全包名
- 支持模糊搜索过滤
3. **Gadget 配置**
- 指定 Gadget 名称(默认 libgadget.so
- **Server 模式**:监听端口等待 Frida 连接
- 监听地址(默认 0.0.0.0
- 监听端口(默认 27042
- 端口冲突处理fail/ignore/close默认 fail
- 加载行为resume/wait默认 resume
- **Script 模式**:执行本地脚本
- 脚本路径(默认 /data/local/tmp/script.js
4. **Frida Gadget 检查与安装**
- 自动检查设备上是否已安装 Gadget
- 如未安装,自动下载 Frida Gadget 16.0.7
- 根据设备架构选择正确版本arm64/arm/x86/x86_64
- 自动解压并安装到模块 SO 库
5. **配置部署**
- 自动生成 config.json 和 gadget 配置文件
- 推送到设备 /data/local/tmp
- 发送广播触发应用配置
## 示例会话
```
============================================================
Zygisk-MyInjector Auto Config Tool
============================================================
Using device: 192.168.1.100:5555 (OnePlus)
=== Loading app packages ===
Include system apps? (y/N): n
Found 156 packages
=== Select Target App ===
Tip: Use Tab for auto-completion, type to filter
Package name: com.example.app
Selected: com.example.app
=== Gadget Configuration ===
Gadget SO name (default: libgadget.so):
libgadget.so
Select mode:
1. Server mode (listen for connections)
2. Script mode (execute script)
Mode (1/2, default: 1): 1
Listen address (default: 0.0.0.0):
Listen port (default: 27042):
Port conflict behavior:
1. fail - Exit if port is in use
2. ignore - Continue anyway
3. close - Close existing connection
On port conflict (1/2/3, default: 1): 1
On load behavior:
1. resume - Continue immediately (recommended)
2. wait - Wait for connection (for debugging)
On load (1/2, default: 1): 1
=== Generating Configuration Files ===
...
✓ Configuration applied successfully!
The app 'com.example.app' has been configured.
You can now use Frida to connect to the app:
frida -H 0.0.0.0:27042 -n <process-name>
```
## 生成的文件
脚本会生成以下文件:
1. **config.json**
- 模块主配置文件
- 存储位置:`/data/adb/modules/zygisk-myinjector/config.json`
2. **gadget 配置文件**
- 格式:`libgadget.config.so`
- 存储位置:`/data/data/<package>/files/libgadget.config.so`
## 广播接收器
配置通过广播接收器应用:
**注意**ConfigApplyReceiver 现在使用**动态注册**方式,第三方 app 无法通过 PackageManager 发现其存在。
同时增加了 UID 权限检查,只允许 shell (2000) 或 root (0) 发送广播。
```bash
# 手动发送广播示例
adb shell am broadcast \
-n com.jiqiu.configapp/.ConfigApplyReceiver \
-a com.jiqiu.configapp.APPLY_CONFIG \
--es package_name "com.example.app" \
--es tmp_config_path "/data/local/tmp/zygisk_config.json" \
--es tmp_gadget_config_path "/data/local/tmp/libgadget.config.so"
```
## 调试
查看日志:
```bash
adb logcat -s ConfigApplyReceiver:* ConfigManager:*
```
## 故障排除
### prompt_toolkit 未安装
```
Error: prompt_toolkit is required. Install it with:
pip install prompt_toolkit
```
**解决方案**:运行 `pip install prompt_toolkit`
### 没有设备连接
```
Error: No devices found. Please connect a device and enable USB debugging.
```
**解决方案**
1. 通过 USB 或 WiFi 连接设备
2. 确保已启用 USB 调试
3. 运行 `adb devices` 确认设备已连接
### 广播发送失败
**解决方案**
1. 确保 configapp 已安装
2. 确保设备已 root
3. 检查 logcat 日志获取详细错误信息
## 高级用法
### 仅生成配置不部署
修改广播命令添加 `deploy_only` 参数:
```bash
adb shell am broadcast \
-n com.jiqiu.configapp/.ConfigApplyReceiver \
-a com.jiqiu.configapp.APPLY_CONFIG \
--es package_name "com.example.app" \
--es tmp_config_path "/data/local/tmp/zygisk_config.json" \
--ez deploy_only true
```
## 注意事项
1. ⚠️ 设备必须已 root
2. ⚠️ configapp 必须已安装
3.**SELinux 会自动检查并设置为 Permissive 模式**(脚本会自动处理)
4. ⚠️ 首次使用会自动下载 Frida Gadget 16.0.7(需要网络连接)
5. ⚠️ 配置完成后需要重启目标应用才能生效
6. ⚠️ 需要安装 `xz` 工具用于解压macOS 通过 `brew install xz` 安装)
### SELinux 问题
Zygisk 模块需要读取 `/data/adb/modules/zygisk-myinjector/config.json`,但 SELinux 默认会阻止 zygote 进程访问。
**自动处理**
`auto_config.py` 脚本会自动检查 SELinux 状态,如果处于 Enforcing 模式会提示设置为 Permissive。
**手动设置**(重启后需重新设置):
```bash
adb shell "su -c 'setenforce 0'"
```
**永久解决方案**(需要 Magisk 模块):
创建 SELinux 策略或修改模块实现方式。
## 完整工作流程
```bash
# 1. 运行自动配置脚本(会自动检查并设置 SELinux
cd scripts
./auto_config.py
# 2. 按提示选择设备、应用和配置(全部使用默认值即可)
# 脚本会自动完成:
# - 生成配置文件
# - 推送到设备
# - 应用配置
# - 重启应用
# - 端口转发
# - 快速测试
# 3. 如果测试成功,直接使用 Frida 连接
frida -H 127.0.0.1:27042 Gadget -l your_script.js
```
### 快速测试示例
```bash
# 测试连接
frida -H 127.0.0.1:27042 Gadget -e "console.log('Connected! PID:', Process.id)"
# 列举模块
frida -H 127.0.0.1:27042 Gadget -e "Process.enumerateModules().forEach(m => console.log(m.name))"
```

841
scripts/auto_config.py Executable file
View File

@@ -0,0 +1,841 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Auto Config Script for Zygisk-MyInjector
通过交互式命令行快速配置应用注入
完整工作流程:
1. 运行自动配置脚本(会自动检查并设置 SELinux
cd scripts
./auto_config.py
2. 按提示选择设备、应用和配置(全部使用默认值即可)
脚本会自动完成:
- 生成配置文件
- 推送到设备
- 应用配置
- 重启应用
- 端口转发
- 快速测试
3. 如果测试成功,直接使用 Frida 连接
frida -H 127.0.0.1:27042 Gadget -l your_script.js
快速测试:
# 测试连接
frida -H 127.0.0.1:27042 Gadget -e "console.log('Connected! PID:', Process.id)"
# 列举模块
frida -H 127.0.0.1:27042 Gadget -e "Process.enumerateModules().forEach(m => console.log(m.name))"
"""
import subprocess
import json
import sys
import os
import tempfile
import shutil
import argparse
from typing import List, Dict, Optional
from pathlib import Path
try:
from prompt_toolkit import prompt
from prompt_toolkit.completion import FuzzyWordCompleter
from prompt_toolkit.styles import Style
except ImportError:
print("Error: prompt_toolkit is required. Install it with:")
print(" pip install prompt_toolkit")
sys.exit(1)
# Define style for prompts
style = Style.from_dict({
'prompt': 'ansicyan bold',
})
# Frida Gadget version
FRIDA_VERSION = "16.0.7"
MODULE_PATH = "/data/adb/modules/zygisk-myinjector"
SO_STORAGE_DIR = f"{MODULE_PATH}/so_files"
# Default ports
DEFAULT_PORTS = [27042, 65320]
# Local cache directory for downloaded gadgets
SCRIPT_DIR = Path(__file__).parent
CACHE_DIR = SCRIPT_DIR / '.cache' / 'frida-gadgets'
class ADBHelper:
"""ADB helper class for device and package operations"""
def __init__(self, device_id: Optional[str] = None):
self.device_id = device_id
self.base_cmd = ['adb']
if device_id:
self.base_cmd.extend(['-s', device_id])
def run(self, args: List[str], check=True) -> subprocess.CompletedProcess:
"""Run adb command"""
cmd = self.base_cmd + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=check)
return result
except subprocess.CalledProcessError as e:
print(f"Error running command: {' '.join(cmd)}")
print(f"Error: {e.stderr}")
if check:
raise
return e
@staticmethod
def get_devices() -> List[Dict[str, str]]:
"""Get list of connected devices"""
result = subprocess.run(['adb', 'devices', '-l'], capture_output=True, text=True)
lines = result.stdout.strip().split('\n')[1:] # Skip header
devices = []
for line in lines:
if not line.strip():
continue
parts = line.split()
if len(parts) >= 2:
device_id = parts[0]
status = parts[1]
# Parse device info
model = ''
product = ''
for part in parts[2:]:
if part.startswith('model:'):
model = part.split(':', 1)[1]
elif part.startswith('product:'):
product = part.split(':', 1)[1]
devices.append({
'id': device_id,
'status': status,
'model': model,
'product': product
})
return devices
def get_packages(self, include_system: bool = False) -> List[str]:
"""Get list of installed packages"""
args = ['shell', 'pm', 'list', 'packages']
if not include_system:
args.append('-3') # Third-party apps only
result = self.run(args)
packages = []
for line in result.stdout.strip().split('\n'):
if line.startswith('package:'):
pkg = line.split(':', 1)[1].strip()
packages.append(pkg)
return sorted(packages)
def push_file(self, local_path: str, remote_path: str) -> bool:
"""Push file to device"""
result = self.run(['push', local_path, remote_path], check=False)
return result.returncode == 0
def send_broadcast(self, action: str, component: str, extras: Dict[str, str]) -> bool:
"""Send broadcast with extras"""
args = ['shell', 'am', 'broadcast', '-n', component, '-a', action]
for key, value in extras.items():
args.extend(['--es', key, value])
result = self.run(args, check=False)
if result.returncode == 0:
print(f"Broadcast sent successfully")
print(result.stdout)
return True
else:
print(f"Failed to send broadcast")
print(result.stderr)
return False
def get_arch(self) -> str:
"""Get device CPU architecture"""
result = self.run(['shell', 'getprop', 'ro.product.cpu.abi'])
arch = result.stdout.strip()
return arch
def file_exists(self, path: str) -> bool:
"""Check if file exists on device"""
result = self.run(['shell', f'su -c "test -f {path} && echo exists"'], check=False)
return 'exists' in result.stdout
def select_device() -> Optional[str]:
"""Interactive device selection"""
devices = ADBHelper.get_devices()
if not devices:
print("Error: No devices found. Please connect a device and enable USB debugging.")
return None
if len(devices) == 1:
device = devices[0]
print(f"Using device: {device['id']} ({device['model'] or device['product']})")
return device['id']
print("\n=== Connected Devices ===")
for idx, device in enumerate(devices, 1):
model_info = device['model'] or device['product'] or 'Unknown'
print(f"{idx}. {device['id']} - {model_info} [{device['status']}]")
while True:
try:
choice = input(f"\nSelect device (1-{len(devices)}): ").strip()
idx = int(choice) - 1
if 0 <= idx < len(devices):
selected = devices[idx]
print(f"Selected: {selected['id']} ({selected['model'] or selected['product']})")
return selected['id']
else:
print(f"Invalid choice. Please enter 1-{len(devices)}")
except (ValueError, KeyboardInterrupt):
print("\nDevice selection cancelled")
return None
def select_package(adb: ADBHelper) -> Optional[str]:
"""Interactive package selection with fuzzy completion"""
print("\n=== Loading app packages ===")
# Ask if include system apps
while True:
choice = input("Include system apps? (y/N): ").strip().lower()
if choice in ['', 'n', 'no']:
include_system = False
break
elif choice in ['y', 'yes']:
include_system = True
break
else:
print("Please enter 'y' or 'n'")
packages = adb.get_packages(include_system=include_system)
if not packages:
print("Error: No packages found")
return None
print(f"Found {len(packages)} packages")
# Create fuzzy completer
completer = FuzzyWordCompleter(packages)
print("\n=== Select Target App ===")
print("Tip: Use Tab for auto-completion, type to filter")
try:
package = prompt(
'Package name: ',
completer=completer,
style=style
).strip()
if package and package in packages:
print(f"Selected: {package}")
return package
elif package:
print(f"Warning: '{package}' not found in package list, using anyway")
return package
else:
print("No package selected")
return None
except KeyboardInterrupt:
print("\nSelection cancelled")
return None
def configure_gadget(preset_port: Optional[int] = None) -> Optional[Dict]:
"""Interactive gadget configuration"""
print("\n=== Gadget Configuration ===")
gadget_config = {}
# Gadget name
gadget_name = input("Gadget SO name (default: libgadget.so): ").strip()
gadget_config['gadgetName'] = gadget_name or 'libgadget.so'
# Mode selection
print("\nSelect mode:")
print("1. Server mode (listen for connections)")
print("2. Script mode (execute script)")
while True:
choice = input("Mode (1/2, default: 1): ").strip()
if choice in ['', '1']:
gadget_config['mode'] = 'server'
break
elif choice == '2':
gadget_config['mode'] = 'script'
break
else:
print("Invalid choice. Please enter 1 or 2")
if gadget_config['mode'] == 'server':
# Server mode configuration
address = input("Listen address (default: 0.0.0.0): ").strip()
gadget_config['address'] = address or '0.0.0.0'
# Use preset port if provided
if preset_port:
gadget_config['port'] = preset_port
print(f"\nUsing preset port: {preset_port}")
else:
print(f"\nAvailable ports: {', '.join(map(str, DEFAULT_PORTS))}")
port = input(f"Listen port (default: {DEFAULT_PORTS[0]}): ").strip()
try:
gadget_config['port'] = int(port) if port else DEFAULT_PORTS[0]
except ValueError:
print(f"Invalid port, using default {DEFAULT_PORTS[0]}")
gadget_config['port'] = DEFAULT_PORTS[0]
print("\nPort conflict behavior:")
print("1. fail - Exit if port is in use")
print("2. ignore - Continue anyway")
print("3. close - Close existing connection")
conflict = input("On port conflict (1/2/3, default: 1): ").strip()
conflict_map = {'1': 'fail', '2': 'ignore', '3': 'close', '': 'fail'}
gadget_config['onPortConflict'] = conflict_map.get(conflict, 'fail')
print("\nOn load behavior:")
print("1. resume - Continue immediately (recommended)")
print("2. wait - Wait for connection (for debugging)")
load = input("On load (1/2, default: 1): ").strip()
load_map = {'1': 'resume', '2': 'wait', '': 'resume'}
gadget_config['onLoad'] = load_map.get(load, 'resume')
else:
# Script mode configuration
script_path = input("Script path (default: /data/local/tmp/script.js): ").strip()
gadget_config['scriptPath'] = script_path or '/data/local/tmp/script.js'
return gadget_config
def download_frida_gadget(arch: str) -> Optional[str]:
"""Download Frida Gadget for specified architecture (with local caching)"""
# Map Android arch to Frida naming
arch_map = {
'arm64-v8a': 'arm64',
'armeabi-v7a': 'arm',
'x86': 'x86',
'x86_64': 'x86_64'
}
frida_arch = arch_map.get(arch)
if not frida_arch:
print(f"Unsupported architecture: {arch}")
return None
# Check local cache first
CACHE_DIR.mkdir(parents=True, exist_ok=True)
cached_file = CACHE_DIR / f"frida-gadget-{FRIDA_VERSION}-android-{frida_arch}.so"
if cached_file.exists():
print(f"\n✓ Using cached Frida Gadget {FRIDA_VERSION} for {arch}")
print(f" Cache: {cached_file}")
# Copy to temp location for consistent behavior
temp_dir = tempfile.mkdtemp(prefix='frida_gadget_')
temp_file = os.path.join(temp_dir, 'libgadget.so')
shutil.copy2(str(cached_file), temp_file)
return temp_file
# Download if not in cache
url = f"https://github.com/frida/frida/releases/download/{FRIDA_VERSION}/frida-gadget-{FRIDA_VERSION}-android-{frida_arch}.so.xz"
print(f"\nDownloading Frida Gadget {FRIDA_VERSION} for {arch}...")
print(f"URL: {url}")
# Create temp directory
temp_dir = tempfile.mkdtemp(prefix='frida_gadget_')
xz_file = os.path.join(temp_dir, f'frida-gadget.so.xz')
so_file = os.path.join(temp_dir, 'libgadget.so')
try:
# Download
result = subprocess.run(['curl', '-L', '-o', xz_file, url],
capture_output=True, text=True, check=False)
if result.returncode != 0:
print(f"Failed to download: {result.stderr}")
shutil.rmtree(temp_dir)
return None
print("✓ Downloaded")
# Decompress
print("Decompressing...")
result = subprocess.run(['xz', '-d', xz_file],
capture_output=True, text=True, check=False)
if result.returncode != 0:
print(f"Failed to decompress: {result.stderr}")
shutil.rmtree(temp_dir)
return None
# Rename
decompressed = xz_file.replace('.xz', '')
os.rename(decompressed, so_file)
print("✓ Decompressed")
# Save to cache for future use
try:
shutil.copy2(so_file, str(cached_file))
print(f"✓ Cached for future use: {cached_file}")
except Exception as e:
print(f"Warning: Failed to cache gadget: {e}")
return so_file
except Exception as e:
print(f"Error downloading Frida Gadget: {e}")
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
return None
def ensure_gadget_installed(adb: ADBHelper, gadget_name: str = 'libgadget.so') -> bool:
"""Ensure Frida Gadget is installed on device"""
gadget_path = f"{SO_STORAGE_DIR}/{gadget_name}"
print(f"\n=== Checking Frida Gadget ===")
# Check if gadget already exists
if adb.file_exists(gadget_path):
print(f"✓ Gadget found: {gadget_path}")
return True
print(f"Gadget not found in: {gadget_path}")
print("Need to download and install Frida Gadget")
# Get device architecture
arch = adb.get_arch()
print(f"Device architecture: {arch}")
# Download gadget
local_gadget = download_frida_gadget(arch)
if not local_gadget:
print("Failed to download Frida Gadget")
return False
try:
# Push to device temp location
print("\nPushing to device...")
if not adb.push_file(local_gadget, '/data/local/tmp/libgadget.so'):
print("Failed to push gadget to device")
return False
print("✓ Pushed to device")
# Copy to SO storage with root
print(f"Installing to {gadget_path}...")
result = adb.run(['shell', f'su -c "mkdir -p {SO_STORAGE_DIR}"'], check=False)
result = adb.run(['shell',
f'su -c "cp /data/local/tmp/libgadget.so {gadget_path}"'],
check=False)
if result.returncode != 0:
print(f"Failed to install gadget: {result.stderr}")
return False
# Set permissions
adb.run(['shell', f'su -c "chmod 755 {gadget_path}"'], check=False)
# Verify
if adb.file_exists(gadget_path):
print(f"✓ Gadget installed successfully: {gadget_path}")
# Clean up temp file on device
adb.run(['shell', 'rm -f /data/local/tmp/libgadget.so'], check=False)
return True
else:
print("Failed to verify gadget installation")
return False
finally:
# Clean up local temp file
temp_dir = os.path.dirname(local_gadget)
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
def generate_config_files(package_name: str, gadget_config: Dict) -> tuple:
"""Generate config.json and gadget config content"""
# Prepare SO file reference for gadget
gadget_name = gadget_config['gadgetName']
gadget_so_ref = {
"name": gadget_name,
"storedPath": f"{SO_STORAGE_DIR}/{gadget_name}",
"originalPath": f"{SO_STORAGE_DIR}/{gadget_name}"
}
# Generate main module config.json
module_config = {
"enabled": True,
"hideInjection": False,
"injectionDelay": 2,
"globalSoFiles": [],
"perAppConfig": {
package_name: {
"enabled": True,
"soFiles": [gadget_so_ref], # Include gadget SO file
"injectionMethod": "standard",
"gadgetConfig": gadget_config,
"useGlobalGadget": False
}
},
"globalGadgetConfig": None
}
# Generate gadget config content based on mode
if gadget_config['mode'] == 'server':
gadget_config_content = {
"interaction": {
"type": "listen",
"address": gadget_config['address'],
"port": gadget_config['port'],
"on_port_conflict": gadget_config['onPortConflict'],
"on_load": gadget_config['onLoad']
}
}
else: # script mode
gadget_config_content = {
"interaction": {
"type": "script",
"path": gadget_config['scriptPath']
}
}
return (
json.dumps(module_config, indent=2, ensure_ascii=False),
json.dumps(gadget_config_content, indent=2, ensure_ascii=False)
)
def quick_test(adb: ADBHelper, port: int = 27042):
"""Quick test connectivity after configuration"""
print("\n=== Quick Test ===")
print("Testing Frida connectivity...\n")
# Check if frida is installed
result = subprocess.run(['which', 'frida'], capture_output=True, text=True)
if result.returncode != 0:
print("⚠️ Frida CLI not found. Please install it with:")
print(" pip install frida-tools")
return False
# Test 1: Basic connection test
print("Test 1: Basic connection...")
# 使用 stdin 输入命令和 exit避免交互式 REPL 导致超时
test_input = "console.log('Connected! PID:', Process.id)\nexit\n"
test_cmd = ['frida', '-H', f'127.0.0.1:{port}', 'Gadget']
result = subprocess.run(test_cmd, input=test_input, capture_output=True, text=True, timeout=10)
if result.returncode == 0 and 'Connected!' in result.stdout:
print("✓ Connection successful!")
# 提取并显示 PID
for line in result.stdout.split('\n'):
if 'Connected! PID:' in line:
print(f" {line.strip()}")
break
else:
print("✗ Connection failed")
print(f" Error: {result.stderr.strip() if result.stderr else 'Unknown error'}")
print("\nTroubleshooting:")
print(" 1. Check if the target app is running")
print(" 2. Verify port forwarding: adb forward tcp:{} tcp:{}".format(port, port))
print(" 3. Check logcat for errors: adb logcat -s Gadget:*")
return False
# Test 2: Enumerate modules
print("\nTest 2: Enumerate loaded modules...")
test_input = "Process.enumerateModules().slice(0, 5).forEach(m => console.log(' -', m.name))\nexit\n"
test_cmd = ['frida', '-H', f'127.0.0.1:{port}', 'Gadget']
result = subprocess.run(test_cmd, input=test_input, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
print("✓ Modules enumerated:")
# 提取并显示模块列表
in_output = False
for line in result.stdout.split('\n'):
if ' -' in line:
in_output = True
print(line)
elif in_output and line.strip() == '':
break
else:
print("✗ Failed to enumerate modules")
print(f" Error: {result.stderr.strip()}")
return True
def setup_port_forward(adb: ADBHelper, port: int) -> bool:
"""Setup ADB port forwarding"""
print(f"\n=== Setting up port forwarding ===")
print(f"Forwarding local port {port} to device port {port}...")
result = adb.run(['forward', f'tcp:{port}', f'tcp:{port}'], check=False)
if result.returncode == 0:
print(f"✓ Port forwarding established: tcp:{port} -> tcp:{port}")
return True
else:
print(f"✗ Failed to setup port forwarding")
print(f" Error: {result.stderr}")
return False
def check_and_set_selinux(adb: ADBHelper) -> bool:
"""Check SELinux status and set to Permissive if needed"""
print("\n=== Checking SELinux Status ===")
# Check current SELinux status
result = adb.run(['shell', 'getenforce'], check=False)
if result.returncode != 0:
print("⚠️ Failed to check SELinux status")
return True # Continue anyway
status = result.stdout.strip()
print(f"Current SELinux mode: {status}")
if status == 'Permissive':
print("✓ SELinux is already in Permissive mode")
return True
elif status == 'Enforcing':
print("\n⚠️ SELinux is in Enforcing mode")
print(" Zygisk modules cannot read config files when SELinux is Enforcing.")
print(" We need to set it to Permissive mode.")
# Ask user for confirmation
while True:
choice = input("\nSet SELinux to Permissive? (Y/n): ").strip().lower()
if choice in ['', 'y', 'yes']:
break
elif choice in ['n', 'no']:
print("\nWarning: Continuing with SELinux Enforcing may cause injection to fail.")
print("You can manually set it later with: adb shell \"su -c 'setenforce 0'\"")
return True
else:
print("Please enter 'y' or 'n'")
# Set SELinux to Permissive
print("\nSetting SELinux to Permissive...")
result = adb.run(['shell', 'su', '-c', 'setenforce 0'], check=False)
if result.returncode == 0:
print("✓ SELinux set to Permissive mode")
print(" Note: This setting will reset after reboot")
return True
else:
print("✗ Failed to set SELinux to Permissive")
print(f" Error: {result.stderr.strip()}")
print("\nPlease manually run: adb shell \"su -c 'setenforce 0'\"")
return False
else:
print(f"Unknown SELinux status: {status}")
return True
def restart_app(adb: ADBHelper, package_name: str):
"""Restart the target application"""
print(f"\n=== Restarting Application ===")
# Force stop
print(f"Stopping {package_name}...")
result = adb.run(['shell', 'am', 'force-stop', package_name], check=False)
if result.returncode == 0:
print("✓ App stopped")
else:
print("⚠️ Failed to stop app")
# Get main activity
print(f"\nGetting launch activity...")
result = adb.run(['shell', 'pm', 'dump', package_name, '|', 'grep', '-A', '1', 'android.intent.action.MAIN'], check=False)
# Try to start the app
print(f"Starting {package_name}...")
result = adb.run(['shell', 'monkey', '-p', package_name, '-c', 'android.intent.category.LAUNCHER', '1'], check=False)
if result.returncode == 0:
print("✓ App started")
print(" Note: The app should now load with Frida Gadget injected")
return True
else:
print("⚠️ Failed to start app automatically")
print(" Please start the app manually from the device")
return False
def main():
"""Main entry point"""
# Parse command line arguments
parser = argparse.ArgumentParser(
description='Zygisk-MyInjector Auto Config Tool',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
'-p', '--port',
type=int,
choices=DEFAULT_PORTS,
help=f'Preset Gadget port (choices: {", ".join(map(str, DEFAULT_PORTS))})'
)
args = parser.parse_args()
print("=" * 60)
print("Zygisk-MyInjector Auto Config Tool")
print("=" * 60)
# Step 1: Select device
device_id = select_device()
if not device_id:
sys.exit(1)
adb = ADBHelper(device_id)
# Step 2: Check and set SELinux
if not check_and_set_selinux(adb):
print("\nError: Failed to configure SELinux")
print("The injection may fail. Please fix SELinux manually and try again.")
sys.exit(1)
# Step 3: Select package
package_name = select_package(adb)
if not package_name:
sys.exit(1)
# Step 4: Configure gadget
gadget_config = configure_gadget(preset_port=args.port)
if not gadget_config:
sys.exit(1)
# Step 5: Ensure Frida Gadget is installed
if not ensure_gadget_installed(adb, gadget_config['gadgetName']):
print("\nError: Failed to install Frida Gadget")
print("Please manually download and install libgadget.so")
sys.exit(1)
# Step 6: Generate config files
print("\n=== Generating Configuration Files ===")
config_json, gadget_config_json = generate_config_files(package_name, gadget_config)
print("\nGenerated config.json:")
print(config_json)
print("\nGenerated gadget config:")
print(gadget_config_json)
# Step 7: Save to temp files
temp_dir = tempfile.mkdtemp(prefix='frida_gadget_config_')
config_file = os.path.join(temp_dir, 'config.json')
gadget_name = gadget_config['gadgetName'].replace('.so', '.config.so')
gadget_config_file = os.path.join(temp_dir, gadget_name)
with open(config_file, 'w', encoding='utf-8') as f:
f.write(config_json)
with open(gadget_config_file, 'w', encoding='utf-8') as f:
f.write(gadget_config_json)
print(f"\nConfig files saved to: {temp_dir}")
# Step 8: Push to device
print("\n=== Pushing Files to Device ===")
remote_config = '/data/local/tmp/zygisk_config.json'
remote_gadget_config = f'/data/local/tmp/{gadget_name}'
if not adb.push_file(config_file, remote_config):
print("Error: Failed to push config.json")
sys.exit(1)
print(f"✓ Pushed config.json -> {remote_config}")
if not adb.push_file(gadget_config_file, remote_gadget_config):
print("Error: Failed to push gadget config")
sys.exit(1)
print(f"✓ Pushed gadget config -> {remote_gadget_config}")
# Step 9: Send broadcast
print("\n=== Sending Broadcast to Apply Config ===")
success = adb.send_broadcast(
action='com.jiqiu.configapp.APPLY_CONFIG',
component='com.jiqiu.configapp/.ConfigApplyReceiver',
extras={
'package_name': package_name,
'tmp_config_path': remote_config,
'tmp_gadget_config_path': remote_gadget_config
}
)
if success:
print("\n✓ Configuration applied successfully!")
print(f"\nThe app '{package_name}' has been configured.")
# 自动完成工作流程
print("\n=== Completing Workflow ===")
# Step 1: Restart app
restart_app(adb, package_name)
# Step 2: Setup port forwarding
port = gadget_config.get('port', 27042)
if setup_port_forward(adb, port):
# Step 3: Quick test
import time
print("\nWaiting 3 seconds for app to initialize...")
time.sleep(3)
try:
test_success = quick_test(adb, port)
if test_success:
# 彩色打印 frida 命令
print("\n" + "="*60)
print("\033[1;32m✓ All tests passed!\033[0m\n")
print("\033[1;36mYou can now use Frida with the following commands:\033[0m\n")
# Interactive mode
print("\033[1;33m# Interactive REPL:\033[0m")
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget\033[0m\n")
# Execute script
print("\033[1;33m# Execute JavaScript code:\033[0m")
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget -e 'YOUR_CODE_HERE'\033[0m\n")
# Load script file
print("\033[1;33m# Load script file:\033[0m")
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget -l your_script.js\033[0m\n")
print("="*60)
except Exception as e:
print(f"\n⚠️ Test failed: {e}")
print("\nYou can manually test with:")
if gadget_config['mode'] == 'server':
print(f"\033[1;32mfrida -H 127.0.0.1:{port} Gadget -l your_script.js\033[0m")
else:
print("\n✗ Failed to apply configuration")
print("Please check logcat for details:")
print(f" adb -s {device_id} logcat -s ConfigApplyReceiver:* ConfigManager:*")
sys.exit(1)
# Clean up temp directory
try:
shutil.rmtree(temp_dir)
except Exception:
pass # Ignore cleanup errors
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\n\nOperation cancelled by user")
sys.exit(130)