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:
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(