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