commit 2b0b14a60f98f68c07ed1cae1be00835f528848c
parent 233d043d454214b118b7fb3cc0a8a9c0946196e1
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Fri, 27 Oct 2023 01:05:26 +0200

refactor: config override
- fix: hidden sc plus features

Diffstat:
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/util/hook/Hooker.kt | 12++++++++++++
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt | 4++--
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt | 47++++++++++++++++++++++++++++++++++++++++++-----
5 files changed, 136 insertions(+), 30 deletions(-)

diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt @@ -4,14 +4,36 @@ import de.robv.android.xposed.XposedHelpers import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField +data class ConfigKeyInfo( + val category: String?, + val name: String?, + val defaultValue: Any? +) + class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val propertyOverrides = mutableMapOf<String, Pair<(() -> Boolean), Any>>() + val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") + val enumMappings = compositeConfigurationProviderMappings["enum"] as Map<*, *> + + fun getConfigKeyInfo(key: Any?) = runCatching { + if (key == null) return@runCatching null + val keyClassMethods = key::class.java.methods + val category = keyClassMethods.firstOrNull { it.name == enumMappings["getCategory"].toString() }?.invoke(key)?.toString() ?: return null + val valueHolder = keyClassMethods.firstOrNull { it.name == enumMappings["getValue"].toString() }?.invoke(key) ?: return null + val defaultValue = valueHolder.getObjectField(enumMappings["defaultValueField"].toString()) ?: return null + ConfigKeyInfo(category, key.toString(), defaultValue) + }.onFailure { + context.log.error("Failed to get config key info", it) + }.getOrNull() - fun overrideProperty(key: String, filter: () -> Boolean, value: Any) { + val propertyOverrides = mutableMapOf<String, Pair<((ConfigKeyInfo) -> Boolean), Any>>() + + fun overrideProperty(key: String, filter: (ConfigKeyInfo) -> Boolean, value: Any) { propertyOverrides[key] = Pair(filter, value) } @@ -33,8 +55,17 @@ class ConfigurationOverride : Feature("Configuration Override", loadParams = Fea overrideProperty(it, { context.config.global.blockAds.get() }, "http://127.0.0.1") } - val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") - val enumMappings = compositeConfigurationProviderMappings["enum"] as Map<*, *> + findClass(compositeConfigurationProviderMappings["class"].toString()).hook( + compositeConfigurationProviderMappings["getProperty"].toString(), + HookStage.AFTER + ) { param -> + val propertyKey = getConfigKeyInfo(param.argNullable<Any>(0)) ?: return@hook + + propertyOverrides[propertyKey.name]?.let { (filter, value) -> + if (!filter(propertyKey)) return@let + param.setResult(value) + } + } findClass(compositeConfigurationProviderMappings["class"].toString()).hook( compositeConfigurationProviderMappings["observeProperty"].toString(), @@ -48,34 +79,59 @@ class ConfigurationOverride : Feature("Configuration Override", loadParams = Fea } propertyOverrides[key]?.let { (filter, value) -> - if (!filter()) return@let + if (!filter(getConfigKeyInfo(enumData) ?: return@let)) return@let setValue(value) } } - findClass(compositeConfigurationProviderMappings["class"].toString()).hook( - compositeConfigurationProviderMappings["getProperty"].toString(), - HookStage.AFTER - ) { param -> - val propertyKey = param.arg<Any>(0).toString() + runCatching { + val appExperimentProviderMappings = compositeConfigurationProviderMappings["appExperimentProvider"] as Map<*, *> + val customBooleanPropertyRules = mutableListOf<(ConfigKeyInfo) -> Boolean>() - propertyOverrides[propertyKey]?.let { (filter, value) -> - if (!filter()) return@let - param.setResult(value) + findClass(appExperimentProviderMappings["GetBooleanAppExperimentClass"].toString()).hook("invoke", HookStage.BEFORE) { param -> + val keyInfo = getConfigKeyInfo(param.arg(1)) ?: return@hook + if (keyInfo.defaultValue !is Boolean) return@hook + if (customBooleanPropertyRules.any { it(keyInfo) }) { + param.setResult(true) + return@hook + } + propertyOverrides[keyInfo.name]?.let { (filter, value) -> + if (!filter(keyInfo)) return@let + param.setResult(value) + } } - } - arrayOf("getBoolean", "getInt", "getLong", "getFloat", "getString").forEach { methodName -> - findClass("android.app.SharedPreferencesImpl").hook( - methodName, - HookStage.BEFORE - ) { param -> - val key = param.argNullable<Any>(0).toString() - propertyOverrides[key]?.let { (filter, value) -> - if (!filter()) return@let - param.setResult(value) + Hooker.ephemeralHookConstructor( + findClass(compositeConfigurationProviderMappings["class"].toString()), + HookStage.AFTER + ) { constructorParam -> + val instance = constructorParam.thisObject<Any>() + val appExperimentProviderInstance = instance::class.java.fields.firstOrNull { + findClass(appExperimentProviderMappings["class"].toString()).isAssignableFrom(it.type) + }?.get(instance) ?: return@ephemeralHookConstructor + + appExperimentProviderInstance::class.java.methods.first { + it.name == appExperimentProviderMappings["hasExperimentMethod"].toString() + }.hook(HookStage.BEFORE) { param -> + val keyInfo = getConfigKeyInfo(param.arg(0)) ?: return@hook + if (keyInfo.defaultValue !is Boolean) return@hook + if (customBooleanPropertyRules.any { it(keyInfo) }) { + param.setResult(true) + return@hook + } + + val propertyOverride = propertyOverrides[keyInfo.name] ?: return@hook + if (propertyOverride.first(keyInfo)) param.setResult(true) + } + } + + if (context.config.experimental.hiddenSnapchatPlusFeatures.get()) { + customBooleanPropertyRules.add { key -> + key.category == "PLUS" && key.name?.endsWith("_GATE") == true } } + }.onFailure { + context.log.error("Failed to hook appExperimentProvider", it) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt @@ -26,6 +26,7 @@ class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_ param.setArg(3, expirationTimeMillis) } + // optional as ConfigurationOverride does this too if (context.config.experimental.hiddenSnapchatPlusFeatures.get()) { findClass("com.snap.plus.FeatureCatalog").methods.last { !it.name.contains("init") && diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/hook/Hooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/hook/Hooker.kt @@ -113,6 +113,18 @@ object Hooker { hookConsumer(param) }.also { unhooks.addAll(it) } } + + inline fun ephemeralHookConstructor( + clazz: Class<*>, + stage: HookStage, + crossinline hookConsumer: (HookAdapter) -> Unit + ) { + val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() + hookConstructor(clazz, stage) { param-> + hookConsumer(param) + unhooks.forEach{ it.unhook() } + }.also { unhooks.addAll(it) } + } } fun Class<*>.hookConstructor( diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt @@ -20,9 +20,9 @@ class MapperContext( return classMap[name.toString()] } - private val mappings = mutableMapOf<String, Any>() + private val mappings = mutableMapOf<String, Any?>() - fun addMapping(key: String, vararg array: Pair<String, Any>) { + fun addMapping(key: String, vararg array: Pair<String, Any?>) { mappings[key] = array.toMap() } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt @@ -1,10 +1,11 @@ package me.rhunk.snapenhance.mapper.impl import me.rhunk.snapenhance.mapper.AbstractClassMapper -import me.rhunk.snapenhance.mapper.ext.findConstString -import me.rhunk.snapenhance.mapper.ext.getClassName -import me.rhunk.snapenhance.mapper.ext.hasStaticConstructorString -import me.rhunk.snapenhance.mapper.ext.isEnum +import me.rhunk.snapenhance.mapper.ext.* +import org.jf.dexlib2.iface.instruction.formats.Instruction21c +import org.jf.dexlib2.iface.instruction.formats.Instruction35c +import org.jf.dexlib2.iface.reference.FieldReference +import org.jf.dexlib2.iface.reference.MethodReference import java.lang.reflect.Modifier class CompositeConfigurationProviderMapper : AbstractClassMapper() { @@ -32,7 +33,35 @@ class CompositeConfigurationProviderMapper : AbstractClassMapper() { it.parameterTypes[2] == enumType.type } + val hasExperimentMethodReference = observePropertyMethod.implementation?.instructions?.firstOrNull { instruction -> + if (instruction !is Instruction35c) return@firstOrNull false + (instruction.reference as? MethodReference)?.let { methodRef -> + methodRef.returnType == "Z" && methodRef.parameterTypes.size == 1 && methodRef.parameterTypes[0] == configEnumInterface.type + } == true + }?.let { (it as Instruction35c).reference as MethodReference } + + val getBooleanAppExperimentClass = classDef.methods.first { + // search for observeBoolean method + it.parameterTypes.size == 1 && + it.parameterTypes[0] == configEnumInterface.type && + it.implementation?.findConstString("observeBoolean") == true + }.let { method -> + // search for static field invocation of GetBooleanAppExperiment class + val getBooleanAppExperimentClassFieldInstruction = method.implementation?.instructions?.firstOrNull { instruction -> + if (instruction !is Instruction21c) return@firstOrNull false + val fieldReference = instruction.reference as? FieldReference ?: return@firstOrNull false + getClass(fieldReference.definingClass)?.methods?.any { + it.returnType == "Ljava/lang/Object;" && + it.parameterTypes.size == 2 && + (0..1).all { i -> it.parameterTypes[i] == "Ljava/lang/Object;" } + } == true + }?.let { (it as Instruction21c).reference as FieldReference } + + getClass(getBooleanAppExperimentClassFieldInstruction?.definingClass)?.getClassName() + } + val enumGetDefaultValueMethod = configEnumInterface.methods.first { getClass(it.returnType)?.interfaces?.contains("Ljava/io/Serializable;") == true } + val enumGetCategoryMethod = configEnumInterface.methods.first { it.parameterTypes.size == 0 && getClass(it.returnType)?.isEnum() == true } val defaultValueField = getClass(enumGetDefaultValueMethod.returnType)!!.fields.first { Modifier.isFinal(it.accessFlags) && Modifier.isPublic(it.accessFlags) && @@ -46,8 +75,16 @@ class CompositeConfigurationProviderMapper : AbstractClassMapper() { "enum" to mapOf( "class" to configEnumInterface.getClassName(), "getValue" to enumGetDefaultValueMethod.name, + "getCategory" to enumGetCategoryMethod.name, "defaultValueField" to defaultValueField.name - ) + ), + "appExperimentProvider" to (hasExperimentMethodReference?.let { + mapOf( + "class" to getClass(it.definingClass)?.getClassName(), + "GetBooleanAppExperimentClass" to getBooleanAppExperimentClass, + "hasExperimentMethod" to hasExperimentMethodReference.name + ) + }) ) return@mapper }