refactor(core): security features

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
rhunk 2025-04-10 18:40:07 +02:00
parent 62350a048c
commit 9761d73ece
4 changed files with 86 additions and 93 deletions

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -52,9 +53,9 @@ class HomeSettings : Routes.Route() {
.clickable { .clickable {
value = !value value = !value
sharedPreferences sharedPreferences
.edit() .edit() {
.putBoolean(realKey, value) putBoolean(realKey, value)
.apply() }
}, },
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -284,7 +285,7 @@ class HomeSettings : Routes.Route() {
Column( Column(
verticalArrangement = Arrangement.spacedBy(4.dp), 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_feature_loading", text = "Disable Feature Loading")
PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = "Disable Auto Mapper") PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = "Disable Auto Mapper")
} }

View File

@ -75,6 +75,7 @@ class ModContext(
val isDeveloper by lazy { config.scripting.developerMode.get() } val isDeveloper by lazy { config.scripting.developerMode.get() }
var isMainActivityPaused = true var isMainActivityPaused = true
var isSafeMode = false
fun <T : Feature> feature(featureClass: KClass<T>): T { fun <T : Feature> feature(featureClass: KClass<T>): T {
return features.get(featureClass)!! return features.get(featureClass)!!

View File

@ -1,16 +1,20 @@
package me.rhunk.snapenhance.core package me.rhunk.snapenhance.core
import android.system.Os import android.system.Os
import androidx.compose.foundation.layout.Row import android.view.ViewGroup
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check 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.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import me.rhunk.snapenhance.common.bridge.FileHandleScope 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.bridge.toWrapper
import me.rhunk.snapenhance.common.config.MOD_DETECTION_VERSION_CHECK import me.rhunk.snapenhance.common.config.MOD_DETECTION_VERSION_CHECK
import me.rhunk.snapenhance.common.config.VersionRequirement import me.rhunk.snapenhance.common.config.VersionRequirement
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.common.ui.createComposeView
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
import me.rhunk.snapenhance.core.ui.CustomComposable 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( class SecurityFeatures(
private val context: ModContext private val context: ModContext
@ -39,10 +38,9 @@ class SecurityFeatures(
transact(this, 0)?.toString(2)?.padStart(32, '0')?.count { it == '1' } 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 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 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 { context.config.experimental.nativeHooks.customSharedLibrary.get().takeIf { it.isNotEmpty() }?.let {
runCatching { runCatching {
@ -54,74 +52,28 @@ class SecurityFeatures(
context.log.error("Failed to load custom shared library", it) context.log.error("Failed to load custom shared library", it)
return true return true
} }
} } ?: context.bridgeClient.getDebugProp("enable_security_features", "false").takeIf { it == "true" }?.runCatching {
context.native.loadSharedLibrary(
if (!debugDisable) { context.fileHandlerManager.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.SIF.key)
runCatching { .toWrapper()
context.native.loadSharedLibrary( .readBytes()
context.fileHandlerManager.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.SIF.key) .takeIf {
.toWrapper() it.isNotEmpty()
.readBytes() } ?: throw IllegalStateException("Binary is empty")
.takeIf { )
it.isNotEmpty() context.log.verbose("loaded sif")
} ?: throw IllegalStateException("buffer is empty") }?.onFailure {
) context.log.error("Failed to load sif: " + it.message)
context.log.verbose("loaded sif") return shouldUseSafeMode
}.onFailure { } ?: context.log.warn("Security features are disabled")
context.log.error("Failed to load sif", it)
return true
}
} else {
context.log.warn("sif is disabled")
}
token // pre init token 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 status = getStatus()
val safeMode = shouldUseSafeMode && (status == null || status < 2) val safeMode = shouldUseSafeMode && (status == null || status < 2)
if (status != null && status >= 2) { if (status != null && status >= 2) {
context.log.verbose("status=$status")
lateinit var composable: CustomComposable lateinit var composable: CustomComposable
composable = { composable = {
Row( Row(
@ -140,17 +92,49 @@ class SecurityFeatures(
context.inAppOverlay.addCustomComposable(composable) 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 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
}
})
}
}
}
}
} }

View File

@ -18,6 +18,7 @@ import me.rhunk.snapenhance.common.action.EnumAction
import me.rhunk.snapenhance.common.bridge.FileHandleScope import me.rhunk.snapenhance.common.bridge.FileHandleScope
import me.rhunk.snapenhance.common.bridge.InternalFileHandleType import me.rhunk.snapenhance.common.bridge.InternalFileHandleType
import me.rhunk.snapenhance.common.bridge.toWrapper 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.FriendStreaks
import me.rhunk.snapenhance.common.data.MessagingFriendInfo import me.rhunk.snapenhance.common.data.MessagingFriendInfo
import me.rhunk.snapenhance.common.data.MessagingGroupInfo import me.rhunk.snapenhance.common.data.MessagingGroupInfo
@ -202,7 +203,6 @@ class SnapEnhance {
it.isNotEmpty() it.isNotEmpty()
}?.toString(Charsets.UTF_8)?.also { }?.toString(Charsets.UTF_8)?.also {
appContext.native.signatureCache = it appContext.native.signatureCache = it
appContext.log.verbose("old signature cache $it")
} }
val lateInit = appContext.native.initOnce { val lateInit = appContext.native.initOnce {
@ -223,7 +223,7 @@ class SnapEnhance {
} }
} }
val safeMode = SecurityFeatures(appContext).init() SecurityFeatures(appContext).init()
Runtime::class.java.findRestrictedMethod { Runtime::class.java.findRestrictedMethod {
it.name == "loadLibrary0" && it.parameterTypes.contentEquals( it.name == "loadLibrary0" && it.parameterTypes.contentEquals(
@ -231,11 +231,18 @@ class SnapEnhance {
else arrayOf(ClassLoader::class.java, String::class.java) else arrayOf(ClassLoader::class.java, String::class.java)
) )
}!!.apply { }!!.apply {
if (safeMode) { if (appContext.isSafeMode) {
hook(HookStage.BEFORE) { param -> hook(HookStage.BEFORE) { param ->
if (param.arg<String>(1) != "scplugin") return@hook if (param.arg<String>(1) != "scplugin") return@hook
param.setResult(null) 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 { runCatching {
Thread.sleep(Long.MAX_VALUE) Thread.sleep(Long.MAX_VALUE)
}.onFailure { }.onFailure {
@ -249,7 +256,6 @@ class SnapEnhance {
hook(HookStage.AFTER) { param -> hook(HookStage.AFTER) { param ->
if (param.arg<String>(1) != "client") return@hook if (param.arg<String>(1) != "client") return@hook
unhook() unhook()
appContext.log.verbose("libclient lateInit")
lateInit() lateInit()
}.also { unhook = { it.unhook() } } }.also { unhook = { it.unhook() } }
} }
@ -311,7 +317,8 @@ class SnapEnhance {
} }
val friends = feedEntries.filter { it.conversationType == 0 }.mapNotNull { 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 val friend = appContext.database.getFriendInfo(friendUserId) ?: return@mapNotNull null
MessagingFriendInfo( MessagingFriendInfo(