Compare commits
15 Commits
config_app
...
v1.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13ee77e96f | ||
|
|
e4ca55d6cd | ||
|
|
f955835df5 | ||
|
|
7e5a96cd78 | ||
|
|
cc4fb60b7b | ||
|
|
0c713a26da | ||
|
|
fee5600129 | ||
|
|
cec1db61fb | ||
|
|
d201c72e22 | ||
|
|
7b7f6de828 | ||
|
|
92a640a3c7 | ||
|
|
680a93cba8 | ||
|
|
da73a3f9bd | ||
|
|
d793712a13 | ||
|
|
aa6709b9d9 |
208
README.md
@@ -1,65 +1,187 @@
|
||||
# [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。
|
||||
|
||||
本项目在原项目基础上做局部更改,请支持原项目作者劳动成果
|
||||
### 版本规划
|
||||
|
||||
1. 安装[Magisk](https://github.com/topjohnwu/Magisk) v24以上版本并开启Zygisk
|
||||
- **v1.x**:专注功能添加,暂不考虑反检测
|
||||
- **v2.x**:实现各种检测绕过,达到100%无痕注入
|
||||
|
||||
2. 生成模块
|
||||
- GitHub Actions
|
||||
1. Fork这个项目
|
||||
2. 在你fork的项目中选择**Actions**选项卡
|
||||
3. 在左边的侧边栏中,单击**Build**
|
||||
4. 选择**Run workflow**
|
||||
5. 输入游戏包名并点击**Run workflow**
|
||||
6. 等待操作完成并下载
|
||||
- Android Studio
|
||||
1. 下载源码
|
||||
2. 编辑`game.h`, 修改`GamePackageName`为游戏包名
|
||||
3. 使用Android Studio运行gradle任务`:module:assembleRelease`编译,zip包会生成在`out`文件夹下
|
||||
## 致谢
|
||||
|
||||
3. 在Magisk里安装模块
|
||||
**项目地址**:[https://github.com/jiqiu2022/Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
|
||||
|
||||
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/
|
||||
|
||||
## 使用教程
|
||||
|
||||
### 步骤一:下载并安装面具模块
|
||||
|
||||
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. 下载模块文件(如下图所示):
|
||||
|
||||

|
||||
|
||||
3. 安装到手机,支持所有面具模块相关的APP(如 KernelSU、APatch 等)
|
||||
|
||||
> **注意**:如果您的APP不支持Zygisk注入,请额外安装 Zygisk-Next 模块,否则可能导致注入失败。
|
||||
|
||||
### 步骤二:准备SO文件并配置
|
||||
|
||||
#### 1. 添加SO文件
|
||||
|
||||
进入"SO文件管理",点击"增加SO文件":
|
||||
|
||||

|
||||
|
||||
选择要注入的SO文件:
|
||||
|
||||

|
||||
|
||||
本教程使用以下两个SO文件进行测试:
|
||||
- `libmylib.so`(全注入测试)
|
||||
- `libgadget.so`(普通注入方式)
|
||||
|
||||
> **提示**:后文将解释为什么自定义linker无法注入 `libgadget.so`。
|
||||
|
||||
#### 2. 处理原文件
|
||||
|
||||
选择SO后,系统会询问是否删除原文件:
|
||||
|
||||

|
||||
|
||||
> **设计理念**:此功能可防止某些APP扫描tmp目录中的SO文件进行风控检测。
|
||||
|
||||
添加成功后,SO管理界面会显示已添加的文件:
|
||||
|
||||

|
||||
|
||||
### 步骤三:配置目标APP
|
||||
|
||||
本教程以珍惜大佬的Hunter为例演示配置过程。
|
||||
|
||||
1. 点击空白处,配置注入参数:
|
||||
|
||||

|
||||
|
||||
2. 保存配置后,开启注入开关:
|
||||
|
||||

|
||||
|
||||
### 步骤四:验证注入效果
|
||||
|
||||
打开目标APP,查看日志输出:
|
||||
|
||||

|
||||
|
||||
成功打印"我已经成功加载",表明注入成功。
|
||||
|
||||
#### 测试Gadget注入
|
||||
|
||||
切换到gadget进行测试:
|
||||
|
||||

|
||||
|
||||
> **特性**:本注入模块支持同时注入多个SO文件。
|
||||
|
||||

|
||||
|
||||
注入成功:
|
||||
|
||||

|
||||
|
||||
> **说明**:由于使用的Hunter版本较旧,可能无法检测到注入。建议使用新版本进行测试。
|
||||
|
||||
#### 测试其他注入方式
|
||||
|
||||
测试Riru Hide功能:
|
||||
|
||||

|
||||
|
||||
> **提示**:修改配置后,建议先关闭再开启注入开关,确保新配置生效。
|
||||
|
||||

|
||||
|
||||
Riru Hide成功生效。
|
||||
|
||||
测试自定义Linker加载:
|
||||
|
||||

|
||||
|
||||
加载成功:
|
||||
|
||||

|
||||
|
||||
## 编译指南
|
||||
|
||||
### 自动编译
|
||||
|
||||
参考项目中的 [CI配置文件](https://github.com/jiqiu2022/Zygisk-MyInjector/blob/main/.github/workflows/ci.yml)。
|
||||
|
||||
如果您fork了本项目,只需在本地修改代码并push,云端将自动编译。
|
||||
|
||||
### 本地编译
|
||||
|
||||
使用项目提供的 `build_all.sh` 脚本进行编译。
|
||||
|
||||
> **环境要求**:需要Java 17环境。
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 注入流程详解
|
||||
|
||||
1. **SO文件管理**
|
||||
- 用户选择SO文件后,APP自动将其复制到 `/data/adb/modules/module-name` 目录下进行统一管理
|
||||
2. **配置生成**
|
||||
- 完成APP配置后,管理器在 `/data/adb/modules/module-name` 下创建config文件
|
||||
- 面具模块启动时读取此配置,执行SO加载
|
||||
3. **注入激活**
|
||||
- 开启注入时:APP将管理器中的SO复制到目标APP目录,并将config状态设置为开启
|
||||
- 关闭注入时:APP自动删除已复制的SO文件,将config状态设置为关闭
|
||||
## 为什么自定义linker不能注入gadget
|
||||
|
||||
```
|
||||
void hack_start(const char *game_data_dir,JavaVM *vm) {
|
||||
load_so(game_data_dir,vm,"test");
|
||||
//如果要注入多个so,那么就在这里不断的添加load_so函数即可
|
||||
}
|
||||
frida_KjnwyG_detect_location: assertion failed: (our_range != null)
|
||||
Bail out! Frida:ERROR:../lib/KjnwyG/KjnwyG.vala:809:frida_KjnwyG_detect_location: assertion failed: (our_range != null)
|
||||
Aborted
|
||||
```
|
||||
|
||||
因为gadget的init_array里面其中有一个函数,尝试使用maps和soinfo获取到自己模块的信息,防止递归调用。
|
||||
|
||||
但是自定义linker的soinfo和内存段都是自己分配的,导致gadget找不到自己,发生了断言失败,我们可以简单的去掉断言,但是可能造成其他bug,后续作者会和小佳一起修复。
|
||||
|
||||
当然还有一种复杂的方法,就是用注入的so的soinfo,替换成要注入的so的,多见于360等壳子,等后续作者会实现。
|
||||
|
||||
|
||||
目前正在开发的分支:
|
||||
|
||||
1. 使用Java的System.load加载so
|
||||
## 后续更新计划
|
||||
|
||||
2. 注入多个so的分支(已完成)
|
||||
|
||||
计划开发:
|
||||
|
||||
1. 第一步,仿照Riru,将注入的so进行内存上的初步隐藏(可以对抗部分业务检测,游戏安全相关已经补齐,建议不要尝试)(已经开发完成)
|
||||
2. 第二步,实现一个自定义的linker,进行更深层次的注入隐藏
|
||||
3. 第三步,搭配对应配套手机的内核模块对注入的模块进行进一步完美擦除,达到完美注入的目的
|
||||
|
||||
以此项目为脚手架的计划开发:
|
||||
|
||||
1. 一个全新的Frida框架,保留大部分原生api,并可以过任何相关注入检测
|
||||
|
||||
2. 一个全新的Trace框架,高性能Trace,速度是Stallker的60倍,并且支持更全面的信息打印。(具体效果可以参考看雪帖子)
|
||||
|
||||
3. 一个全新的无痕调试框架,支持像GDB一样调试,没有ptrace痕迹,两种思路进行无痕调试(基于硬件断点以及基于VM)
|
||||
- **深度隐藏**:联动内核模块,提供maps等更深层次的隐藏功能
|
||||
- **用户体验优化**:
|
||||
- 增加gadget配置一键生成功能
|
||||
- 提供更友好的界面交互
|
||||
- 支持批量配置管理
|
||||
|
||||
---
|
||||
|
||||
欢迎关注项目进展,期待您的贡献!
|
||||
|
||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 584 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 916 KiB |
|
After Width: | Height: | Size: 902 KiB |
|
After Width: | Height: | Size: 832 KiB |
|
After Width: | Height: | Size: 870 KiB |
|
After Width: | Height: | Size: 906 KiB |
|
After Width: | Height: | Size: 926 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,53 @@ 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);
|
||||
CheckBox checkboxEnableGadget = dialogView.findViewById(R.id.checkboxEnableGadget);
|
||||
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
|
||||
ConfigManager.GadgetConfig gadgetConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||
checkboxEnableGadget.setChecked(gadgetConfig != null);
|
||||
btnConfigureGadget.setEnabled(gadgetConfig != null);
|
||||
|
||||
// Setup gadget listeners
|
||||
checkboxEnableGadget.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
btnConfigureGadget.setEnabled(isChecked);
|
||||
if (!isChecked) {
|
||||
// Remove gadget config when unchecked
|
||||
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
|
||||
}
|
||||
});
|
||||
|
||||
btnConfigureGadget.setOnClickListener(v -> {
|
||||
ConfigManager.GadgetConfig currentConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||
if (currentConfig == null) {
|
||||
currentConfig = new ConfigManager.GadgetConfig();
|
||||
}
|
||||
|
||||
GadgetConfigDialog gadgetDialog = GadgetConfigDialog.newInstance(currentConfig);
|
||||
gadgetDialog.setOnGadgetConfigListener(config -> {
|
||||
configManager.setAppGadgetConfig(appInfo.getPackageName(), config);
|
||||
});
|
||||
gadgetDialog.show(getParentFragmentManager(), "gadget_config");
|
||||
});
|
||||
|
||||
// Setup SO list
|
||||
List<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles();
|
||||
@@ -165,8 +205,26 @@ 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 gadget config if enabled
|
||||
if (checkboxEnableGadget.isChecked()) {
|
||||
ConfigManager.GadgetConfig currentGadgetConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||
if (currentGadgetConfig == null) {
|
||||
// Create default config if not already configured
|
||||
currentGadgetConfig = new ConfigManager.GadgetConfig();
|
||||
configManager.setAppGadgetConfig(appInfo.getPackageName(), currentGadgetConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Save SO selection
|
||||
if (soListRecyclerView.getAdapter() != null) {
|
||||
|
||||
@@ -151,9 +151,17 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy SO file to our storage
|
||||
Shell.Result result = Shell.cmd("cp \"" + originalPath + "\" \"" + storedPath + "\"").exec();
|
||||
@@ -227,6 +235,159 @@ 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) {
|
||||
return null;
|
||||
}
|
||||
return appConfig.gadgetConfig;
|
||||
}
|
||||
|
||||
public void setAppGadgetConfig(String packageName, GadgetConfig gadgetConfig) {
|
||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||
if (appConfig == null) {
|
||||
appConfig = new AppConfig();
|
||||
config.perAppConfig.put(packageName, appConfig);
|
||||
}
|
||||
|
||||
// Remove old gadget from SO list if exists
|
||||
if (appConfig.gadgetConfig != null) {
|
||||
String oldGadgetName = appConfig.gadgetConfig.gadgetName;
|
||||
appConfig.soFiles.removeIf(soFile -> soFile.name.equals(oldGadgetName));
|
||||
}
|
||||
|
||||
appConfig.gadgetConfig = gadgetConfig;
|
||||
|
||||
// Add new gadget to SO list if configured
|
||||
if (gadgetConfig != null) {
|
||||
// Check if gadget SO file exists in global storage
|
||||
String gadgetPath = SO_STORAGE_DIR + "/" + gadgetConfig.gadgetName;
|
||||
Shell.Result checkResult = Shell.cmd("test -f \"" + gadgetPath + "\" && echo 'exists'").exec();
|
||||
|
||||
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
|
||||
// Add gadget as a SO file
|
||||
SoFile gadgetSoFile = new SoFile();
|
||||
gadgetSoFile.name = gadgetConfig.gadgetName;
|
||||
gadgetSoFile.storedPath = gadgetPath;
|
||||
gadgetSoFile.originalPath = gadgetPath;
|
||||
|
||||
// Check if already in list
|
||||
boolean alreadyExists = false;
|
||||
for (SoFile soFile : appConfig.soFiles) {
|
||||
if (soFile.name.equals(gadgetSoFile.name)) {
|
||||
alreadyExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyExists) {
|
||||
appConfig.soFiles.add(gadgetSoFile);
|
||||
Log.i(TAG, "Added gadget SO to app's SO list: " + gadgetSoFile.name);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Gadget SO file not found in storage: " + gadgetPath);
|
||||
Log.w(TAG, "Please ensure " + gadgetConfig.gadgetName + " is added to SO library");
|
||||
}
|
||||
}
|
||||
|
||||
saveConfig();
|
||||
|
||||
// If app is enabled, deploy both gadget SO and config file
|
||||
if (appConfig.enabled) {
|
||||
if (gadgetConfig != null) {
|
||||
deployGadgetConfigFile(packageName, gadgetConfig);
|
||||
}
|
||||
// Re-deploy all SO files including gadget
|
||||
deploySoFilesToApp(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
private void deployGadgetConfigFile(String packageName, GadgetConfig gadgetConfig) {
|
||||
try {
|
||||
// Create gadget config JSON
|
||||
String configJson;
|
||||
if ("script".equals(gadgetConfig.mode)) {
|
||||
configJson = String.format(
|
||||
"{\n" +
|
||||
" \"interaction\": {\n" +
|
||||
" \"type\": \"script\",\n" +
|
||||
" \"path\": \"%s\"\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
gadgetConfig.scriptPath
|
||||
);
|
||||
} else {
|
||||
configJson = String.format(
|
||||
"{\n" +
|
||||
" \"interaction\": {\n" +
|
||||
" \"type\": \"listen\",\n" +
|
||||
" \"address\": \"%s\",\n" +
|
||||
" \"port\": %d,\n" +
|
||||
" \"on_port_conflict\": \"%s\",\n" +
|
||||
" \"on_load\": \"%s\"\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
gadgetConfig.address,
|
||||
gadgetConfig.port,
|
||||
gadgetConfig.onPortConflict,
|
||||
gadgetConfig.onLoad
|
||||
);
|
||||
}
|
||||
|
||||
// Write to temp file
|
||||
String tempFile = context.getCacheDir() + "/" + gadgetConfig.gadgetName + ".config";
|
||||
java.io.FileWriter writer = new java.io.FileWriter(tempFile);
|
||||
writer.write(configJson);
|
||||
writer.close();
|
||||
|
||||
// Copy to app's files directory
|
||||
String filesDir = "/data/data/" + packageName + "/files";
|
||||
String gadgetConfigName = gadgetConfig.gadgetName.replace(".so", ".config.so");
|
||||
String targetPath = filesDir + "/" + gadgetConfigName;
|
||||
|
||||
Shell.Result copyResult = Shell.cmd("cp " + tempFile + " " + targetPath).exec();
|
||||
if (copyResult.isSuccess()) {
|
||||
// Set permissions
|
||||
Shell.cmd("chmod 644 " + targetPath).exec();
|
||||
Log.i(TAG, "Deployed gadget config to: " + targetPath);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to deploy gadget config: " + String.join("\n", copyResult.getErr()));
|
||||
}
|
||||
|
||||
// Clean up temp file
|
||||
new java.io.File(tempFile).delete();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to create gadget config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy SO files directly to app's data directory
|
||||
private void deploySoFilesToApp(String packageName) {
|
||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||
@@ -270,9 +431,8 @@ public class ConfigManager {
|
||||
|
||||
// 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();
|
||||
@@ -316,6 +476,11 @@ public class ConfigManager {
|
||||
}
|
||||
|
||||
Log.i(TAG, "Deployment complete for: " + packageName);
|
||||
|
||||
// Deploy gadget config if configured
|
||||
if (appConfig.gadgetConfig != null) {
|
||||
deployGadgetConfigFile(packageName, appConfig.gadgetConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up deployed SO files when app is disabled
|
||||
@@ -336,8 +501,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 +526,23 @@ public class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up gadget config file if exists
|
||||
if (appConfig.gadgetConfig != null) {
|
||||
String gadgetConfigName = appConfig.gadgetConfig.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);
|
||||
}
|
||||
|
||||
@@ -377,6 +559,7 @@ public class ConfigManager {
|
||||
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<>();
|
||||
}
|
||||
@@ -384,6 +567,8 @@ public class ConfigManager {
|
||||
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 static class SoFile {
|
||||
@@ -399,4 +584,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";
|
||||
}
|
||||
}
|
||||
170
configapp/src/main/java/com/jiqiu/configapp/FileUtils.java
Normal file
@@ -0,0 +1,170 @@
|
||||
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);
|
||||
|
||||
// Use root to copy to a more permanent location
|
||||
String targetPath = "/data/local/tmp/" + fileName;
|
||||
Shell.Result result = Shell.cmd(
|
||||
"cp \"" + tempFile.getAbsolutePath() + "\" \"" + targetPath + "\"",
|
||||
"chmod 644 \"" + targetPath + "\""
|
||||
).exec();
|
||||
|
||||
// Clean up temp file
|
||||
tempFile.delete();
|
||||
|
||||
if (result.isSuccess()) {
|
||||
return targetPath;
|
||||
} else {
|
||||
Log.e(TAG, "Failed to copy file to /data/local/tmp/");
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error copying file from content URI", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file name from URI
|
||||
* @param context Context
|
||||
* @param uri URI to get name from
|
||||
* @return File name or null
|
||||
*/
|
||||
private static String getFileName(Context context, Uri uri) {
|
||||
String fileName = null;
|
||||
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
try (Cursor cursor = context.getContentResolver().query(
|
||||
uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (nameIndex >= 0) {
|
||||
fileName = cursor.getString(nameIndex);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error getting file name from URI", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileName == null) {
|
||||
fileName = uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,614 @@
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return new MaterialAlertDialogBuilder(getContext())
|
||||
.setTitle("Gadget 配置")
|
||||
.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();
|
||||
}
|
||||
|
||||
private String getPathFromUri(Uri uri) {
|
||||
String path = null;
|
||||
|
||||
// Try to get path from MediaStore
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
try {
|
||||
ContentResolver resolver = getContext().getContentResolver();
|
||||
try (Cursor cursor = resolver.query(uri, new String[]{"_data"}, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int columnIndex = cursor.getColumnIndex("_data");
|
||||
if (columnIndex != -1) {
|
||||
path = cursor.getString(columnIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Try DocumentsContract if MediaStore fails
|
||||
if (path == null && DocumentsContract.isDocumentUri(getContext(), uri)) {
|
||||
try {
|
||||
String docId = DocumentsContract.getDocumentId(uri);
|
||||
if (uri.getAuthority().equals("com.android.externalstorage.documents")) {
|
||||
String[] split = docId.split(":");
|
||||
if (split.length >= 2) {
|
||||
String type = split[0];
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
path = "/storage/emulated/0/" + split[1];
|
||||
} else {
|
||||
path = "/storage/" + type + "/" + split[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
} else if ("file".equals(uri.getScheme())) {
|
||||
path = uri.getPath();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ 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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -24,6 +27,8 @@ public class SettingsFragment extends Fragment {
|
||||
private RadioGroup radioGroupFilter;
|
||||
private RadioButton radioShowAll;
|
||||
private RadioButton radioHideSystem;
|
||||
private EditText editInjectionDelay;
|
||||
private ConfigManager configManager;
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
private OnSettingsChangeListener settingsChangeListener;
|
||||
@@ -53,6 +58,9 @@ 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);
|
||||
|
||||
configManager = new ConfigManager(getContext());
|
||||
}
|
||||
|
||||
private void initSharedPreferences() {
|
||||
@@ -67,6 +75,10 @@ public class SettingsFragment extends Fragment {
|
||||
} else {
|
||||
radioShowAll.setChecked(true);
|
||||
}
|
||||
|
||||
// Load injection delay
|
||||
int injectionDelay = configManager.getInjectionDelay();
|
||||
editInjectionDelay.setText(String.valueOf(injectionDelay));
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
@@ -86,6 +98,32 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
|
||||
|
||||
@@ -196,6 +196,12 @@ public class SoManagerFragment extends Fragment {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
// Add MIME types that might help filter SO files
|
||||
String[] mimeTypes = {"application/octet-stream", "application/x-sharedlib", "*/*"};
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||
// Suggest starting location
|
||||
intent.putExtra("android.provider.extra.INITIAL_URI",
|
||||
android.net.Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload"));
|
||||
filePickerLauncher.launch(intent);
|
||||
}
|
||||
|
||||
@@ -219,14 +225,12 @@ public class SoManagerFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void handleFileSelection(Uri uri) {
|
||||
// Get real path from URI
|
||||
String path = uri.getPath();
|
||||
// Get real path from URI using proper URI handling
|
||||
String path = FileUtils.getRealPathFromUri(requireContext(), uri);
|
||||
if (path != null) {
|
||||
// Remove the file:// prefix if present
|
||||
if (path.startsWith("file://")) {
|
||||
path = path.substring(7);
|
||||
}
|
||||
showDeleteOriginalDialog(path);
|
||||
} else {
|
||||
Toast.makeText(getContext(), "无法获取文件路径,请尝试其他方式", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<?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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
@@ -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,12 +81,81 @@
|
||||
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="1dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="隐藏注入"
|
||||
android:layout_marginTop="8dp" />
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkboxEnableGadget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="启用 Gadget 配置"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnConfigureGadget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:text="配置"
|
||||
android:textSize="12sp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:enabled="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gadget可用于Frida调试,勾选后可配置监听地址和端口"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="注入方式"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_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"
|
||||
@@ -90,7 +163,25 @@
|
||||
android:text="使用Riru Hide隐藏注入的SO文件"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginStart="56dp"
|
||||
android:layout_marginTop="4dp" />
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
<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>
|
||||
299
configapp/src/main/res/layout/dialog_gadget_config.xml
Normal file
@@ -0,0 +1,299 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gadget 配置"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="提示:请确保对应的 gadget SO 文件已添加到 SO 库中"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- 模式选择 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gadget 模式"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/modeRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioModeServer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Server 模式"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioModeScript"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Script 模式" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<!-- Server 模式配置区域 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/serverModeLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 监听地址配置 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="监听地址"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/addressRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioAddressAll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0.0.0.0 (监听所有接口)"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioAddressLocal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="127.0.0.1 (仅本地)" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioAddressCustom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="自定义" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editCustomAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="输入自定义地址"
|
||||
android:inputType="text"
|
||||
android:enabled="false"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- 端口配置 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="监听端口"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editPort"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="27042"
|
||||
android:hint="1-65535"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- 端口冲突处理 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="端口冲突处理"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/portConflictRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioConflictFail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="fail (启动失败)"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioConflictPickNext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="pick-next (尝试下一个端口)" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<!-- 加载处理方式 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="加载时处理方式"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/onLoadRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioLoadWait"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="wait (等待连接)"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radioLoadResume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="resume (立即继续)" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Script 模式配置区域 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/scriptModeLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Script 文件路径"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editScriptPath"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="/data/local/tmp/script.js"
|
||||
android:inputType="text"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSelectScript"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="选择"
|
||||
android:textSize="12sp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="提示:Script 模式下,Gadget 会在程序入口点执行前自动加载并运行指定的脚本文件"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Gadget 名称 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gadget 文件名"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editGadgetName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="libgadget.so"
|
||||
android:hint="例如: libgadget.so"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- JSON 预览区域 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="配置预览 (可直接编辑)"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardElevation="2dp"
|
||||
app:cardCornerRadius="4dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editJsonPreview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="150dp"
|
||||
android:gravity="top"
|
||||
android:padding="12dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="12sp"
|
||||
android:inputType="textMultiLine"
|
||||
android:scrollbars="vertical"
|
||||
android:overScrollMode="always" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@@ -72,6 +72,77 @@
|
||||
|
||||
</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>
|
||||
|
||||
<!-- 其他设置可以在这里添加 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,115 @@ 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);
|
||||
// Get injection delay from config
|
||||
int delay = Config::getInjectionDelay();
|
||||
LOGI("Waiting %d seconds before injection", delay);
|
||||
|
||||
// Wait for app to initialize and files to be copied
|
||||
sleep(delay);
|
||||
|
||||
// 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);
|
||||
std::thread hack_thread(hack_thread_func, game_data_dir, package_name, vm);
|
||||
hack_thread.detach();
|
||||
}
|
||||
@@ -42,9 +42,15 @@ 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);
|
||||
// Get JavaVM
|
||||
JavaVM *vm = nullptr;
|
||||
if (env->GetJavaVM(&vm) == JNI_OK) {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
module/src/main/cpp/mylinker/CMakeLists.txt
Normal 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 shared library
|
||||
add_library(mylinker SHARED ${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()
|
||||
187
module/src/main/cpp/mylinker/elf_loader.cpp
Normal 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;
|
||||
}
|
||||
149
module/src/main/cpp/mylinker/elf_reader.cpp
Normal 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;
|
||||
}
|
||||
47
module/src/main/cpp/mylinker/include/common.h
Normal 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;
|
||||
27
module/src/main/cpp/mylinker/include/elf_loader.h
Normal 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_;
|
||||
};
|
||||
38
module/src/main/cpp/mylinker/include/elf_reader.h
Normal 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_;
|
||||
};
|
||||
34
module/src/main/cpp/mylinker/include/memory_manager.h
Normal 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_;
|
||||
};
|
||||
17
module/src/main/cpp/mylinker/include/mylinker.h
Normal 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
|
||||
25
module/src/main/cpp/mylinker/include/relocator.h
Normal 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);
|
||||
};
|
||||
75
module/src/main/cpp/mylinker/include/soinfo_manager.h
Normal 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_;
|
||||
};
|
||||
21
module/src/main/cpp/mylinker/include/utils.h
Normal 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);
|
||||
}
|
||||
}
|
||||
49
module/src/main/cpp/mylinker/main.cpp
Normal 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
|
||||
241
module/src/main/cpp/mylinker/memory_manager.cpp
Normal 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;
|
||||
}
|
||||
61
module/src/main/cpp/mylinker/mylinker.cpp
Normal 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");
|
||||
}
|
||||
342
module/src/main/cpp/mylinker/relocator.cpp
Normal 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;
|
||||
}
|
||||
312
module/src/main/cpp/mylinker/soinfo_manager.cpp
Normal 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);
|
||||
}
|
||||
157
module/src/main/cpp/mylinker/utils.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||