diff --git a/manager/app/src/main/assets/ksu_susfs_1.5.12 b/manager/app/src/main/assets/ksu_susfs_1.5.12 index b627a1e5..c10851bb 100644 Binary files a/manager/app/src/main/assets/ksu_susfs_1.5.12 and b/manager/app/src/main/assets/ksu_susfs_1.5.12 differ diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt index dfa01e5d..5097047b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt @@ -26,20 +26,10 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.sukisu.ultra.R -import com.sukisu.ultra.ui.susfs.component.AddAppPathDialog -import com.sukisu.ultra.ui.susfs.component.AddKstatStaticallyDialog -import com.sukisu.ultra.ui.susfs.component.AddPathDialog -import com.sukisu.ultra.ui.susfs.component.AddTryUmountDialog -import com.sukisu.ultra.ui.susfs.component.ConfirmDialog -import com.sukisu.ultra.ui.susfs.component.EnabledFeaturesContent -import com.sukisu.ultra.ui.susfs.component.KstatConfigContent -import com.sukisu.ultra.ui.susfs.component.PathSettingsContent -import com.sukisu.ultra.ui.susfs.component.SusLoopPathsContent -import com.sukisu.ultra.ui.susfs.component.SusMountsContent -import com.sukisu.ultra.ui.susfs.component.SusPathsContent -import com.sukisu.ultra.ui.susfs.component.TryUmountContent +import com.sukisu.ultra.ui.susfs.component.* import com.sukisu.ultra.ui.theme.CardConfig import com.sukisu.ultra.ui.susfs.util.SuSFSManager +import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion1512 import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158 import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion159 import com.sukisu.ultra.ui.util.getSuSFSVersion @@ -56,6 +46,7 @@ enum class SuSFSTab(val displayNameRes: Int) { BASIC_SETTINGS(R.string.susfs_tab_basic_settings), SUS_PATHS(R.string.susfs_tab_sus_paths), SUS_LOOP_PATHS(R.string.susfs_tab_sus_loop_paths), + SUS_MAPS(R.string.susfs_tab_sus_maps), SUS_MOUNTS(R.string.susfs_tab_sus_mounts), TRY_UMOUNT(R.string.susfs_tab_try_umount), KSTAT_CONFIG(R.string.susfs_tab_kstat_config), @@ -63,11 +54,12 @@ enum class SuSFSTab(val displayNameRes: Int) { ENABLED_FEATURES(R.string.susfs_tab_enabled_features); companion object { - fun getAllTabs(isSusVersion158: Boolean, isSusVersion159: Boolean): List { + fun getAllTabs(isSusVersion158: Boolean, isSusVersion159: Boolean, isSusVersion1512: Boolean): List { return when { - isSusVersion159 -> entries.toList() + isSusVersion1512 -> entries.toList() + isSusVersion159 -> entries.filter { it != SUS_MOUNTS} isSusVersion158 -> entries.filter { it != SUS_LOOP_PATHS } - else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS } + else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS && it != SUS_MOUNTS } } } } @@ -106,6 +98,7 @@ fun SuSFSConfigScreen( // 路径管理相关状态 var susPaths by remember { mutableStateOf(emptySet()) } var susLoopPaths by remember { mutableStateOf(emptySet()) } + var susMaps by remember { mutableStateOf(emptySet()) } var susMounts by remember { mutableStateOf(emptySet()) } var tryUmounts by remember { mutableStateOf(emptySet()) } var androidDataPath by remember { mutableStateOf("") } @@ -130,6 +123,7 @@ fun SuSFSConfigScreen( // 对话框状态 var showAddPathDialog by remember { mutableStateOf(false) } var showAddLoopPathDialog by remember { mutableStateOf(false) } + var showAddSusMapDialog by remember { mutableStateOf(false) } var showAddAppPathDialog by remember { mutableStateOf(false) } var showAddMountDialog by remember { mutableStateOf(false) } var showAddUmountDialog by remember { mutableStateOf(false) } @@ -139,6 +133,7 @@ fun SuSFSConfigScreen( // 编辑状态 var editingPath by remember { mutableStateOf(null) } var editingLoopPath by remember { mutableStateOf(null) } + var editingSusMap by remember { mutableStateOf(null) } var editingMount by remember { mutableStateOf(null) } var editingUmount by remember { mutableStateOf(null) } var editingKstatConfig by remember { mutableStateOf(null) } @@ -147,6 +142,7 @@ fun SuSFSConfigScreen( // 重置确认对话框状态 var showResetPathsDialog by remember { mutableStateOf(false) } var showResetLoopPathsDialog by remember { mutableStateOf(false) } + var showResetSusMapsDialog by remember { mutableStateOf(false) } var showResetMountsDialog by remember { mutableStateOf(false) } var showResetUmountsDialog by remember { mutableStateOf(false) } var showResetKstatDialog by remember { mutableStateOf(false) } @@ -160,7 +156,7 @@ fun SuSFSConfigScreen( var isNavigating by remember { mutableStateOf(false) } - val allTabs = SuSFSTab.getAllTabs(isSusVersion158(), isSusVersion159()) + val allTabs = SuSFSTab.getAllTabs(isSusVersion158(), isSusVersion159(), isSusVersion1512()) // 实时判断是否可以启用开机自启动 val canEnableAutoStart by remember { @@ -307,6 +303,7 @@ fun SuSFSConfigScreen( executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) susPaths = SuSFSManager.getSusPaths(context) susLoopPaths = SuSFSManager.getSusLoopPaths(context) + susMaps = SuSFSManager.getSusMaps(context) susMounts = SuSFSManager.getSusMounts(context) tryUmounts = SuSFSManager.getTryUmounts(context) androidDataPath = SuSFSManager.getAndroidDataPath(context) @@ -479,6 +476,7 @@ fun SuSFSConfigScreen( executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) susPaths = SuSFSManager.getSusPaths(context) susLoopPaths = SuSFSManager.getSusLoopPaths(context) + susMaps = SuSFSManager.getSusMaps(context) susMounts = SuSFSManager.getSusMounts(context) tryUmounts = SuSFSManager.getTryUmounts(context) androidDataPath = SuSFSManager.getAndroidDataPath(context) @@ -597,6 +595,35 @@ fun SuSFSConfigScreen( initialValue = editingLoopPath ?: "" ) + AddPathDialog( + showDialog = showAddSusMapDialog, + onDismiss = { + showAddSusMapDialog = false + editingSusMap = null + }, + onConfirm = { path -> + coroutineScope.launch { + isLoading = true + val success = if (editingSusMap != null) { + SuSFSManager.editSusMap(context, editingSusMap!!, path) + } else { + SuSFSManager.addSusMap(context, path) + } + if (success) { + susMaps = SuSFSManager.getSusMaps(context) + } + isLoading = false + showAddSusMapDialog = false + editingSusMap = null + } + }, + isLoading = isLoading, + titleRes = if (editingSusMap != null) R.string.susfs_edit_sus_map else R.string.susfs_add_sus_map, + labelRes = R.string.susfs_sus_map_label, + placeholderRes = R.string.susfs_sus_map_placeholder, + initialValue = editingSusMap ?: "" + ) + AddAppPathDialog( showDialog = showAddAppPathDialog, onDismiss = { showAddAppPathDialog = false }, @@ -817,6 +844,27 @@ fun SuSFSConfigScreen( isDestructive = true ) + ConfirmDialog( + showDialog = showResetSusMapsDialog, + onDismiss = { showResetSusMapsDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveSusMaps(context, emptySet()) + susMaps = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetSusMapsDialog = false + } + }, + titleRes = R.string.susfs_reset_sus_maps_title, + messageRes = R.string.susfs_reset_sus_maps_message, + isLoading = isLoading, + isDestructive = true + ) + ConfirmDialog( showDialog = showResetMountsDialog, onDismiss = { showResetMountsDialog = false }, @@ -1036,6 +1084,28 @@ fun SuSFSConfigScreen( } } + SuSFSTab.SUS_MAPS -> { + OutlinedButton( + onClick = { showResetSusMapsDialog = true }, + enabled = !isLoading && susMaps.isNotEmpty(), + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + stringResource(R.string.susfs_reset_sus_maps_title), + fontWeight = FontWeight.Medium + ) + } + } + SuSFSTab.SUS_MOUNTS -> { OutlinedButton( onClick = { showResetMountsDialog = true }, @@ -1300,6 +1370,26 @@ fun SuSFSConfigScreen( } ) } + SuSFSTab.SUS_MAPS -> { + SusMapsContent( + susMaps = susMaps, + isLoading = isLoading, + onAddSusMap = { showAddSusMapDialog = true }, + onRemoveSusMap = { map -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeSusMap(context, map)) { + susMaps = SuSFSManager.getSusMaps(context) + } + isLoading = false + } + }, + onEditSusMap = { map -> + editingSusMap = map + showAddSusMapDialog = true + } + ) + } SuSFSTab.SUS_MOUNTS -> { val isSusVersion158 = remember { isSusVersion158() } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt index 0ec7aef2..be683f23 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt @@ -310,6 +310,116 @@ fun SusLoopPathsContent( } } +/** + * SUS Maps内容组件 + */ +@Composable +fun SusMapsContent( + susMaps: Set, + isLoading: Boolean, + onAddSusMap: () -> Unit, + onRemoveSusMap: (String) -> Unit, + onEditSusMap: ((String) -> Unit)? = null +) { + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 说明卡片 + item { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.sus_maps_description_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.sus_maps_description_text), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.sus_maps_warning), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.secondary + ) + Text( + text = stringResource(R.string.sus_maps_debug_info), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + } + + if (susMaps.isEmpty()) { + item { + EmptyStateCard( + message = stringResource(R.string.susfs_no_sus_maps_configured) + ) + } + } else { + item { + SectionHeader( + title = stringResource(R.string.sus_maps_section), + subtitle = null, + icon = Icons.Default.Security, + count = susMaps.size + ) + } + + items(susMaps.toList()) { map -> + PathItemCard( + path = map, + icon = Icons.Default.Security, + onDelete = { onRemoveSusMap(map) }, + onEdit = if (onEditSusMap != null) { { onEditSusMap(map) } } else null, + isLoading = isLoading + ) + } + } + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddSusMap, + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = stringResource(R.string.add)) + } + } + } + } + } +} + /** * SUS挂载内容组件 */ diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt index 24a570f6..49a64830 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt @@ -42,6 +42,8 @@ object SuSFSManager { private const val KEY_AUTO_START_ENABLED = "auto_start_enabled" private const val KEY_SUS_PATHS = "sus_paths" private const val KEY_SUS_LOOP_PATHS = "sus_loop_paths" + + private const val KEY_SUS_MAPS = "sus_maps" private const val KEY_SUS_MOUNTS = "sus_mounts" private const val KEY_TRY_UMOUNTS = "try_umounts" private const val KEY_ANDROID_DATA_PATH = "android_data_path" @@ -65,6 +67,7 @@ object SuSFSManager { private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID" private const val MIN_VERSION_FOR_HIDE_MOUNT = "1.5.8" private const val MIN_VERSION_FOR_LOOP_PATH = "1.5.9" + private const val MIN_VERSION_SUS_MAPS = "1.5.12" const val MAX_SUSFS_VERSION = "1.5.12" private const val BACKUP_FILE_EXTENSION = ".susfs_backup" private const val MEDIA_DATA_PATH = "/data/media/0/Android/data" @@ -153,6 +156,7 @@ object SuSFSManager { val executeInPostFsData: Boolean, val susPaths: Set, val susLoopPaths: Set, + val susMaps: Set, val susMounts: Set, val tryUmounts: Set, val androidDataPath: String, @@ -175,6 +179,7 @@ object SuSFSManager { buildTimeValue != DEFAULT_BUILD_TIME || susPaths.isNotEmpty() || susLoopPaths.isNotEmpty() || + susMaps.isNotEmpty() || susMounts.isNotEmpty() || tryUmounts.isNotEmpty() || kstatConfigs.isNotEmpty() || @@ -264,6 +269,15 @@ object SuSFSManager { } } + fun isSusVersion1512(): Boolean { + return try { + val currentVersion = getSuSFSVersion() + compareVersions(currentVersion, MIN_VERSION_SUS_MAPS) >= 0 + } catch (_: Exception) { + true + } + } + /** * 获取当前模块配置 */ @@ -275,6 +289,7 @@ object SuSFSManager { executeInPostFsData = getExecuteInPostFsData(context), susPaths = getSusPaths(context), susLoopPaths = getSusLoopPaths(context), + susMaps = getSusMaps(context), susMounts = getSusMounts(context), tryUmounts = getTryUmounts(context), androidDataPath = getAndroidDataPath(context), @@ -379,6 +394,12 @@ object SuSFSManager { fun getSusLoopPaths(context: Context): Set = getPrefs(context).getStringSet(KEY_SUS_LOOP_PATHS, emptySet()) ?: emptySet() + fun saveSusMaps(context: Context, maps: Set) = + getPrefs(context).edit { putStringSet(KEY_SUS_MAPS, maps) } + + fun getSusMaps(context: Context): Set = + getPrefs(context).getStringSet(KEY_SUS_MAPS, emptySet()) ?: emptySet() + fun saveSusMounts(context: Context, mounts: Set) = getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) } @@ -544,6 +565,7 @@ object SuSFSManager { KEY_AUTO_START_ENABLED to isAutoStartEnabled(context), KEY_SUS_PATHS to getSusPaths(context), KEY_SUS_LOOP_PATHS to getSusLoopPaths(context), + KEY_SUS_MAPS to getSusMaps(context), KEY_SUS_MOUNTS to getSusMounts(context), KEY_TRY_UMOUNTS to getTryUmounts(context), KEY_ANDROID_DATA_PATH to getAndroidDataPath(context), @@ -1137,6 +1159,54 @@ object SuSFSManager { } } + // 添加 SUS Maps + suspend fun addSusMap(context: Context, map: String): Boolean { + val success = executeSusfsCommand(context, "add_sus_map '$map'") + if (success) { + saveSusMaps(context, getSusMaps(context) + map) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_sus_map_added_success, map)) + } + return success + } + + suspend fun removeSusMap(context: Context, map: String): Boolean { + saveSusMaps(context, getSusMaps(context) - map) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_sus_map_removed, map)) + return true + } + + suspend fun editSusMap(context: Context, oldMap: String, newMap: String): Boolean { + return try { + val currentMaps = getSusMaps(context).toMutableSet() + if (!currentMaps.remove(oldMap)) { + showToast(context, "Original SUS map not found: $oldMap") + return false + } + + saveSusMaps(context, currentMaps) + + val success = addSusMap(context, newMap) + + if (success) { + showToast(context, context.getString(R.string.susfs_sus_map_updated, oldMap, newMap)) + return true + } else { + // 如果添加新映射失败,恢复旧映射 + currentMaps.add(oldMap) + saveSusMaps(context, currentMaps) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update SUS map, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating SUS map: ${e.message}") + false + } + } + // 添加SUS挂载 suspend fun addSusMount(context: Context, mount: String): Boolean { val success = executeSusfsCommand(context, "add_sus_mount '$mount'") diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt index 1f2768f7..e0d9baef 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt @@ -497,6 +497,10 @@ object ScriptGenerator { if (config.susLoopPaths.isNotEmpty()) { generateSusLoopPathsSection(config.susLoopPaths) } + + if (config.susMaps.isNotEmpty()) { + generateSusMapsSection(config.susMaps) + } } } @@ -504,6 +508,17 @@ object ScriptGenerator { } } + private fun StringBuilder.generateSusMapsSection(susMaps: Set) { + if (susMaps.isNotEmpty()) { + appendLine("# 添加SUS映射") + susMaps.forEach { map -> + appendLine("\"${'$'}SUSFS_BIN\" add_sus_map '$map'") + appendLine("echo \"$(get_current_time): 添加SUS映射: $map\" >> \"${'$'}LOG_FILE\"") + } + appendLine() + } + } + @SuppressLint("SdCardPath") private fun StringBuilder.generatePathSettingSection(androidDataPath: String, sdcardPath: String) { appendLine("# 路径配置") diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 6a2235c1..35fb29f4 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -670,4 +670,21 @@ 作者 描述 支持设备 + + SUS映射 + 库文件路径 + /data/adb/modules/my_module/zygisk/arm64-v8a.so + 添加SUS映射 + 编辑SUS映射 + SUS映射添加成功: %1$s + SUS映射已移除: %1$s + SUS映射已更新: %1$s -> %2$s + 未配置SUS映射 + 重置SUS映射 + 这将移除所有已配置的SUS映射。此操作无法撤销。 + 内存映射隐藏 + 隐藏/proc/self/中各种映射中的mmap真实文件 + 从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子。 + 重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测。 + 首先通过 ps -enf 查找目标应用的 PID 和 UID,然后检查 /proc/<pid>/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作。 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 63d2451a..8991e3cd 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -678,4 +678,21 @@ Important Note:\n Author Description Supported Devices + + SUS Maps + Library Path + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Add SUS Map + Edit SUS Map + SUS map added successfully: %1$s + SUS map removed: %1$s + SUS map updated: %1$s -> %2$s + No SUS maps configured + Reset SUS Maps + This will remove all configured SUS maps. This action cannot be undone. + Memory Map Hiding + Hide the mmapped real file from various maps in /proc/self/ + Hide the real file paths of memory mappings from /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Please note: This feature does not support hiding anonymous memory mappings, nor can it hide inline hooks or PLT hooks caused by the injected library itself. + Important Notice: For applications with well-implemented injection detection mechanisms, this feature may not effectively bypass detection. + First, find the target application\'s PID and UID using ps -enf, then check the relevant paths in /proc/<pid>/maps and compare the device numbers with those in /proc/1/mountinfo to ensure consistency. Only when the device numbers match can the map hiding function work properly. \ No newline at end of file