refactor: security features

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
rhunk 2025-01-02 19:18:31 +01:00
parent 23666e5528
commit 8e8220a55e
5 changed files with 132 additions and 117 deletions

View File

@ -284,10 +284,9 @@ 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 = "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")
PreferenceToggle(context.sharedPreferences, key = "disable_sif", text = "Disable Security Features")
PreferenceToggle(context.sharedPreferences, key = "disable_mod_detection_version_check", text = "Disable Mod Detection Version Check")
} }
} }
Spacer(modifier = Modifier.height(50.dp)) Spacer(modifier = Modifier.height(50.dp))

View File

@ -0,0 +1,126 @@
package me.rhunk.snapenhance.core
import android.system.Os
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NotInterested
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.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.core.util.ktx.setObjectField
import java.io.FileDescriptor
import kotlin.text.isNotEmpty
class SecurityFeatures(
private val context: ModContext
) {
private fun transact(option: Int, option2: Long) = runCatching { Os.prctl(option, option2, 0, 0, 0) }.getOrNull()
private val token by lazy {
transact(0, 0)
}
private fun getStatus() = token?.run {
transact(this, 0)?.toString(2)?.padStart(32, '0')?.count { it == '1' }
}
fun init(): Boolean {
val snapchatVersionCode = context.androidContext.packageManager?.getPackageInfo(context.androidContext.packageName, 0)?.longVersionCode ?: throw IllegalStateException("Failed to get version code")
val mustUseSafeMode = MOD_DETECTION_VERSION_CHECK.checkVersion(snapchatVersionCode)?.second == VersionRequirement.OLDER_REQUIRED
val debugDisable = context.bridgeClient.getDebugProp("disable_sif_prod", "false") != "true"
context.config.experimental.nativeHooks.customSharedLibrary.get().takeIf { it.isNotEmpty() }?.let {
runCatching {
context.native.loadSharedLibrary(
context.fileHandlerManager.getFileHandle(FileHandleScope.USER_IMPORT.key, it).toWrapper().readBytes()
)
context.log.verbose("loaded custom shared library")
}.onFailure {
context.log.error("Failed to load custom shared library", it)
return true
}
}
if (debugDisable && !mustUseSafeMode) {
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")
}
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 = mustUseSafeMode || (status == null || status < 2)
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
}
}

View File

@ -223,35 +223,7 @@ class SnapEnhance {
} }
} }
appContext.config.experimental.nativeHooks.customSharedLibrary.get().takeIf { it.isNotEmpty() }?.let { val safeMode = SecurityFeatures(appContext).init()
runCatching {
appContext.native.loadSharedLibrary(
appContext.fileHandlerManager.getFileHandle(FileHandleScope.USER_IMPORT.key, it).toWrapper().readBytes()
)
appContext.log.verbose("loaded custom shared library")
}.onFailure {
appContext.log.error("Failed to load custom shared library", it)
}
}
if (appContext.bridgeClient.getDebugProp("disable_sif", "false") != "true") {
runCatching {
appContext.native.loadSharedLibrary(
appContext.fileHandlerManager.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.SIF.key)
.toWrapper()
.readBytes()
.takeIf {
it.isNotEmpty()
} ?: throw IllegalStateException("buffer is empty")
)
appContext.log.verbose("loaded sif")
}.onFailure {
safeMode = true
appContext.log.error("Failed to load sif", it)
}
} else {
appContext.log.warn("sif is disabled")
}
Runtime::class.java.findRestrictedMethod { Runtime::class.java.findRestrictedMethod {
it.name == "loadLibrary0" && it.parameterTypes.contentEquals( it.name == "loadLibrary0" && it.parameterTypes.contentEquals(

View File

@ -28,6 +28,10 @@ class FeatureManager(
private val features = mutableMapOf<KClass<out Feature>, Feature>() private val features = mutableMapOf<KClass<out Feature>, Feature>()
private val onActivityCreateListeners = mutableListOf<(Activity) -> Unit>() private val onActivityCreateListeners = mutableListOf<(Activity) -> Unit>()
fun addActivityCreateListener(block: (Activity) -> Unit) {
onActivityCreateListeners.add(block)
}
private fun register(vararg featureList: Feature) { private fun register(vararg featureList: Feature) {
if (context.bridgeClient.getDebugProp("disable_feature_loading") == "true") { if (context.bridgeClient.getDebugProp("disable_feature_loading") == "true") {
context.log.warn("Feature loading is disabled") context.log.warn("Feature loading is disabled")
@ -61,7 +65,6 @@ class FeatureManager(
fun init() { fun init() {
register( register(
Debug(), Debug(),
SecurityFeatures(),
EndToEndEncryption(), EndToEndEncryption(),
ScopeSync(), ScopeSync(),
PreventMessageListAutoScroll(), PreventMessageListAutoScroll(),

View File

@ -1,85 +0,0 @@
package me.rhunk.snapenhance.core.features.impl
import android.annotation.SuppressLint
import android.system.Os
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NotInterested
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.core.features.Feature
import me.rhunk.snapenhance.core.util.ktx.setObjectField
import java.io.FileDescriptor
class SecurityFeatures : Feature("Security Features") {
private fun transact(option: Int, option2: Long) = runCatching { Os.prctl(option, option2, 0, 0, 0) }.getOrNull()
private val token by lazy {
transact(0, 0)
}
private fun getStatus() = token?.run {
transact(this, 0)?.toString(2)?.padStart(32, '0')?.count { it == '1' }
}
@SuppressLint("SetTextI18n")
override fun init() {
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)?.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()
}!!
buffer = ProtoEditor(buffer).apply {
edit(errorDataIndex) {
remove(1)
addString(1, status)
}
}.toByteArray()
}
}
}
val status = getStatus()
val canCheckVersion = context.bridgeClient.getDebugProp("disable_mod_detection_version_check", "false") != "true"
val snapchatVersionCode = context.androidContext.packageManager.getPackageInfo(context.androidContext.packageName, 0).longVersionCode
if (canCheckVersion && MOD_DETECTION_VERSION_CHECK.checkVersion(snapchatVersionCode)?.second == VersionRequirement.OLDER_REQUIRED && (status == null || status < 2)) {
onNextActivityCreate {
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
)
}
}
}
}