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