diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index 86018e9..11d700b 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -2,7 +2,8 @@ package app.revanced.patcher import app.revanced.patcher.PatchBundleLoader.Utils.getInstance import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.fingerprint.MethodFingerprint +import app.revanced.patcher.fingerprint.LookupMap.Maps.clearLookupMaps +import app.revanced.patcher.fingerprint.LookupMap.Maps.initializeLookupMaps import app.revanced.patcher.fingerprint.MethodFingerprint.Companion.resolveUsingLookupMap import app.revanced.patcher.patch.* import kotlinx.coroutines.flow.flow @@ -187,7 +188,7 @@ class Patcher( if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() - MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext) + initializeLookupMaps(context.bytecodeContext) // Prevent from decoding the app manifest twice if it is not needed. if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL) @@ -249,7 +250,7 @@ class Patcher( } } - override fun close() = MethodFingerprint.clearFingerprintResolutionLookupMaps() + override fun close() = clearLookupMaps() /** * Compile and save the patched APK file. diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt new file mode 100644 index 0000000..a0adadf --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt @@ -0,0 +1,125 @@ +package app.revanced.patcher.fingerprint + +import app.revanced.patcher.data.BytecodeContext +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.StringReference +import java.util.* + +internal typealias MethodClassPair = Pair + +/** + * Lookup map for methods. + */ +internal class LookupMap : MutableMap by mutableMapOf() { + /** + * Adds a [MethodClassPair] to the list associated with the given key. + * If the key does not exist, a new list is created and the [MethodClassPair] is added to it. + */ + fun add( + key: String, + methodClassPair: MethodClassPair + ) { + getOrPut(key) { MethodClassList() }.add(methodClassPair) + } + + /** + * List of methods and the class they are a member of. + */ + internal class MethodClassList : LinkedList() + + companion object Maps { + /** + * A list of methods and the class they are a member of. + */ + internal val methods = MethodClassList() + + /** + * Lookup map for methods keyed to the methods access flags, return type and parameter. + */ + internal val methodSignatureLookupMap = LookupMap() + + /** + * Lookup map for methods associated by strings referenced in the method. + */ + internal val methodStringsLookupMap = LookupMap() + + /** + * Initializes lookup maps for [MethodFingerprint] resolution + * using attributes of methods such as the method signature or strings. + * + * @param context The [BytecodeContext] containing the classes to initialize the lookup maps with. + */ + internal fun initializeLookupMaps(context: BytecodeContext) { + if (methods.isNotEmpty()) clearLookupMaps() + + context.classes.forEach { classDef -> + classDef.methods.forEach { method -> + val methodClassPair = method to classDef + + // For fingerprints with no access or return type specified. + methods += methodClassPair + + val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first() + + // Add as the key. + methodSignatureLookupMap.add(accessFlagsReturnKey, methodClassPair) + + // Add [parameters] as the key. + methodSignatureLookupMap.add( + buildString { + append(accessFlagsReturnKey) + appendParameters(method.parameterTypes) + }, + methodClassPair + ) + + // Add strings contained in the method as the key. + method.implementation?.instructions?.forEach instructions@{ instruction -> + if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) + return@instructions + + val string = ((instruction as ReferenceInstruction).reference as StringReference).string + + methodStringsLookupMap.add(string, methodClassPair) + } + + // In the future, the class type could be added to the lookup map. + // This would require MethodFingerprint to be changed to include the class type. + } + } + } + + /** + * Clears the internal lookup maps created in [initializeLookupMaps] + */ + internal fun clearLookupMaps() { + methods.clear() + methodSignatureLookupMap.clear() + methodStringsLookupMap.clear() + } + + /** + * Appends a string based on the parameter reference types of this method. + */ + internal fun StringBuilder.appendParameters(parameters: Iterable) { + // Maximum parameters to use in the signature key. + // Some apps have methods with an incredible number of parameters (over 100 parameters have been seen). + // To keep the signature map from becoming needlessly bloated, + // group together in the same map entry all methods with the same access/return and 5 or more parameters. + // The value of 5 was chosen based on local performance testing and is not set in stone. + val maxSignatureParameters = 5 + // Must append a unique value before the parameters to distinguish this key includes the parameters. + // If this is not appended, then methods with no parameters + // will collide with different keys that specify access/return but omit the parameters. + append("p:") + parameters.forEachIndexed { index, parameter -> + if (index >= maxSignatureParameters) return + append(parameter.first()) + } + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt index d1ee5b3..4efe14a 100644 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt @@ -2,9 +2,14 @@ package app.revanced.patcher.fingerprint import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod +import app.revanced.patcher.fingerprint.LookupMap.Maps.appendParameters +import app.revanced.patcher.fingerprint.LookupMap.Maps.initializeLookupMaps +import app.revanced.patcher.fingerprint.LookupMap.Maps.methodSignatureLookupMap +import app.revanced.patcher.fingerprint.LookupMap.Maps.methodStringsLookupMap +import app.revanced.patcher.fingerprint.LookupMap.Maps.methods +import app.revanced.patcher.fingerprint.MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.util.proxy.ClassProxy import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef @@ -12,12 +17,6 @@ import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.StringReference -import com.android.tools.smali.dexlib2.util.MethodUtil -import java.util.* - -private typealias StringMatch = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult.StringMatch -private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult -private typealias MethodClassPair = Pair /** * A fingerprint to resolve methods. @@ -43,7 +42,7 @@ abstract class MethodFingerprint( var result: MethodFingerprintResult? = null /** - * Resolve a [MethodFingerprint] using the lookup map built by [initializeFingerprintResolutionLookupMaps]. + * Resolve a [MethodFingerprint] using the lookup map built by [initializeLookupMaps]. * * [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable * amount of time because they are resolved in sequence. @@ -60,7 +59,7 @@ abstract class MethodFingerprint( * * @return A list of [MethodClassPair]s that match the methods strings present in a [MethodFingerprint]. */ - fun MethodFingerprint.methodStringsLookup(): List? { + fun MethodFingerprint.methodStringsLookup(): LookupMap.MethodClassList? { strings?.forEach { val methods = methodStringsLookupMap[it] if (methods != null) return methods @@ -73,7 +72,7 @@ abstract class MethodFingerprint( * * @return A list of [MethodClassPair]s that match the method signature present in a [MethodFingerprint]. */ - fun MethodFingerprint.methodSignatureLookup(): List { + fun MethodFingerprint.methodSignatureLookup(): LookupMap.MethodClassList { if (accessFlags == null) return methods var returnTypeValue = returnType @@ -91,7 +90,7 @@ abstract class MethodFingerprint( append(returnTypeValue.first()) if (parameters != null) appendParameters(parameters) } - return methodSignatureLookupMap[key] ?: return emptyList() + return methodSignatureLookupMap[key] ?: return LookupMap.MethodClassList() } /** @@ -99,8 +98,8 @@ abstract class MethodFingerprint( * * @return True if the resolution was successful, false otherwise. */ - fun MethodFingerprint.resolveUsingMethodClassPair(classMethods: Iterable): Boolean { - classMethods.forEach { classAndMethod -> + fun MethodFingerprint.resolveUsingMethodClassPair(methodClasses: LookupMap.MethodClassList): Boolean { + methodClasses.forEach { classAndMethod -> if (resolve(context, classAndMethod.first, classAndMethod.second)) return true } return false @@ -187,12 +186,7 @@ abstract class MethodFingerprint( val index = stringsList.indexOfFirst(string::contains) if (index == -1) return@forEachIndexed - add( - StringMatch( - string, - instructionIndex - ) - ) + add(StringsScanResult.StringMatch(string, instructionIndex)) stringsList.removeAt(index) } @@ -204,6 +198,26 @@ abstract class MethodFingerprint( val patternScanResult = if (methodFingerprint.opcodes != null) { method.implementation?.instructions ?: return false + fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.newWarnings( + pattern: Iterable, instructions: Iterable + ) = buildList { + for ((patternIndex, instructionIndex) in (this@newWarnings.startIndex until this@newWarnings.endIndex).withIndex()) { + val originalOpcode = instructions.elementAt(instructionIndex).opcode + val patternOpcode = pattern.elementAt(patternIndex) + + if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue + + this.add( + MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning( + originalOpcode, + patternOpcode, + instructionIndex, + patternIndex + ) + ) + } + } + fun Method.patternScan( fingerprint: MethodFingerprint ): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? { @@ -241,7 +255,7 @@ abstract class MethodFingerprint( index + patternIndex ) if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result - result.warnings = result.createWarnings(pattern, instructions) + result.warnings = result.newWarnings(pattern, instructions) return result } @@ -268,110 +282,7 @@ abstract class MethodFingerprint( companion object { /** - * A list of methods and the class they were found in. - */ - private val methods = mutableListOf() - - /** - * Lookup map for methods keyed to the methods access flags, return type and parameter. - */ - private val methodSignatureLookupMap = mutableMapOf>() - - /** - * Lookup map for methods keyed to the strings contained in the method. - */ - private val methodStringsLookupMap = mutableMapOf>() - - /** - * Appends a string based on the parameter reference types of this method. - */ - private fun StringBuilder.appendParameters(parameters: Iterable) { - // Maximum parameters to use in the signature key. - // Some apps have methods with an incredible number of parameters (over 100 parameters have been seen). - // To keep the signature map from becoming needlessly bloated, - // group together in the same map entry all methods with the same access/return and 5 or more parameters. - // The value of 5 was chosen based on local performance testing and is not set in stone. - val maxSignatureParameters = 5 - // Must append a unique value before the parameters to distinguish this key includes the parameters. - // If this is not appended, then methods with no parameters - // will collide with different keys that specify access/return but omit the parameters. - append("p:") - parameters.forEachIndexed { index, parameter -> - if (index >= maxSignatureParameters) return - append(parameter.first()) - } - } - - /** - * Initializes lookup maps for [MethodFingerprint] resolution - * using attributes of methods such as the method signature or strings. - * - * @param context The [BytecodeContext] containing the classes to initialize the lookup maps with. - */ - internal fun initializeFingerprintResolutionLookupMaps(context: BytecodeContext) { - fun MutableMap>.add( - key: String, - methodClassPair: MethodClassPair - ) { - var methodClassPairs = this[key] - - methodClassPairs ?: run { - methodClassPairs = LinkedList().also { this[key] = it } - } - - methodClassPairs!!.add(methodClassPair) - } - - if (methods.isNotEmpty()) clearFingerprintResolutionLookupMaps() - - context.classes.forEach { classDef -> - classDef.methods.forEach { method -> - val methodClassPair = method to classDef - - // For fingerprints with no access or return type specified. - methods += methodClassPair - - val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first() - - // Add as the key. - methodSignatureLookupMap.add(accessFlagsReturnKey, methodClassPair) - - // Add [parameters] as the key. - methodSignatureLookupMap.add( - buildString { - append(accessFlagsReturnKey) - appendParameters(method.parameterTypes) - }, - methodClassPair - ) - - // Add strings contained in the method as the key. - method.implementation?.instructions?.forEach instructions@{ instruction -> - if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) - return@instructions - - val string = ((instruction as ReferenceInstruction).reference as StringReference).string - - methodStringsLookupMap.add(string, methodClassPair) - } - - // In the future, the class type could be added to the lookup map. - // This would require MethodFingerprint to be changed to include the class type. - } - } - } - - /** - * Clears the internal lookup maps created in [initializeFingerprintResolutionLookupMaps] - */ - internal fun clearFingerprintResolutionLookupMaps() { - methods.clear() - methodSignatureLookupMap.clear() - methodStringsLookupMap.clear() - } - - /** - * Resolve a list of [MethodFingerprint] using the lookup map built by [initializeFingerprintResolutionLookupMaps]. + * Resolve a list of [MethodFingerprint] using the lookup map built by [initializeLookupMaps]. * * [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable * amount of time because they are resolved in sequence. @@ -403,26 +314,6 @@ abstract class MethodFingerprint( if (fingerprint.resolve(context, classDef)) break } } - - private fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.createWarnings( - pattern: Iterable, instructions: Iterable - ) = buildList { - for ((patternIndex, instructionIndex) in (this@createWarnings.startIndex until this@createWarnings.endIndex).withIndex()) { - val originalOpcode = instructions.elementAt(instructionIndex).opcode - val patternOpcode = pattern.elementAt(patternIndex) - - if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue - - this.add( - MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning( - originalOpcode, - patternOpcode, - instructionIndex, - patternIndex - ) - ) - } - } } } diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprintResult.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprintResult.kt index 5db1a47..318e1a0 100644 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprintResult.kt +++ b/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprintResult.kt @@ -1,6 +1,7 @@ package app.revanced.patcher.fingerprint import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.util.proxy.ClassProxy import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.Method @@ -14,6 +15,7 @@ import com.android.tools.smali.dexlib2.util.MethodUtil * @param scanResult The result of scanning for the [MethodFingerprint]. * @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies. */ +@Suppress("MemberVisibilityCanBePrivate") class MethodFingerprintResult( val method: Method, val classDef: ClassDef,