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.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")
}

View File

@ -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)!!

View File

@ -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
}
})
}
}
}
}
}

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