commit 9d4256ffe8a3f796723e79c0bb3ef00cb98d0f38
parent e26ea0121fdda620a0a1be2910effcb3c4e8048e
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Wed, 30 Jul 2025 16:49:42 +0200

Merge branch 'refs/heads/loading_fix' into dev

Diffstat:
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SecurityFeatures.kt | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 11++++-------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlatformClientAttestationMapper.kt | 13+++++++++++++
4 files changed, 96 insertions(+), 14 deletions(-)

diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt @@ -61,7 +61,7 @@ class ModContext( val messageSender = MessageSender(this) val features = FeatureManager(this) - val mappings by lazy { MappingsWrapper(lazyFileHandlerManager) } + val mappings by lazy { MappingsWrapper(lazyFileHandlerManager).apply { init(androidContext) } } val actionManager = ActionManager(this) val database = DatabaseAccess(this) val event = EventBus(this) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SecurityFeatures.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SecurityFeatures.kt @@ -24,10 +24,16 @@ import me.rhunk.snapenhance.common.config.VersionRequirement import me.rhunk.snapenhance.common.ui.createComposeView import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent import me.rhunk.snapenhance.core.ui.CustomComposable +import me.rhunk.snapenhance.core.util.dataBuilder import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.mapper.impl.CallbackMapper +import me.rhunk.snapenhance.mapper.impl.PlatformClientAttestationMapper +import java.io.IOException +import java.lang.reflect.Method +import kotlin.system.exitProcess class SecurityFeatures( private val context: ModContext @@ -85,21 +91,87 @@ class SecurityFeatures( context.log.verbose("disablePlugin=${context.disablePlugin}") if (!context.disablePlugin) return + val allowedEPs = listOf( + "/messagingcoreservice.MessagingCoreService/", + "/GetConvoSafetyPrompt", + "/GetSnapchatterPublicInfo", + "/UserRecentlyActive", + ) + context.event.subscribe(UnaryCallEvent::class) { event -> val callOptions = event.adapter.arg<Any>(2).let { it.javaClass.getMethod("build").invoke(it) } ?: return@subscribe if (callOptions.getObjectField("mAttestation") != null) { + context.log.verbose("blocked ep ${event.adapter.arg<Any>(0)}", "UnaryCallEvent") event.canceled = true + val eventHandler = event.adapter.arg<Any>(3) + eventHandler.javaClass.methods.first { it.name == "onEvent" }.also { method -> + method.invoke(eventHandler, null, method.parameterTypes[0].dataBuilder { + set("mStatusCode", "CANCELLED") + }) + } } } - context.androidContext.classLoader.loadClass("com.snapchat.client.client_attestation.ArgosClient\$CppProxy").apply { - hook("getArgosTokenAsync", HookStage.BEFORE) { it.setResult(null) } - hook("getAttestationHeaders", HookStage.BEFORE) { it.setResult(null) } + context.androidContext.classLoader.apply { + val argosClientClass = loadClass("com.snapchat.client.client_attestation.ArgosClient\$CppProxy") + argosClientClass.apply { + hookConstructor(HookStage.BEFORE) { it.setResult(null) } + hook("getArgosTokenAsync", HookStage.BEFORE) { it.setResult(null) } + hook("getAttestationHeaders", HookStage.BEFORE) { it.setResult(null) } + } + loadClass("com.snapchat.client.client_attestation.ArgosClient").hook("createInstance", HookStage.BEFORE) { param -> + param.setResult(argosClientClass.declaredConstructors.first().also { it.isAccessible = true }.newInstance(0)) + } + loadClass("com.snap.security.attestation.impl.SCClientAttestationDurableJob").hookConstructor(HookStage.BEFORE) { param -> + param.setArg(0, null) + } + loadClass("com.snapchat.client.grpc.AuthContext").hookConstructor(HookStage.AFTER) { param -> + val headers by lazy { (param.thisObject<Any>().getObjectField("mHeaders") as? List<*>)?.filterNotNull() ?: emptyList() } + + if (param.thisObject<Any>().getObjectField("mAuthTokenErrorCode") != null || + headers.isEmpty() || + headers.mapNotNull { it.getObjectField("mKey")?.toString()?.lowercase() }.any { it != "x-snap-access-token" } + ) { + context.log.error("invalid headers ${headers.size}") + exitProcess(139) + } + } } - context.androidContext.classLoader.loadClass("com.snap.security.attestation.impl.SCClientAttestationDurableJob") - .hookConstructor(HookStage.BEFORE) { param -> - param.setArg(0, null) + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("AuthContextDelegate")?.hook("getAuthContext", HookStage.BEFORE) { param -> + val authContextRequest = param.arg<Any>(0) + val requestPath = authContextRequest.getObjectField("mRequestPath").toString() + + if (authContextRequest.getObjectField("mAttestationRequired") == true) { + if (allowedEPs.any { requestPath.contains(it) }) { + context.log.verbose("ep $requestPath", "AuthContextDelegate") + return@hook + } + + context.log.verbose("blocked ep $requestPath", "AuthContextDelegate") + param.setResult(null) + } + } ?: error("AuthContextDelegate not found in mappings") + } + + context.mappings.useMapper(PlatformClientAttestationMapper::class) { + apiInvocationHandler.getAsClass()?.hook("invoke", HookStage.BEFORE) { param -> + val method = param.arg<Method>(1) + if (method.annotations.any { it.toString().contains("attestation") }) { + context.log.verbose("blocked call ${method.declaringClass.name}.${method.name}(...)") + if (method.returnType.name.endsWith("Single")) { + param.setResult( + method.returnType.methods.first { + java.lang.reflect.Modifier.isStatic(it.modifiers) && it.parameterCount == 1 && it.parameterTypes[0] == Throwable::class.java + }.invoke(null, IOException()) + ) + return@hook + } + + param.setResult(null) + } + } ?: context.log.warn("apiInvocationHandler not found in mappings") } context.features.addActivityCreateListener { activity -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -154,14 +154,13 @@ class SnapEnhance { } reloadConfig() - initWidgetListener() initNative() + initWidgetListener() scope.launch(Dispatchers.IO) { translation.userLocale = getConfigLocale() translation.load() } - mappings.init(androidContext) database.init() eventDispatcher.init() userInterface.init() @@ -243,12 +242,10 @@ class SnapEnhance { it.declaringClass == pluginNativeClass.getAsClass() }?.forEach { method -> method.hook(HookStage.BEFORE) { - appContext.log.verbose("called $method") - if (Throwable().stackTrace.lastOrNull()?.methodName == "getAttestationPayloadProto") { - appContext.log.verbose("sleeping") - Thread.sleep(Long.MAX_VALUE) - } + appContext.log.error("Calling $method", Throwable()) it.setResult(null) + runCatching { exitProcess(139) } + runCatching { Thread.sleep(Long.MAX_VALUE) } } } ?: error("Failed to get pluginNativeClass class") } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlatformClientAttestationMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlatformClientAttestationMapper.kt @@ -4,14 +4,27 @@ import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c import com.android.tools.smali.dexlib2.iface.reference.MethodReference import me.rhunk.snapenhance.mapper.AbstractClassMapper +import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getSuperClassName class PlatformClientAttestationMapper: AbstractClassMapper("PlatformClientAttestationMapper") { val pluginNativeClass = classReference("pluginNativeClass") + val apiInvocationHandler = classReference("apiInvocationHandler") init { mapper { for (clazz in classes) { + if (clazz.interfaces.firstOrNull()?.endsWith("InvocationHandler;") != true) continue + val invokeMethod = clazz.methods.firstOrNull { it.name == "invoke" } ?: continue + invokeMethod.implementation?.instructions?.firstOrNull { it is Instruction35c && (it.reference as? MethodReference)?.name == "getDeclaringClass" } ?: continue + + apiInvocationHandler.set(clazz.getClassName()) + return@mapper + } + } + + mapper { + for (clazz in classes) { if (clazz.getSuperClassName()?.endsWith("PlatformClientAttestation") != true) continue val getSignatureMethod = clazz.methods.firstOrNull { it.name == "getSignature" } ?: continue