diff --git a/build.gradle b/build.gradle index 5e02054..b138350 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ allprojects { repositories { mavenCentral() google() + maven { url 'https://jitpack.io' } } } diff --git a/configapp/build.gradle b/configapp/build.gradle index df3a7bd..19abb87 100644 --- a/configapp/build.gradle +++ b/configapp/build.gradle @@ -23,8 +23,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } @@ -42,6 +42,12 @@ dependencies { // RecyclerView for app list implementation 'androidx.recyclerview:recyclerview:1.3.2' + + // Root access library + implementation 'com.github.topjohnwu.libsu:core:6.0.0' + + // JSON parsing + implementation 'com.google.code.gson:gson:2.10.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' diff --git a/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java b/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java index 7ff0486..f9e5fbe 100644 --- a/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java +++ b/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java @@ -36,6 +36,7 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog private List allApps; private boolean hideSystemApps = false; + private ConfigManager configManager; @Nullable @Override @@ -48,6 +49,8 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + configManager = new ConfigManager(requireContext()); + initViews(view); setupRecyclerView(); setupSearchView(); @@ -102,8 +105,8 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog @Override public void onAppToggle(AppInfo appInfo, boolean isEnabled) { - // 这里可以保存应用的启用状态到配置文件或数据库 - // 暂时只是打印日志 + // 保存应用的启用状态到配置文件 + configManager.setAppEnabled(appInfo.getPackageName(), isEnabled); android.util.Log.d("AppListFragment", "App " + appInfo.getAppName() + " toggle: " + isEnabled); } @@ -133,6 +136,9 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog isSystemApp ); + // 从配置中加载启用状态 + app.setEnabled(configManager.isAppEnabled(packageName)); + apps.add(app); } catch (Exception e) { // 忽略无法获取信息的应用 diff --git a/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java b/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java new file mode 100644 index 0000000..9e2b16a --- /dev/null +++ b/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java @@ -0,0 +1,179 @@ +package com.jiqiu.configapp; + +import android.content.Context; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.topjohnwu.superuser.Shell; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ConfigManager { + private static final String TAG = "ConfigManager"; + public static final String MODULE_PATH = "/data/adb/modules/zygisk-myinjector"; + public static final String CONFIG_FILE = MODULE_PATH + "/config.json"; + public static final String SO_STORAGE_DIR = MODULE_PATH + "/so_files"; + + private final Context context; + private final Gson gson; + private ModuleConfig config; + + static { + // Configure Shell to use root + Shell.enableVerboseLogging = BuildConfig.DEBUG; + Shell.setDefaultBuilder(Shell.Builder.create() + .setFlags(Shell.FLAG_REDIRECT_STDERR) + .setTimeout(10)); + } + + public ConfigManager(Context context) { + this.context = context; + this.gson = new GsonBuilder().setPrettyPrinting().create(); + loadConfig(); + } + + public boolean isRootAvailable() { + return Shell.getShell().isRoot(); + } + + public void ensureModuleDirectories() { + Shell.cmd("mkdir -p " + MODULE_PATH).exec(); + Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec(); + } + + private void loadConfig() { + Shell.Result result = Shell.cmd("cat " + CONFIG_FILE).exec(); + if (result.isSuccess() && !result.getOut().isEmpty()) { + String json = String.join("\n", result.getOut()); + try { + config = gson.fromJson(json, ModuleConfig.class); + } catch (Exception e) { + Log.e(TAG, "Failed to parse config", e); + config = new ModuleConfig(); + } + } else { + config = new ModuleConfig(); + } + } + + public void saveConfig() { + String json = gson.toJson(config); + // Write to temp file first + String tempFile = context.getCacheDir() + "/config.json"; + try { + java.io.FileWriter writer = new java.io.FileWriter(tempFile); + writer.write(json); + writer.close(); + + // Copy to module directory with root + Shell.cmd("cp " + tempFile + " " + CONFIG_FILE).exec(); + Shell.cmd("chmod 644 " + CONFIG_FILE).exec(); + + // Clean up temp file + new File(tempFile).delete(); + } catch (Exception e) { + Log.e(TAG, "Failed to save config", e); + } + } + + public boolean isAppEnabled(String packageName) { + AppConfig appConfig = config.perAppConfig.get(packageName); + return appConfig != null && appConfig.enabled; + } + + public void setAppEnabled(String packageName, boolean enabled) { + AppConfig appConfig = config.perAppConfig.get(packageName); + if (appConfig == null) { + appConfig = new AppConfig(); + config.perAppConfig.put(packageName, appConfig); + } + appConfig.enabled = enabled; + saveConfig(); + } + + public List getAppSoFiles(String packageName) { + AppConfig appConfig = config.perAppConfig.get(packageName); + if (appConfig == null) { + return new ArrayList<>(); + } + return new ArrayList<>(appConfig.soFiles); + } + + public void addSoFileToApp(String packageName, String originalPath, boolean deleteOriginal) { + AppConfig appConfig = config.perAppConfig.get(packageName); + if (appConfig == null) { + appConfig = new AppConfig(); + config.perAppConfig.put(packageName, appConfig); + } + + // Generate unique filename + String fileName = new File(originalPath).getName(); + String storedPath = SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName; + + // Copy SO file to our storage + Shell.Result result = Shell.cmd("cp " + originalPath + " " + storedPath).exec(); + if (result.isSuccess()) { + SoFile soFile = new SoFile(); + soFile.name = fileName; + soFile.storedPath = storedPath; + soFile.originalPath = originalPath; + appConfig.soFiles.add(soFile); + + if (deleteOriginal) { + Shell.cmd("rm " + originalPath).exec(); + } + + saveConfig(); + } + } + + public void removeSoFileFromApp(String packageName, SoFile soFile) { + AppConfig appConfig = config.perAppConfig.get(packageName); + if (appConfig == null) return; + + appConfig.soFiles.remove(soFile); + // Delete the stored file + Shell.cmd("rm " + soFile.storedPath).exec(); + saveConfig(); + } + + public boolean getHideInjection() { + return config.hideInjection; + } + + public void setHideInjection(boolean hide) { + config.hideInjection = hide; + saveConfig(); + } + + // Data classes + public static class ModuleConfig { + public boolean enabled = true; + public boolean hideInjection = false; + public Map perAppConfig = new HashMap<>(); + } + + public static class AppConfig { + public boolean enabled = false; + public List soFiles = new ArrayList<>(); + } + + public static class SoFile { + public String name; + public String storedPath; + public String originalPath; + + @Override + public boolean equals(Object obj) { + if (obj instanceof SoFile) { + return storedPath.equals(((SoFile) obj).storedPath); + } + return false; + } + } +} \ No newline at end of file diff --git a/configapp/src/main/java/com/jiqiu/configapp/MainActivity.java b/configapp/src/main/java/com/jiqiu/configapp/MainActivity.java index 65a401b..6311488 100644 --- a/configapp/src/main/java/com/jiqiu/configapp/MainActivity.java +++ b/configapp/src/main/java/com/jiqiu/configapp/MainActivity.java @@ -18,6 +18,7 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment. private BottomNavigationView bottomNavigationView; private AppListFragment appListFragment; private SettingsFragment settingsFragment; + private SoManagerFragment soManagerFragment; @Override protected void onCreate(Bundle savedInstanceState) { @@ -49,6 +50,9 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment. if (itemId == R.id.navigation_apps) { showAppListFragment(); return true; + } else if (itemId == R.id.navigation_so_manager) { + showSoManagerFragment(); + return true; } else if (itemId == R.id.navigation_settings) { showSettingsFragment(); return true; @@ -64,6 +68,13 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment. showFragment(appListFragment); } + private void showSoManagerFragment() { + if (soManagerFragment == null) { + soManagerFragment = new SoManagerFragment(); + } + showFragment(soManagerFragment); + } + private void showSettingsFragment() { if (settingsFragment == null) { settingsFragment = new SettingsFragment(); diff --git a/configapp/src/main/java/com/jiqiu/configapp/SoListAdapter.java b/configapp/src/main/java/com/jiqiu/configapp/SoListAdapter.java new file mode 100644 index 0000000..c1b8cff --- /dev/null +++ b/configapp/src/main/java/com/jiqiu/configapp/SoListAdapter.java @@ -0,0 +1,75 @@ +package com.jiqiu.configapp; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +public class SoListAdapter extends RecyclerView.Adapter { + + private List soFiles = new ArrayList<>(); + private OnSoFileActionListener listener; + + public interface OnSoFileActionListener { + void onDeleteClick(ConfigManager.SoFile soFile); + } + + public void setSoFiles(List files) { + this.soFiles = files; + notifyDataSetChanged(); + } + + public void setOnSoFileActionListener(OnSoFileActionListener listener) { + this.listener = listener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_so_file, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + ConfigManager.SoFile soFile = soFiles.get(position); + holder.bind(soFile); + } + + @Override + public int getItemCount() { + return soFiles.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + private TextView textFileName; + private TextView textFilePath; + private ImageButton buttonDelete; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + textFileName = itemView.findViewById(R.id.textFileName); + textFilePath = itemView.findViewById(R.id.textFilePath); + buttonDelete = itemView.findViewById(R.id.buttonDelete); + } + + public void bind(ConfigManager.SoFile soFile) { + textFileName.setText(soFile.name); + textFilePath.setText(soFile.originalPath); + + buttonDelete.setOnClickListener(v -> { + if (listener != null) { + listener.onDeleteClick(soFile); + } + }); + } + } +} \ No newline at end of file diff --git a/configapp/src/main/java/com/jiqiu/configapp/SoManagerFragment.java b/configapp/src/main/java/com/jiqiu/configapp/SoManagerFragment.java new file mode 100644 index 0000000..70278e9 --- /dev/null +++ b/configapp/src/main/java/com/jiqiu/configapp/SoManagerFragment.java @@ -0,0 +1,231 @@ +package com.jiqiu.configapp; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +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.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.topjohnwu.superuser.Shell; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class SoManagerFragment extends Fragment { + + private RecyclerView recyclerView; + private LinearLayout emptyView; + private SoListAdapter adapter; + private ConfigManager configManager; + private List globalSoFiles = new ArrayList<>(); + + private ActivityResultLauncher filePickerLauncher; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + configManager = new ConfigManager(requireContext()); + + // Initialize file picker + filePickerLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + Uri uri = result.getData().getData(); + if (uri != null) { + handleFileSelection(uri); + } + } + } + ); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_so_manager, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + recyclerView = view.findViewById(R.id.recyclerView); + emptyView = view.findViewById(R.id.emptyView); + FloatingActionButton fabAdd = view.findViewById(R.id.fabAdd); + + // Setup RecyclerView + adapter = new SoListAdapter(); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + + adapter.setOnSoFileActionListener(this::showDeleteConfirmation); + + // Setup FAB + fabAdd.setOnClickListener(v -> showAddSoDialog()); + + // Check root access + if (!configManager.isRootAvailable()) { + Toast.makeText(getContext(), "需要Root权限", Toast.LENGTH_LONG).show(); + } else { + configManager.ensureModuleDirectories(); + loadSoFiles(); + } + } + + private void loadSoFiles() { + // Load global SO files from the storage directory + Shell.Result result = Shell.cmd("ls -la " + ConfigManager.SO_STORAGE_DIR).exec(); + + globalSoFiles.clear(); + if (result.isSuccess()) { + for (String line : result.getOut()) { + if (line.contains(".so")) { + // Parse file info + String[] parts = line.split("\\s+"); + if (parts.length >= 9) { + String fileName = parts[parts.length - 1]; + ConfigManager.SoFile soFile = new ConfigManager.SoFile(); + soFile.name = fileName; + soFile.storedPath = ConfigManager.SO_STORAGE_DIR + "/" + fileName; + soFile.originalPath = soFile.storedPath; // For display + globalSoFiles.add(soFile); + } + } + } + } + + updateUI(); + } + + private void updateUI() { + if (globalSoFiles.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + adapter.setSoFiles(globalSoFiles); + } + } + + private void showAddSoDialog() { + String[] options = {"从文件管理器选择", "输入路径"}; + + new MaterialAlertDialogBuilder(requireContext()) + .setTitle("添加SO文件") + .setItems(options, (dialog, which) -> { + if (which == 0) { + openFilePicker(); + } else { + showPathInputDialog(); + } + }) + .show(); + } + + private void openFilePicker() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + 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.setHint("/data/local/tmp/example.so"); + + new MaterialAlertDialogBuilder(requireContext()) + .setTitle("输入SO文件路径") + .setView(view) + .setPositiveButton("添加", (dialog, which) -> { + String path = editText.getText().toString().trim(); + if (!path.isEmpty()) { + addSoFile(path, false); + } + }) + .setNegativeButton("取消", null) + .show(); + } + + private void handleFileSelection(Uri uri) { + // Get real path from URI + String path = uri.getPath(); + if (path != null) { + // Remove the file:// prefix if present + if (path.startsWith("file://")) { + path = path.substring(7); + } + addSoFile(path, false); + } + } + + private void addSoFile(String path, boolean deleteOriginal) { + // Verify file exists + Shell.Result result = Shell.cmd("test -f " + path + " && echo 'exists'").exec(); + if (!result.isSuccess() || result.getOut().isEmpty()) { + Toast.makeText(getContext(), "文件不存在: " + path, Toast.LENGTH_SHORT).show(); + return; + } + + // Generate unique filename + String fileName = new File(path).getName(); + String storedPath = ConfigManager.SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName; + + // Copy file + result = Shell.cmd("cp " + path + " " + storedPath).exec(); + if (result.isSuccess()) { + ConfigManager.SoFile soFile = new ConfigManager.SoFile(); + soFile.name = fileName; + soFile.storedPath = storedPath; + soFile.originalPath = path; + globalSoFiles.add(soFile); + + if (deleteOriginal) { + Shell.cmd("rm " + path).exec(); + } + + updateUI(); + Toast.makeText(getContext(), "SO文件已添加", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), "复制文件失败", Toast.LENGTH_SHORT).show(); + } + } + + private void showDeleteConfirmation(ConfigManager.SoFile soFile) { + new MaterialAlertDialogBuilder(requireContext()) + .setTitle("删除SO文件") + .setMessage("确定要删除 " + soFile.name + " 吗?") + .setPositiveButton("删除", (dialog, which) -> { + deleteSoFile(soFile); + }) + .setNegativeButton("取消", null) + .show(); + } + + private void deleteSoFile(ConfigManager.SoFile soFile) { + Shell.cmd("rm " + soFile.storedPath).exec(); + globalSoFiles.remove(soFile); + updateUI(); + Toast.makeText(getContext(), "SO文件已删除", Toast.LENGTH_SHORT).show(); + } +} \ No newline at end of file diff --git a/configapp/src/main/res/layout/dialog_input.xml b/configapp/src/main/res/layout/dialog_input.xml new file mode 100644 index 0000000..cbdb0ea --- /dev/null +++ b/configapp/src/main/res/layout/dialog_input.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/configapp/src/main/res/layout/fragment_so_manager.xml b/configapp/src/main/res/layout/fragment_so_manager.xml new file mode 100644 index 0000000..2023503 --- /dev/null +++ b/configapp/src/main/res/layout/fragment_so_manager.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/configapp/src/main/res/layout/item_so_file.xml b/configapp/src/main/res/layout/item_so_file.xml new file mode 100644 index 0000000..5e959d1 --- /dev/null +++ b/configapp/src/main/res/layout/item_so_file.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/configapp/src/main/res/menu/bottom_nav_menu.xml b/configapp/src/main/res/menu/bottom_nav_menu.xml index 644622b..753e065 100644 --- a/configapp/src/main/res/menu/bottom_nav_menu.xml +++ b/configapp/src/main/res/menu/bottom_nav_menu.xml @@ -5,6 +5,11 @@ android:icon="@android:drawable/ic_menu_view" android:title="@string/title_apps" /> + + 应用列表 + SO库管理 全局设置 diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/module/src/main/cpp/CMakeLists.txt b/module/src/main/cpp/CMakeLists.txt index 659cb82..84b4a40 100644 --- a/module/src/main/cpp/CMakeLists.txt +++ b/module/src/main/cpp/CMakeLists.txt @@ -35,7 +35,8 @@ aux_source_directory(xdl xdl-src) add_library(${MODULE_NAME} SHARED main.cpp - hack.cpp + hack_new.cpp + config.cpp newriruhide.cpp pmparser.cpp ${xdl-src}) diff --git a/module/src/main/cpp/config.cpp b/module/src/main/cpp/config.cpp new file mode 100644 index 0000000..c4ce30f --- /dev/null +++ b/module/src/main/cpp/config.cpp @@ -0,0 +1,189 @@ +#include "config.h" +#include +#include +#include + +#define LOG_TAG "MyInjector" +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +namespace Config { + + static ModuleConfig g_config; + static bool g_configLoaded = false; + + // Simple JSON parser for our specific format + std::string extractValue(const std::string& json, const std::string& key) { + size_t keyPos = json.find("\"" + key + "\""); + if (keyPos == std::string::npos) return ""; + + size_t colonPos = json.find(":", keyPos); + if (colonPos == std::string::npos) return ""; + + size_t valueStart = json.find_first_not_of(" \t\n", colonPos + 1); + if (valueStart == std::string::npos) return ""; + + if (json[valueStart] == '"') { + // String value + size_t valueEnd = json.find('"', valueStart + 1); + if (valueEnd == std::string::npos) return ""; + return json.substr(valueStart + 1, valueEnd - valueStart - 1); + } else if (json[valueStart] == 't' || json[valueStart] == 'f') { + // Boolean value + return (json.substr(valueStart, 4) == "true") ? "true" : "false"; + } + + return ""; + } + + void parseAppConfig(const std::string& packageName, const std::string& appJson) { + AppConfig appConfig; + + // Parse enabled + std::string enabledStr = extractValue(appJson, "enabled"); + appConfig.enabled = (enabledStr == "true"); + + // Parse soFiles array + size_t soFilesPos = appJson.find("\"soFiles\""); + if (soFilesPos != std::string::npos) { + size_t arrayStart = appJson.find("[", soFilesPos); + size_t arrayEnd = appJson.find("]", arrayStart); + + if (arrayStart != std::string::npos && arrayEnd != std::string::npos) { + std::string soFilesArray = appJson.substr(arrayStart + 1, arrayEnd - arrayStart - 1); + + // Parse each SO file object + size_t objStart = 0; + while ((objStart = soFilesArray.find("{", objStart)) != std::string::npos) { + size_t objEnd = soFilesArray.find("}", objStart); + if (objEnd == std::string::npos) break; + + std::string soFileObj = soFilesArray.substr(objStart, objEnd - objStart + 1); + + SoFile soFile; + soFile.name = extractValue(soFileObj, "name"); + soFile.storedPath = extractValue(soFileObj, "storedPath"); + soFile.originalPath = extractValue(soFileObj, "originalPath"); + + if (!soFile.storedPath.empty()) { + appConfig.soFiles.push_back(soFile); + LOGD("Added SO file: %s at %s", soFile.name.c_str(), soFile.storedPath.c_str()); + } + + objStart = objEnd + 1; + } + } + } + + g_config.perAppConfig[packageName] = appConfig; + LOGD("Loaded config for app: %s, enabled: %d, SO files: %zu", + packageName.c_str(), appConfig.enabled, appConfig.soFiles.size()); + } + + ModuleConfig readConfig() { + if (g_configLoaded) { + return g_config; + } + + const char* configPath = "/data/adb/modules/zygisk-myinjector/config.json"; + std::ifstream file(configPath); + + if (!file.is_open()) { + LOGE("Failed to open config file: %s", configPath); + g_configLoaded = true; + return g_config; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string json = buffer.str(); + file.close(); + + // Parse global settings + std::string enabledStr = extractValue(json, "enabled"); + g_config.enabled = (enabledStr != "false"); + + std::string hideStr = extractValue(json, "hideInjection"); + g_config.hideInjection = (hideStr == "true"); + + LOGD("Module enabled: %d, hide injection: %d", g_config.enabled, g_config.hideInjection); + + // Parse perAppConfig + size_t perAppPos = json.find("\"perAppConfig\""); + if (perAppPos != std::string::npos) { + size_t objStart = json.find("{", perAppPos + 14); + size_t objEnd = json.rfind("}"); + + if (objStart != std::string::npos && objEnd != std::string::npos) { + std::string perAppObj = json.substr(objStart + 1, objEnd - objStart - 1); + + // Find each package config + size_t pos = 0; + while (pos < perAppObj.length()) { + // Find package name + size_t pkgStart = perAppObj.find("\"", pos); + if (pkgStart == std::string::npos) break; + + size_t pkgEnd = perAppObj.find("\"", pkgStart + 1); + if (pkgEnd == std::string::npos) break; + + std::string packageName = perAppObj.substr(pkgStart + 1, pkgEnd - pkgStart - 1); + + // Find app config object + size_t appObjStart = perAppObj.find("{", pkgEnd); + if (appObjStart == std::string::npos) break; + + // Find matching closing brace + int braceCount = 1; + size_t appObjEnd = appObjStart + 1; + while (appObjEnd < perAppObj.length() && braceCount > 0) { + if (perAppObj[appObjEnd] == '{') braceCount++; + else if (perAppObj[appObjEnd] == '}') braceCount--; + appObjEnd++; + } + + if (braceCount == 0) { + std::string appConfigStr = perAppObj.substr(appObjStart, appObjEnd - appObjStart); + parseAppConfig(packageName, appConfigStr); + } + + pos = appObjEnd; + } + } + } + + g_configLoaded = true; + return g_config; + } + + bool isAppEnabled(const std::string& packageName) { + if (!g_configLoaded) { + readConfig(); + } + + auto it = g_config.perAppConfig.find(packageName); + if (it != g_config.perAppConfig.end()) { + return it->second.enabled; + } + return false; + } + + std::vector getAppSoFiles(const std::string& packageName) { + if (!g_configLoaded) { + readConfig(); + } + + auto it = g_config.perAppConfig.find(packageName); + if (it != g_config.perAppConfig.end()) { + return it->second.soFiles; + } + return {}; + } + + bool shouldHideInjection() { + if (!g_configLoaded) { + readConfig(); + } + return g_config.hideInjection; + } +} \ No newline at end of file diff --git a/module/src/main/cpp/config.h b/module/src/main/cpp/config.h new file mode 100644 index 0000000..7401555 --- /dev/null +++ b/module/src/main/cpp/config.h @@ -0,0 +1,40 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include + +namespace Config { + + struct SoFile { + std::string name; + std::string storedPath; + std::string originalPath; + }; + + struct AppConfig { + bool enabled = false; + std::vector soFiles; + }; + + struct ModuleConfig { + bool enabled = true; + bool hideInjection = false; + std::unordered_map perAppConfig; + }; + + // Read configuration from file + ModuleConfig readConfig(); + + // Check if app is enabled for injection + bool isAppEnabled(const std::string& packageName); + + // Get SO files for specific app + std::vector getAppSoFiles(const std::string& packageName); + + // Get hide injection setting + bool shouldHideInjection(); +} + +#endif // CONFIG_H \ No newline at end of file diff --git a/module/src/main/cpp/hack_new.cpp b/module/src/main/cpp/hack_new.cpp new file mode 100644 index 0000000..6274132 --- /dev/null +++ b/module/src/main/cpp/hack_new.cpp @@ -0,0 +1,97 @@ +#include "hack.h" +#include "config.h" +#include "log.h" +#include +#include +#include +#include +#include +#include + +void load_so_file(const char *game_data_dir, const Config::SoFile &soFile) { + char dest_path[512]; + snprintf(dest_path, sizeof(dest_path), "%s/files/%s", game_data_dir, soFile.name.c_str()); + + // Copy SO file from storage to app directory + int src_fd = open(soFile.storedPath.c_str(), O_RDONLY); + if (src_fd < 0) { + LOGE("Failed to open source SO: %s", soFile.storedPath.c_str()); + return; + } + + int dest_fd = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC, 0755); + if (dest_fd < 0) { + LOGE("Failed to create dest SO: %s", dest_path); + close(src_fd); + return; + } + + char buffer[4096]; + ssize_t bytes; + while ((bytes = read(src_fd, buffer, sizeof(buffer))) > 0) { + if (write(dest_fd, buffer, bytes) != bytes) { + LOGE("Failed to write SO file"); + close(src_fd); + close(dest_fd); + return; + } + } + + close(src_fd); + close(dest_fd); + chmod(dest_path, 0755); + + // Load the SO file + void *handle = dlopen(dest_path, RTLD_NOW | RTLD_LOCAL); + if (handle) { + LOGI("Successfully loaded SO: %s", soFile.name.c_str()); + + // Hide if configured + if (Config::shouldHideInjection()) { + // Call hide function if available + void (*hide_func)(const char*) = (void(*)(const char*))dlsym(handle, "riru_hide"); + if (hide_func) { + hide_func(soFile.name.c_str()); + } + } + } else { + LOGE("Failed to load SO: %s - %s", dest_path, dlerror()); + } +} + +void hack_thread_func(const char *game_data_dir, const char *package_name) { + LOGI("Hack thread started for package: %s", package_name); + + // Wait a bit for app to initialize + sleep(1); + + // 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 + for (const auto &soFile : soFiles) { + LOGI("Loading SO: %s", soFile.name.c_str()); + load_so_file(game_data_dir, soFile); + } +} + +void hack_prepare(const char *game_data_dir, void *data, size_t length) { + // Get package name from game_data_dir + // Format: /data/user/0/com.example.app or /data/data/com.example.app + const char *package_name = nullptr; + if (strstr(game_data_dir, "/data/user/")) { + package_name = strrchr(game_data_dir, '/'); + if (package_name) package_name++; + } else if (strstr(game_data_dir, "/data/data/")) { + package_name = game_data_dir + strlen("/data/data/"); + } + + if (!package_name) { + LOGE("Failed to extract package name from: %s", game_data_dir); + return; + } + + std::thread hack_thread(hack_thread_func, game_data_dir, package_name); + hack_thread.detach(); +} \ No newline at end of file diff --git a/module/src/main/cpp/main.cpp b/module/src/main/cpp/main.cpp index 4594e0c..27edb64 100644 --- a/module/src/main/cpp/main.cpp +++ b/module/src/main/cpp/main.cpp @@ -11,6 +11,7 @@ #include "game.h" #include "log.h" #include "dlfcn.h" +#include "config.h" using zygisk::Api; using zygisk::AppSpecializeArgs; using zygisk::ServerSpecializeArgs; @@ -51,7 +52,11 @@ private: size_t length; void preSpecialize(const char *package_name, const char *app_data_dir) { - if (strcmp(package_name, AimPackageName) == 0) { + // Read configuration + Config::readConfig(); + + // Check if this app is enabled for injection + if (Config::isAppEnabled(package_name)) { LOGI("成功注入目标进程: %s", package_name); enable_hack = true; _data_dir = new char[strlen(app_data_dir) + 1];