commit 9761d73ece7a5c35abde08c7a2fb39e321a6528c
parent 62350a048c39042226ee9a4edae7f5549dd2237a
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 10 Apr 2025 18:40:07 +0200

refactor(core): security features

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt | 9+++++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SecurityFeatures.kt | 148+++++++++++++++++++++++++++++++++++--------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 19+++++++++++++------
4 files changed, 85 insertions(+), 92 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog +import androidx.core.content.edit import androidx.core.net.toUri import androidx.navigation.NavBackStackEntry import kotlinx.coroutines.launch @@ -52,9 +53,9 @@ class HomeSettings : Routes.Route() { .clickable { value = !value sharedPreferences - .edit() - .putBoolean(realKey, value) - .apply() + .edit() { + putBoolean(realKey, value) + } }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically @@ -284,7 +285,7 @@ class HomeSettings : Routes.Route() { Column( verticalArrangement = Arrangement.spacedBy(4.dp), ) { - PreferenceToggle(context.sharedPreferences, key = "disable_sif_prod", text = "Disable Snap Integrity Fix") + PreferenceToggle(context.sharedPreferences, key = "enable_security_features", text = "Enable Security Features") PreferenceToggle(context.sharedPreferences, key = "disable_feature_loading", text = "Disable Feature Loading") PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = "Disable Auto Mapper") } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt @@ -75,6 +75,7 @@ class ModContext( val isDeveloper by lazy { config.scripting.developerMode.get() } var isMainActivityPaused = true + var isSafeMode = false fun <T : Feature> feature(featureClass: KClass<T>): T { return features.get(featureClass)!! diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SecurityFeatures.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SecurityFeatures.kt @@ -1,16 +1,20 @@ package me.rhunk.snapenhance.core import android.system.Os -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import android.view.ViewGroup +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.NotInterested +import androidx.compose.material.icons.rounded.NotInterested import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import me.rhunk.snapenhance.common.bridge.FileHandleScope @@ -18,13 +22,8 @@ import me.rhunk.snapenhance.common.bridge.InternalFileHandleType import me.rhunk.snapenhance.common.bridge.toWrapper import me.rhunk.snapenhance.common.config.MOD_DETECTION_VERSION_CHECK import me.rhunk.snapenhance.common.config.VersionRequirement -import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor -import me.rhunk.snapenhance.common.util.protobuf.ProtoReader -import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent +import me.rhunk.snapenhance.common.ui.createComposeView import me.rhunk.snapenhance.core.ui.CustomComposable -import me.rhunk.snapenhance.core.util.ktx.setObjectField -import java.io.FileDescriptor -import kotlin.text.isNotEmpty class SecurityFeatures( private val context: ModContext @@ -39,10 +38,9 @@ class SecurityFeatures( transact(this, 0)?.toString(2)?.padStart(32, '0')?.count { it == '1' } } - fun init(): Boolean { + private fun isSafeMode(): Boolean { val snapchatVersionCode = context.androidContext.packageManager?.getPackageInfo(context.androidContext.packageName, 0)?.longVersionCode ?: throw IllegalStateException("Failed to get version code") val shouldUseSafeMode = MOD_DETECTION_VERSION_CHECK.checkVersion(snapchatVersionCode)?.second == VersionRequirement.OLDER_REQUIRED // true if version is >12.81.0.44 - val debugDisable = context.bridgeClient.getDebugProp("disable_sif_prod", "false") == "true" context.config.experimental.nativeHooks.customSharedLibrary.get().takeIf { it.isNotEmpty() }?.let { runCatching { @@ -54,74 +52,28 @@ class SecurityFeatures( context.log.error("Failed to load custom shared library", it) return true } - } - - if (!debugDisable) { - runCatching { - context.native.loadSharedLibrary( - context.fileHandlerManager.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.SIF.key) - .toWrapper() - .readBytes() - .takeIf { - it.isNotEmpty() - } ?: throw IllegalStateException("buffer is empty") - ) - context.log.verbose("loaded sif") - }.onFailure { - context.log.error("Failed to load sif", it) - return true - } - } else { - context.log.warn("sif is disabled") - } + } ?: context.bridgeClient.getDebugProp("enable_security_features", "false").takeIf { it == "true" }?.runCatching { + context.native.loadSharedLibrary( + context.fileHandlerManager.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.SIF.key) + .toWrapper() + .readBytes() + .takeIf { + it.isNotEmpty() + } ?: throw IllegalStateException("Binary is empty") + ) + context.log.verbose("loaded sif") + }?.onFailure { + context.log.error("Failed to load sif: " + it.message) + return shouldUseSafeMode + } ?: context.log.warn("Security features are disabled") token // pre init token - context.event.subscribe(UnaryCallEvent::class) { event -> - if (!event.uri.contains("/Login")) return@subscribe - - // intercept login response - event.addResponseCallback { - val response = ProtoReader(buffer) - val isBlocked = when { - event.uri.contains("TLv") -> response.getVarInt(1) == 14L - else -> response.getVarInt(1) == 16L - } - - val errorDataIndex = when { - response.contains(11) -> 11 - response.contains(10) -> 10 - response.contains(8) -> 8 - else -> return@addResponseCallback - } - - if (isBlocked) { - val status = transact(token ?: return@addResponseCallback, 1) - ?.takeIf { it != 0 } - ?.let { - val buffer = ByteArray(8192) - val fd = FileDescriptor().apply { - setObjectField("descriptor", it) - } - val read = Os.read(fd, buffer, 0, buffer.size) - Os.close(fd) - buffer.copyOfRange(0, read).decodeToString() - } ?: return@addResponseCallback - - buffer = ProtoEditor(buffer).apply { - edit(errorDataIndex) { - remove(1) - addString(1, status) - } - }.toByteArray() - } - } - } - val status = getStatus() val safeMode = shouldUseSafeMode && (status == null || status < 2) if (status != null && status >= 2) { + context.log.verbose("status=$status") lateinit var composable: CustomComposable composable = { Row( @@ -140,17 +92,49 @@ class SecurityFeatures( context.inAppOverlay.addCustomComposable(composable) } - if (safeMode && !debugDisable) { - context.features.addActivityCreateListener { - context.inAppOverlay.showStatusToast( - icon = Icons.Filled.NotInterested, - text = "SnapEnhance is not compatible with this version of Snapchat without SIF and will result in a ban.\nUse Snapchat ${MOD_DETECTION_VERSION_CHECK.maxVersion?.first ?: "0.0.0"} or older to avoid detections or use test accounts.", - durationMs = 10000, - maxLines = 6 - ) + return safeMode + } + + fun init() { + context.isSafeMode = isSafeMode() + context.log.verbose("isSafeMode=${context.isSafeMode}") + if (!context.isSafeMode) return + + context.features.addActivityCreateListener { activity -> + if (!activity.javaClass.name.endsWith("LoginSignupActivity")) return@addActivityCreateListener + + activity.findViewById<ViewGroup>(android.R.id.content).apply { + visibility = ViewGroup.INVISIBLE + + post { + addView(createComposeView(activity) { + Surface( + modifier = Modifier.fillMaxSize() + ) { + Box( + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.align(Alignment.Center).padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon(Icons.Rounded.NotInterested, contentDescription = null, tint = MaterialTheme.colorScheme.onSurface, modifier = Modifier.size(110.dp)) + Spacer(Modifier.height(50.dp)) + Text( + "SnapEnhance can't be used to login or signup because your Snapchat version isn't the recommended one (v${MOD_DETECTION_VERSION_CHECK.maxVersion?.first ?: "0.0.0"}). Please downgrade to continue using it.\n\nFor more details, join t.me/snapenhance_chat", + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + } + } + } + + LaunchedEffect(Unit) { + visibility = ViewGroup.VISIBLE + } + }) + } } } - - return safeMode } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -18,6 +18,7 @@ import me.rhunk.snapenhance.common.action.EnumAction import me.rhunk.snapenhance.common.bridge.FileHandleScope import me.rhunk.snapenhance.common.bridge.InternalFileHandleType import me.rhunk.snapenhance.common.bridge.toWrapper +import me.rhunk.snapenhance.common.config.MOD_DETECTION_VERSION_CHECK import me.rhunk.snapenhance.common.data.FriendStreaks import me.rhunk.snapenhance.common.data.MessagingFriendInfo import me.rhunk.snapenhance.common.data.MessagingGroupInfo @@ -202,7 +203,6 @@ class SnapEnhance { it.isNotEmpty() }?.toString(Charsets.UTF_8)?.also { appContext.native.signatureCache = it - appContext.log.verbose("old signature cache $it") } val lateInit = appContext.native.initOnce { @@ -223,7 +223,7 @@ class SnapEnhance { } } - val safeMode = SecurityFeatures(appContext).init() + SecurityFeatures(appContext).init() Runtime::class.java.findRestrictedMethod { it.name == "loadLibrary0" && it.parameterTypes.contentEquals( @@ -231,11 +231,18 @@ class SnapEnhance { else arrayOf(ClassLoader::class.java, String::class.java) ) }!!.apply { - if (safeMode) { + if (appContext.isSafeMode) { hook(HookStage.BEFORE) { param -> if (param.arg<String>(1) != "scplugin") return@hook param.setResult(null) - appContext.log.warn("Can't load scplugin in safe mode") + appContext.runOnUiThread { + appContext.inAppOverlay.showStatusToast( + Icons.Outlined.Cancel, + "SnapEnhance is not compatible with this version of Snapchat and will result in a ban.\nUse Snapchat ${MOD_DETECTION_VERSION_CHECK.maxVersion?.first ?: "0.0.0"} or older to avoid detections.", + durationMs = 7000, + maxLines = 6 + ) + } runCatching { Thread.sleep(Long.MAX_VALUE) }.onFailure { @@ -249,7 +256,6 @@ class SnapEnhance { hook(HookStage.AFTER) { param -> if (param.arg<String>(1) != "client") return@hook unhook() - appContext.log.verbose("libclient lateInit") lateInit() }.also { unhook = { it.unhook() } } } @@ -311,7 +317,8 @@ class SnapEnhance { } val friends = feedEntries.filter { it.conversationType == 0 }.mapNotNull { - val friendUserId = it.friendUserId ?: it.participants?.filter { it != appContext.database.myUserId }?.firstOrNull() ?: return@mapNotNull null + val friendUserId = it.friendUserId ?: it.participants?.firstOrNull { it != appContext.database.myUserId } + ?: return@mapNotNull null val friend = appContext.database.getFriendInfo(friendUserId) ?: return@mapNotNull null MessagingFriendInfo(